# @objectstack/formula

## 17.6.0

### Patch Changes

- 3fbf3ca: Refusals, log lines and field help in core, the in-memory and MongoDB drivers, formula, metadata, metadata-core, objectql and platform-objects no longer cite tracker numbers; each states the reason in words
  
  Clause-②: no
  
  Many messages these packages show to authors, administrators and operators ended with an issue-tracker
  number where the reason belonged. The number goes, and where the sentence did not already say what was
  decided, it now does. Where an ADR stood beside the number, the ADR stays.
  
  - Refusals and prescriptions: the retired health-check keys, the `IMetadataService.register` refusals
    (the contract refuses loudly and names the mismatch, never coerces a value into storability), the
    kernel's plugin-ordering errors (registration order is not a contract), the in-memory and MongoDB
    filter and aggregation refusals, formula's empty field constraint, the retired `artifact-api`
    source, and the by-id update and delete refusals. The MongoDB retired-aggregate refusal now says the
    function left `AggregationFunction` because no SQL backend compiled it; its undeclared-aggregate
    refusal says the builder used to sum an unrecognised name before this refusal existed.
  - The `findOne` no-predicate refusal loses its citation in `objectql` and in `metadata-core`'s
    `engineFindOnePredicateRefusalMessage` together, so the two still read byte for byte the same.
  - The in-memory and MongoDB drivers' multi-tenancy refusals (`MEMORY_MULTI_TENANT_UNSUPPORTED`,
    `MONGODB_MULTI_TENANT_UNSUPPORTED`) no longer end with a `Tracking:` line linking a tracker card;
    the sentence above it already says the driver refuses rather than run or answer unisolated.
  - Field help and protection text: the `sys_account` token help (and its es-ES, ja-JP and zh-CN
    translations), the `sys_email` headers help and the SCIM credential store's protection reason.
  - Log lines: the superseded-registration warning, the authz cache posture line, the endpoint matcher's
    excluded-item error, the metadata history and loader-read failure errors, and the fresh-datastore
    attestation info lines.
  
  Text only: no error code, field name, status or behaviour changes.
- 1a75e39: fix(spec,drivers): a `datetime` filter `$lte '9999-12-31'`, or a `$between` whose maximum is that day, includes the whole last supported day on every backend (#20600)
  
  Clause-②: yes (widening) — three new exports on `@objectstack/spec` (`data`) and `@objectstack/core`: the constant `UNBOUNDED_ABOVE`, its type `UnboundedAbove` and the guard `isUnboundedAbove`; `nextUtcCalendarDay` answers the constant for one input that used to answer a string. Nothing any door accepted before is refused, and nothing is removed or renamed.
  
  **BREAKING for TypeScript and JavaScript callers of `nextUtcCalendarDay`** (`@objectstack/spec/data`, re-exported by `@objectstack/core`): its return type gains a member and its answer for one input changes from a string to a symbol, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). No filter an author writes and no stored row changes meaning except that a whole-day upper bound on `9999-12-31` now includes that day.
  
  `9999-12-31` is the last day of the supported years (0001..9999). A bare-day upper bound on a `datetime` field — `$lte`, a `$between` maximum, an analytics `dateRange` end — means that whole day, and is compiled as "before the next day's midnight". That day has no next day with a `YYYY-MM-DD` spelling: `nextUtcCalendarDay('9999-12-31')` answered the five-digit `'10000-01-01'`, which sorts below `'2026-…'` as text. So on SQLite, where a `datetime` column is ISO text, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered no rows; PostgreSQL parsed the bound as an instant and answered them. The memory and mongo drivers, the analytics strategies and the draft preview built their bound from the same answer, and `formula`'s RLS `check` evaluator compared a `'2026-…'` value against it and denied the write.
  
  Every supported value is at most the last millisecond of `9999-12-31`, so that day's whole-day bound bounds nothing. `nextUtcCalendarDay('9999-12-31')` now answers `UNBOUNDED_ABOVE`, a symbol that is neither `null` ("not a calendar day", which would compile the day's midnight and miss the rest of it) nor a string, and every backend compiles no upper bound for it:
  
  - `$lte` / `<=` on that day asks only that the value is not null: `IS NOT NULL` on the SQL drivers and the analytics echo, `$ne: null` on the memory and mongo drivers.
  - A `$between` / `between` whose maximum is that day, and an explicit analytics `dateRange` ending on it, keep only their minimum.
  - The type-blind `formula` `check` evaluator and the draft preview admit every value that denotes an instant, and compare any other value as written.
  - `$gte`, `$gt`, `$lt` and `$eq` on that day are unchanged: they anchor to its midnight, as on every other day. `9999-12-30` and every earlier day compile the same bound as before.
  
  Measured through `POST /api/v1/data/:object/query`, rows at `2026-07-15T14:00Z`, `9999-12-30T10:00Z`, `9999-12-31T00:00Z`, `T10:00Z` and `T23:59:59.999Z`: on SQLite, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered none of them and now answer all five; `$between ['9999-12-31', '9999-12-31']` answered none and now answers the three on that day. PostgreSQL 16 answers the same before and after. `$lte '9999-12-30'` answers the first two rows on both, before and after.
  
  **If your code stops compiling.** `nextUtcCalendarDay` now returns `string | UnboundedAbove | null`, where `UnboundedAbove` is a `symbol` with a structural brand. TypeScript refuses that member in a template literal (TS2731), a relational comparison (TS2469) and a `string` parameter (TS2345), so code that used the answer as a day string no longer compiles until it handles the last day. Test the answer with `isUnboundedAbove(answer)` (or `typeof answer === 'symbol'`) first: on its false branch the answer is `string | null` as before, and on its true branch there is no upper bound to compile. `answer === UNBOUNDED_ABOVE` compares correctly but does not narrow, because the branded type is not a unit type. The type is structural on purpose: `@objectstack/spec` ships `./data` as `index.d.mts` and `index.d.ts`, and a `unique symbol` would be two unrelated types in a program that meets both.
  
  **If your JavaScript code handled the answer as text.** For `'9999-12-31'` it is now a registered symbol (`Symbol.for('objectstack.calendarDay.unboundedAbove')`), not `'10000-01-01'`: a template literal or a relational comparison on it throws a `TypeError`, and better-sqlite3 and `pg` refuse to bind it. Every other input answers exactly as before.
  
  The shared temporal conformance kit (`TEMPORAL_ROWS` / `TEMPORAL_CASES` in `@objectstack/spec/data`) gains the row `z_last` (`9999-12-31T10:00:00.000Z`) and five last-day cases, so every backend it drives is held to this answer; three existing `$gte` / `$gt` cases now also expect `z_last`.
  
  <!-- adr-0087: not-required (no-migration-prescription) Nothing an author writes moves — no spec key, no stored row and no accept set changes, so `objectstack migrate meta` has nothing to reach — and what moves is one published function's return type and its answer for one input, whose channel is the caller's compiler and the banner above. -->
- e18fea6: fix(objectql,driver-mongodb,formula): the `having` and per-aggregation evaluator compiles the whole-day comparison it is handed, and `$contains` asks membership on a JSON-stored field in `MongoDBDriver` and in `matchesFilterCondition` (ADR-0053 D-D1 items 5 and 9; the `FILTER_OPERATORS` `$contains` contract, #20822)
  
  Clause-②: no
  
  - **`@objectstack/objectql`: the aggregate evaluator's own whole-day copy is deleted.** The walker behind `having` and `aggregations[i].filter` no longer widens a bare `YYYY-MM-DD` `$lte`, or a `$between` maximum, on a `datetime` column to the whole day, and no longer drops the bound on `9999-12-31`. Through `engine.aggregate` nothing changes: the engine's seam lowers both positions with the shared `lowerFilterCondition` (`@objectstack/spec/data`) before the walker runs, by the object's declared `datetime` fields for the per-aggregation `filter` and by the aggregated column's type for `having`. A caller that passes no seam gets the comparison it wrote: `applyInMemoryAggregation(rows, ast, tz, fields)` called directly now counts `{ at: { $lte: '2026-02-01' } }` against that day's midnight. To keep the seam's reading on a direct call, lower each `filter` first with `lowerFilterCondition(filter, { isDatetimeColumn })`.
  - **`@objectstack/driver-mongodb`: `$contains` / `$notContains` ask membership on a declared JSON-stored field.** On a field `syncSchema` recorded as `multiple: true`, a multi-option type (`tags`, `multiselect`, `checkboxes`) or a JSON type, `translateFilter` (every verb, and the aggregation `$match`) now emits an array-only `$elemMatch` over the members the comparand names, with the candidate rule the SQL dialects bind (`jsonMembershipCandidates`, `@objectstack/core`): `'1'` names the string `'1'` or the number `1`, `'true'` the string or `true`. It used to emit a `$regex`, which MongoDB applies to each element, so `{ owners: { $contains: 'u1' } }` matched a stored `['u10']` and `{ tags: { $contains: 'red' } }` a stored `['redwood']`. `$notContains` is the exact complement, and still admits a row with no value. A scalar column, and a field whose declaration the driver does not hold (an object never synced, a standalone `translateFilter` call), keep the substring `$regex`.
  - **`@objectstack/formula`: `matchesFilterCondition` asks membership of a JSON-stored column.** When the caller supplies `options.fields` and it names the column, the declaration decides: membership on a JSON-stored column, substring on any other. Otherwise the stored value decides: an array asks membership, anything else substring. A stored array used to fail `$contains` and pass `$notContains` whatever it held.
    - **The RLS write check, which evaluates a policy with this function, moves with it.** Under a `check` such as `record.tags.contains('x')` on a multi-valued field, a write whose post-image holds `['x']` (a row the same policy's read shows) is now admitted; it was refused `PERMISSION_DENIED` / 403. `['xy']` stays refused, and the read hides it.
    - A scalar written to a declared multi-valued field is judged as written, before the write door wraps it in a list. So `tags: 'xy'`, which the check used to admit while the read hides the stored `['xy']`, is now refused 403. And `tags: 'x'` is now refused 403 too, although the read shows the stored `['x']`. Send the list, `tags: ['x']`.
  - **`@objectstack/spec`: docblock only, in the shipped `src/data/filter.zod.ts`.** The three pointers to the deleted `SqlDriver.calendarDayUpperBoundRewrite` / `calendarDayBetweenRewrite` now name the shared `lowerFilterCondition` at the seams, and the `FILTER_OPERATORS` `$contains` implementation-status list gains `driver-mongodb` and `formula`. No schema, type or export changes.
  - No exported name changes.
- 05be352: fix(formula,plugin-security): the refusal of a field-to-field comparison across comparison classes now leads with its remedy, so the remedy reaches REST callers (#20869)
  
  Clause-②: no
  
  A row-level policy that compares two fields of no shared comparison class (text against a number, or any field against a file field, a formula field, or a field that holds a list or an object) is refused with `INVALID_FILTER` / 400. The REST door keeps a 4xx message under 500 characters by cutting it to its first 499 characters plus an ellipsis. Both messages for this refusal put the remedy last, so the remedy was always cut off, and a caller read the diagnosis but never the fix:
  
  - The record matcher's message (`@objectstack/formula`, raised by the RLS write check on an insert or update through `/data`) was 972 characters, with the remedy starting at character 825.
  - The explain engine's message (`@objectstack/plugin-security`, answered by `GET` / `POST /api/v1/security/explain`) put the remedy after the policy names and the diagnostic. Those have no length limit, so the message was 601 characters with a short policy name and longer with longer names.
  
  Both messages now start with the remedy. It is the same sentence as before and has only moved:
  
  - The record matcher's message is 494 characters and reaches the wire whole. In order it says: the remedy; that the two columns share no class, and which classes exist; why the comparison is refused; and why the columns are not named. It still names no column, operator or policy; the server log names them.
  - The explain engine's message starts with the remedy, then names the policy and both columns, then gives the reason. Whatever the names' length, the remedy sits in the first 125 characters. With long names the REST door may cut the reason at the end.
  
  Unchanged: the error code (`INVALID_FILTER`), the status (400), which comparisons are refused, the refusal a find answers with (driver-sql's read refusal, 383 characters, which already reached the wire whole), and every other refusal.
- Updated dependencies [e5c7d07]
- Updated dependencies [addbbf0]
- Updated dependencies [93d4e0e]
- Updated dependencies [88b484e]
- Updated dependencies [9905e61]
- Updated dependencies [f11b5f2]
- Updated dependencies [0cb72cf]
- Updated dependencies [c1d8051]
- Updated dependencies [a918fe7]
- Updated dependencies [41dcf11]
- Updated dependencies [c46279f]
- Updated dependencies [688ddef]
- Updated dependencies [b1aab1e]
- Updated dependencies [274e162]
- Updated dependencies [0efbdc3]
- Updated dependencies [c8dd8dd]
- Updated dependencies [03cdb9a]
- Updated dependencies [15b586d]
- Updated dependencies [542670d]
- Updated dependencies [e73ee2d]
- Updated dependencies [92fe081]
- Updated dependencies [c4c68ca]
- Updated dependencies [d78a0bd]
- Updated dependencies [5363e2d]
- Updated dependencies [c876a74]
- Updated dependencies [f1e921a]
- Updated dependencies [7a1faf1]
- Updated dependencies [c9d234c]
- Updated dependencies [24d521e]
- Updated dependencies [3a89d45]
- Updated dependencies [f379f57]
- Updated dependencies [05cb2bc]
- Updated dependencies [7510663]
- Updated dependencies [1a75e39]
- Updated dependencies [d7631d5]
- Updated dependencies [d830d71]
- Updated dependencies [1ab9892]
- Updated dependencies [fbec216]
- Updated dependencies [35587f7]
- Updated dependencies [ace770d]
- Updated dependencies [ed54768]
- Updated dependencies [99786f9]
- Updated dependencies [63bfe69]
- Updated dependencies [1940afd]
- Updated dependencies [4f83db5]
- Updated dependencies [f5c7b2c]
- Updated dependencies [6afccda]
- Updated dependencies [671d4c1]
- Updated dependencies [bbcd20c]
- Updated dependencies [c8111a5]
- Updated dependencies [9ad6544]
- Updated dependencies [c9c182e]
- Updated dependencies [4b4ee88]
- Updated dependencies [f10d802]
- Updated dependencies [93e9e42]
- Updated dependencies [ca5408c]
- Updated dependencies [b280546]
- Updated dependencies [975b248]
- Updated dependencies [ebb66aa]
- Updated dependencies [ceee88f]
- Updated dependencies [e18fea6]
- Updated dependencies [f750119]
- Updated dependencies [660a9b2]
- Updated dependencies [f6ccca4]
- Updated dependencies [26437ae]
- Updated dependencies [32d3b3c]
- Updated dependencies [c6b3a01]
- Updated dependencies [bee75ce]
- Updated dependencies [2742e53]
- Updated dependencies [a75311d]
- Updated dependencies [d98bf24]
- Updated dependencies [8368f1c]
- Updated dependencies [31c3996]
- Updated dependencies [95555e7]
- Updated dependencies [a29a0ea]
- Updated dependencies [83480c6]
- Updated dependencies [013f97d]
- Updated dependencies [5d5e679]
- Updated dependencies [e07566b]
- Updated dependencies [11d28c1]
- Updated dependencies [399e3aa]
- Updated dependencies [ba03198]
- Updated dependencies [94608a7]
- Updated dependencies [b3d7a70]
- Updated dependencies [b3917d9]
- Updated dependencies [c27404f]
- Updated dependencies [27c0cf3]
- Updated dependencies [70dae53]
- Updated dependencies [665cab3]
- Updated dependencies [62b90d7]
- Updated dependencies [cb45469]
- Updated dependencies [f3b16fc]
- Updated dependencies [d6d6e87]
- Updated dependencies [df1feae]
- Updated dependencies [336e191]
- Updated dependencies [9bdc6d3]
- Updated dependencies [24c554d]
- Updated dependencies [3dc33b2]
- Updated dependencies [9969228]
- Updated dependencies [95e24b0]
- Updated dependencies [1a4c7f8]
- Updated dependencies [c7396f1]
- Updated dependencies [434c6c7]
- Updated dependencies [4b59a38]
- Updated dependencies [cfa9315]
- Updated dependencies [0803a8b]
- Updated dependencies [0d42104]
- Updated dependencies [a3d7588]
- Updated dependencies [b8191f7]
- Updated dependencies [315888d]
- Updated dependencies [1741c5d]
- Updated dependencies [3711e0b]
- Updated dependencies [a8acee2]
- Updated dependencies [a51920f]
- Updated dependencies [0f6dcac]
- Updated dependencies [682873f]
- Updated dependencies [2123fcc]
  - @objectstack/spec@17.6.0

## 17.5.0

### Minor Changes

- ce57857: feat(spec)!: every engine-evaluated expression slot requires a non-blank `source` — the #15430 rule generalised from the flow-node ledger to the other 36 declaring positions (#15811, decision batch #122 item 2)
  
  <!-- adr-0087: registered evaluated-expression-slots-source-required -->
  
  **BREAKING** accept-set narrowing on 36 published metadata slots. Each of them
  composed `ExpressionInputSchema` and now composes `EvaluatedExpressionInputSchema`,
  so an envelope carrying only `ast` (`{ dialect: 'cel', ast: … }` with no `source`)
  and a `source` that is blank after trimming — through the envelope key or through
  the bare-string shorthand — are refused at the door instead of parsing and then
  faulting at run time. The prescription is registered under protocol major 18 as
  the semantic migration `evaluated-expression-slots-source-required`.
  
  **⚠️ Graded `minor`, not `major`, and the ruling said `major`.** Decision batch
  #122 item 3 ordered a 「`major` changeset」. This repo's launch-window convention
  ships breaking changes as `minor` while the fixed group versions in lockstep, and
  `scripts/check-changeset-no-major.mjs` enforces it: a `major` marker here would
  promote all ~70 packages to a whole-stack major release, which is a release act.
  The convention's own written carriers for breaking-ness are used instead and both
  are present — this **BREAKING** banner and the ADR-0087 disposition above. The
  ruling's substance (a breaking narrowing, carried by an ADR-0087 semantic
  migration entry) is delivered; only the marker differs, and it differs because a
  repo gate forbids the marker.
  
  **What is NOT narrowed.** `ExpressionSchema` / `ExpressionInputSchema` remain the
  persistence contract (`source` OR `ast`), by item 2 of the same ruling, and so
  does `PredicateInputSchema`, which is a plain alias of the latter. A slot that
  only PERSISTS an envelope is untouched; the narrowing is at the slots an engine
  EVALUATES. An `ast` carried BESIDE a string `source` stays admitted everywhere.
  
  **The population was re-derived, not inherited.** By identity — a negative
  lookaround on identifier characters, so `CronExpressionInputSchema` and
  `TemplateExpressionInputSchema` cannot leak in as substrings — over
  `packages/spec/src`, non-test: 34 declaring source lines, two of which are
  file-local alias consts (`ui/action.zod.ts` `ActionConditionInputSchema`,
  `system/settings-manifest.zod.ts` `SettingsVisibilityInputSchema`) that mount two
  slots each, giving **36 declaring positions**. Three of them reach the schema as a
  union member rather than head-of-declaration (`RecordAlertProps.visible`,
  `ServiceLevelIndicator.successCriteria`, `TraceSamplingConfig.composite[].condition`).
  
  On **two of those three the sibling arm is untouched**: `RecordAlertProps.visible`
  still takes a boolean literal, and `ServiceLevelIndicator.successCriteria` still
  takes its structured `{ threshold, operator, percentile? }` object — including one
  that happens to carry a `dialect` key.
  
  ⚠️ **On the third, `TraceSamplingConfig.composite[].condition`, the sibling arm
  narrows too, and deliberately.** Its structured-filter arm is a bare
  `z.record(z.string(), z.unknown())`, which accepted `{ dialect: 'cel', ast }` as an
  ordinary filter — so swapping the expression arm changed nothing at all there. That
  arm now declines any object carrying a `dialect` key, and six shapes the base
  accepted THROUGH THAT ARM ALONE (measured: the base's `ExpressionInputSchema`
  refused every one of them) are refused at this slot:
  
  | authored `condition` | base | now |
  |---|---|---|
  | `{ dialect: 'cel' }` | accepted | refused |
  | `{ dialect: 'js', source: 'x' }` | accepted | refused |
  | `{ dialect: 'nope', source: 'x' }` | accepted | refused |
  | `{ dialect: 'cel', source: 5 }` | accepted | refused |
  | `{ dialect: 'cel', source: 'x', meta: { rationale: 5 } }` | accepted | refused |
  | `{ dialect: 'zzz', foo: 1 }` | accepted | refused |
  
  FROM → TO at that slot: if the value really is a **structured filter**, drop the
  `dialect` key (`{ dialect: 'cel', service: 'api' }` → `{ service: 'api' }`); if it is
  an **expression**, give it a dialect this platform evaluates and a non-blank `source`
  (`{ dialect: 'js', source: 'x' }` → `{ dialect: 'cel', source: 'x' }`). A structured
  filter that carries no `dialect` key — `{}`, `{ service: 'api' }`,
  `{ attributes: { 'http.route': '/v1/orders' } }` — is accepted exactly as before.
  
  **Why an authoring-time refusal and not a run-time one.** Measured at the
  chokepoint, `celEngine.evaluate` never silently succeeds on either shape — it
  returns a `parse` fault — so what happened next was decided entirely by the
  slot's fail policy, and the two halves of that population fail in opposite
  directions: fail-CLOSED slots (`ObjectFieldGroup.visibleWhen`,
  `RowCrudActionOverride.visibleWhen`, `BulkActionDef.visible`, the two
  settings-manifest `visible` slots) hid a group, a row button, or silently excluded
  every selected record from a bulk run and reported them as *skipped*; fail-SOFT
  slots left a gate that had stopped gating. Nothing in between said a word: the
  authoring lint `validateVisibilityPredicates` measured 0 findings on an `ast`-only
  envelope and 0 on a blank `source`, against two control legs that each measured 1.
  
  **`@objectstack/formula` gains `printCelAst(ast)`** — the inverse of
  `parseCelToAst`, and the lossless half of the migration: an `ast`-only CEL
  envelope is printed back to surface syntax mechanically, with no judgment asked of
  the author. It is lossless about MEANING, not bytes (the printer re-renders from
  the parse tree, so `'x'` comes back as `"x"`), and it answers `null` — never a
  guess — for anything it cannot round-trip through the platform's own bounded
  parser. That `null`, and every blank `source`, are what the semantic migration
  entry's structured TODO covers.
  
  **The published TypeScript interface `RowCrudPredicates` narrows with it**
  (`Expression | ExpressionInput` → `EvaluatedExpression | EvaluatedExpressionInput`),
  because it mirrors the two `RowCrudActionOverride` slots and a type that still
  promised an `ast`-only envelope would advertise what the schema now refuses.
  
  **So do the four expression constructors — `expression()`, `cel`, `tmpl`, `cron`
  (and therefore the `F` / `P` aliases) — which now return `EvaluatedExpression`
  instead of `Expression`.** Each one assigns a `string` to `source`
  unconditionally, so the wider return type described none of them; it was slop
  that cost nothing until an evaluated slot began requiring `source`, at which
  point ``visibleWhen: P`…` `` — the spelling the spec's own docblock teaches —
  stopped type-checking, and `@objectstack/platform-objects` failed its DTS build
  on exactly that. `EvaluatedExpression` is assignable to `Expression`, so every
  persistence-contract slot keeps accepting these values unchanged; what the
  narrower return type adds is that an evaluated slot accepts them too. An author
  who genuinely has no `source` was never calling these constructors — an
  `ast`-only envelope is an object literal, and an evaluated slot refuses it on
  purpose.
- 9be2b59: `EvalContext` no longer declares `api?: { exists, count, lookup }` — the kernel query API behind `os.exists` / `os.count` / `os.lookup`, which `buildScope()` never bound (#18318).
  
  **BREAKING** for a TypeScript consumer: an `EvalContext` literal that carries `api` stops compiling. The level stays `minor` because the launch window refuses `major` outright — while it is open, breaking-ness is carried by this banner and by the ADR-0087 disposition at the foot of this changeset, not by the bump.
  
  The member's docblock said it was "implemented opportunistically by call sites that have a query engine", and no call site ever could: `ctx.api` was read **zero** times in this package — control in the same sweep, `ctx.user`, three reads in `stdlib.ts` — so the three functions reached no evaluation scope however completely a caller populated the member. An author who wrote a predicate to the declaration got `runtime: found no matching overload for 'dyn.lookup(string, dyn)'` instead, and because an unevaluable predicate refuses the write it guards, a validation rule authored that way locked **every** write on its object. The harm came from the declaration existing, not from the implementation missing, so it is removed rather than implemented — with the reason written at the deletion site, and with no shim, alias or reserved spelling left behind.
  
  **Your fix — delete the `api: { … }` property.** There is no replacement key and nothing to re-point: every implementation ever passed there was discarded before evaluation, so removing the property changes no result your predicates produce. TypeScript is where you will hear about it: an `EvalContext` literal carrying `api` now fails to compile, which is the whole of the break. Reading a related record's field from inside a predicate remains unexpressible in any spelling — that capability is tracked as its own card, relationship traversal (`record.crm_account.type`), and deliberately not as `os.lookup` queries; no schedule is implied by this removal.
  
  <!-- adr-0087: not-required (runtime-interface-only packages/formula/src/types.ts#EvalContext) `EvalContext` is a published runtime TypeScript interface with no metadata surface — no Zod schema, no `packages/spec` declaration, no stored representation — so `objectstack migrate meta` has nothing to rewrite and the ledger has no channel to carry. The compiler at the consumer's own call site reaches every affected caller instead. Nothing is re-pointed either: `buildScope()` never read `ctx.api`, so the whole remedy is deleting a property that was already inert. -->
  
  Clause-②: yes
- 627382b: Add `current_user.can(object, verb)` — the permission predicate — to the CEL engine, together with the data it is answered from.
  
  `Clause-②: yes` — a new callable name widens the authorable surface. Purely additive: nothing is removed, renamed or narrowed, and every expression that evaluated before evaluates the same way.
  
  **What you can write now**
  
  ```cel
  current_user.can('crm_lead', 'edit')
  ```
  
  `can` is registered **receiver-only**, so it is called ON the acting subject (`current_user`, or its `user` / `ctx.user` / `os.user` aliases — the same object). A bare `can(object, verb)` is deliberately not registered and keeps faulting: a permission question with no subject has no meaning.
  
  The verb vocabulary is the closed table `OBJECT_PERMISSION_VERBS` in `@objectstack/spec/security` — `read`, `create`, `edit`/`update`/`write`, `delete`/`remove`, `export`, `transfer`, `import`. A verb outside it is refused loudly rather than answered `false`. The answer folds the super-user bits exactly as the enforcement door does, so a predicate and the server's 403 cannot disagree.
  
  **What a call site must pass**
  
  `EvalContext` gains `permissions` — a pure data map, object name → `EffectiveObjectPermission`, which is the `objects` map of the published `/auth/me/permissions` response, unchanged. Build it through the new `toEvalPermissions(response.objects)`, which refuses a payload that is not that shape.
  
  ```ts
  import { toEvalPermissions } from '@objectstack/formula';
  
  const permissions = toEvalPermissions(mePermissions.objects);
  ExpressionEngine.evaluate(predicate, { user, record, permissions });
  ```
  
  **With no permission data in the context, `can` THROWS** (`ok: false`, `kind: 'runtime'`) and names the missing input. It never answers `true` (which would reveal what the subject may not see) and never answers a silent `false` (which would hide a gated element from everyone, indistinguishable from a real denial). An *empty* map is a real answer and evaluates to `false`, as does an object the map does not mention.
  
  **Also new, all additive**: `EvalPermissions` and `PermissionBinding` types, `registerPermissionPredicate()`, and an optional fourth argument on `registerStdLib()` carrying the binding. Existing three-argument calls are unaffected.
- e75cc3c: `ExprSchemaHint` gains `roots` — an authoring surface naming the binding roots it mounts beyond the platform baseline, so `validateExpression` can accept them without standing down on everything else (#18554).
  
  A page component's `visibleWhen` binds three roots at runtime, and `ExprSchemaHint` could express neither of the two shapes it needs: `scope: 'record'` refused `page.selectedProjectId != ''` — the worked example `packages/spec/src/ui/page.zod.ts`'s own `visibleWhen` describe ends with, under a sentence naming the contract-bound roots as `record`, `current_user` and page state as `page.<var>` — and prescribed `record.page`, which names nothing on any layer; `scope: 'flattened'` accepted that example and accepted a bare `status == 'done'` with it, which is the shorthand the narrowing exists to catch. Downstream the refusal is not cosmetic: an editor that lints a page block on the `record` face disables Save for the author who wrote the platform's own documented spelling.
  
  ```ts
  validateExpression('predicate', "page.selectedProjectId != ''", {
    scope: 'record',
    roots: ['page'],      // what this surface mounts beyond the baseline
  });                     // -> ok; `status == 'done'` at the same site is still an error
  ```
  
  - **It only ever adds.** A root listed in `roots` is declared alongside `SCOPE_ROOTS`, never instead of it, so passing the key can turn a refusal into an acceptance and never the reverse — a caller adopting it cannot silently lose a check it has today, and a call site that does not pass it gets the verdict and the prescription it got before, byte for byte.
  - **Declaring a root is not becoming permissive.** The bare-field shorthand, an undeclared root, and a typo of a declared root are all still hard errors at a surface that declares `page`. Trading a false refusal for a silent acceptance is the worse of the two directions, so the surface says *which* roots it binds rather than asking the validator to stop checking.
  - **A mistyped root is sent to the root, not to `record.<typo>`.** When a surface has declared its roots, a namespace reference within edit distance of one of them (`pge.selectedProjectId`) is named as an unbound root and pointed at `page`. Every other shape — a bare value reference, a known field used as a JSON namespace, any site with no declared roots — keeps the existing `record.<name>` prescription, which is the right fix for the case it was written for.
  - **`introspectScope` advertises what the validator accepts.** Declared roots join the roots it hands an author, from the same declaration, so a root that is accepted is never one an author has no way to discover.
  - **Not a closed-set mechanism.** A surface that must *refuse* a baseline root it never mounts still says so with `collectCelRootIdentifiers`, which reads the AST and is independent of this key. The two directions stay two mechanisms.
  
  Clause-②: yes (widening)
- 1f05ea4: A validation rule can read one hop through a lookup — `record.account.type` on an opportunity resolves the owning account's field instead of faulting (#18682)
  
  Clause-②: yes (widening)
  
  A validation predicate could only read the record it guards. A `lookup` /
  `master_detail` field carries an **id**, so the natural cross-object rule —
  "a partner account may not carry an opportunity over 10000" — faulted with
  `runtime: No such key: type`, and because a broken validation is fail-closed it
  rejected every write on the object. The capability mainstream platforms provide
  as a matter of course could not be authored at all.
  
  ### What you can write now
  
  ```ts
  validations: [{
    name: 'partner_cap',
    type: 'script',
    message: 'Partner accounts are capped at 10000.',
    condition: "record.account.type == 'partner' && record.amount > 10000",
  }]
  ```
  
  One hop, through any reference-typed field (`lookup`, `master_detail`, `user`,
  `tree`). The engine reads the related row before evaluating and binds it in
  place of the id, so `record.<reference field>.<field>` resolves.
  
  ### It is data pinned BEFORE evaluation, not a query from inside CEL
  
  There is no `os.lookup(...)` / `os.exists` / `os.count` — those stay removed.
  The engine statically analyses the predicate, learns exactly which reference
  fields it reads through and which related fields it names, and loads those
  **before** evaluation. Every registered function stays pure once `now` is
  pinned, so `objectstack build` artifacts stay byte-stable.
  
  The cost is bounded by construction: one hop, only the fields a rule actually
  names, one batched read per reference field per write, and nothing at all when
  no rule traverses.
  
  ### Read authority — system, bounded by the projection
  
  The related row is read under **system authority**. A validation rule's output
  is a pass/fail the *system* enforces, not data handed to the caller — which is
  why RLS predicates are excluded from this capability altogether. Reading as the
  acting user instead made the rule unauthorable for exactly the persona it exists
  to constrain: a member with CRUD on the child and no read on the parent faulted
  on every write.
  
  What bounds the elevation is the **projection**: only the
  columns the predicate names, intersected with the related object's declared
  fields. A column the related object does not declare never enters the query, and
  is refused as the authoring fault it is — distinct from a column that exists and
  is empty, which evaluates as `null`.
  
  A related object no organization wall scopes — no tenant column (`sys_user`
  behind a `user` field), `tenancy.enabled: false`, or `external` — is bounded by
  row as well: for any caller that is not system (a user, a public-form
  submitter, a caller with no principal), only a row the caller's own read of that
  object returns. A reference to any other row refuses the write as not readable,
  whatever that row holds. Under a walled posture (`group` or `isolated`), such a
  caller with no active organization gets no related read at all: a rule reading
  through a stored reference refuses the write as not found.
  
  ⚠️ **The accepted cost, stated plainly.** A caller can *infer* a related value
  they cannot see by observing which writes are refused. The value itself never
  appears — the refusal names the field and the rule, never the value — and the
  channel is deliberately no wider than "this rule refused this write".
  
  ### Two shapes are refused, with a prescription
  
  Both fault at evaluation today, so neither removes anything that works:
  
  | Shape | Why | Write instead |
  | --- | --- | --- |
  | `record.account.type == 'x' && record.account == 'acc_1'` | reading through the relationship resolves `record.account` to the related RECORD, so the id comparison would stop matching — silently | `record.account.id == 'acc_1'` for the value comparison |
  | `record.account.owner.email` | a second hop is not loaded | denormalise onto `account`'s object, or read it in a hook |
  
  A field that is **not** reference-typed is untouched: `record.address.city` on
  an object-valued field traverses today and keeps traversing.
  
  ### `@objectstack/plugin-security` gains `canWriteObject`
  
  The WRITE admission — the sibling of the existing `canReadObject`, running the
  middleware's own arms in the middleware's own order: system bypass; then, before
  anything resolves, the ADR-0103 engine-owned write guard and the ADR-0090 D12
  delegated-administration gate, each called as the middleware's own primitive;
  then no resolved permission sets, unresolvable posture, the ADR-0066 D3
  `requiredPermissions` capability AND-gate for both principals, the CRUD grant,
  the ADR-0090 D10 delegator check, and — when the caller's payload is supplied —
  the field-level security WRITE gate over it (`getFieldPermissions`, folded
  through the D3 field-capability contract, intersected with the delegator's mask
  under D10, then the forbidden-write detection); and last, the ADR-0123 D2
  no-active-organization wall, the same verdict the middleware's step 3.7 throws
  on. It exists for doors that must ask
  "could this caller perform this write" without running the engine middleware —
  the write preview is the first.
  
  ⭐ What it answers, POSITIVELY — by naming what it RUNS, never a category of the
  write decision: the ADR-0103 engine-owned affordance gate, the ADR-0090 D12
  delegated-admin gate, the fail-closed postures (#3545's unresolvable posture and
  the D10 dangling delegator), the ADR-0066 D3 capability AND-gate for both
  principals, the `allowCreate`/`allowEdit` CRUD grant, the D10 delegator's
  independent grant, the step 2.5 FLS write gate over the keys the payload
  names, and the ADR-0123 D2 organization wall. It says nothing about any refusal
  not in that list. `@objectstack/plugin-security`'s
  `can-write-object-admission.test.ts` pins the method's answer equal to the
  registered middleware's on its equivalence block's cases, and pins one D12
  UPDATE case as a direction: the method `false`, the middleware `true`.
  
  ⛔ `true` never means the write will succeed, and ⛔ what follows is not an
  enumeration of the distance to success: the middleware refuses both before and
  after `next()` for reasons this method is never asked. Nearest to hand are the
  remaining pre-resolution gates that run beside the two named above — the
  package-managed and system-row write gates, which judge a row's PROVENANCE; the
  curated-capability-name and audience-anchor binding refusals, which judge a
  payload VALUE; and the ADR-0056 public-form grant, which no caller can present
  to this method and which has no extracted primitive to call; the row-level and
  post-image refusals — the `using` pre-image, the ADR-0055 controlled-by-parent
  master edit, the RLS `check` post-image and the Layer 0 tenant post-image, none
  of which this method can judge because it is asked about no ROW; the
  payload-VALUE refusals the same caller passes by simply not sending the value —
  the masked echo and the `owner_id` forge, which therefore widen the caller class
  by nothing; the anti-filter-oracle guard on the caller's own predicate, which
  this method is handed none of; the post-`next()` assertion that the insert
  `check` seam really ran, which judges an executed write; and, outside the
  middleware entirely, `readonlyWhen`, the static `readonly` strip and the
  validation rules themselves.
  
  ### Scope
  
  Object validation rules (`script` / `cross_field`) — and the system-authority
  read is confined to that one seam. The field-level
  `requiredWhen` / `readonlyWhen` / option `visibleWhen` predicates fail **open**
  and are deliberately not covered here; RLS predicates are out too. Depth is one
  hop. The cleanup UPDATE a `set_null` delete issues on a referencing record
  resolves no relationship, so a rule there is evaluated as before this release —
  against the bare id, where reading through it faults and refuses the cleanup,
  and with it the delete.
- 9347c1f: A row-level or sharing-rule predicate comparing a field against a list with `!=` / `==` is refused at the CEL lowering instead of lowering to a filter that widens on driver-mongodb, and driver-mongodb refuses `$ne` with an array comparand (#19886).
  
  **BREAKING** — an accept-set narrowing, shipped by `@objectstack/formula`, `@objectstack/driver-mongodb`, `@objectstack/plugin-security`, `@objectstack/plugin-sharing` and `@objectstack/lint` as `minor` under the repo's launch-window convention for accept-set narrowings. The hand-migration prescription is registered under protocol major 18 as `cel-predicate-list-comparand-refused`.
  
  Clause-②: no (narrowing)
  
  **Security fix for RLS reads on MongoDB and RLS write checks.** A policy written `record.status != ['closed', 'archived']` (or `!(record.status == [...])`, or `!=` against a `current_user` membership set) lowered to `{ status: { $ne: [...] } }` (or `$not` around a bare-array equality). The RLS `using` clause is composed into the query after the engine's comparand-shape check, and driver-mongodb passed the shape to the server, where it selects every scalar row: the read returned the rows the policy was written to hide. A `check` written `!=` against a membership set admitted every write.
  
  - `@objectstack/formula`: `compileCelToFilter` refuses `==` / `!=` whose comparand is a list (`unsupported`): a list literal, or a `current_user` variable that resolves to an array. The authoring shape check (`isPushdownableCel`, `isSupportedRlsExpression`) reports the literal; a resolved array is refused per request.
  - `@objectstack/plugin-security`: the RLS compiler drops such a policy and fails closed when no other policy applies (`RLS_DENY_FILTER`: reads return no rows, `check` writes are refused 403). A CEL-authored `check` gets this 403; the `INVALID_FILTER` / 400 of `matchesFilterCondition` remains for a filter passed to it directly.
  - `@objectstack/plugin-sharing`: a declared sharing rule with such a `condition` is skipped at bootstrap and never seeded.
  - `@objectstack/lint`: the list-literal form is reported (`rls-predicate-unenforceable`, `sharing-rule-unlowerable-condition`). The RLS reference pass probes each kernel-resolved `current_user` key with its runtime type.
  - `@objectstack/driver-mongodb`: `translateFilter` refuses `$ne` with an array comparand at any depth, with `INVALID_FILTER` / 400, as driver-sql and driver-memory already do.
  - `@objectstack/spec`: the migration registry carries the entry.
  
  **What to change.** "One of these values" is `record.status in ['open', 'pending']`; "none of these values" is `!(record.status in ['closed', 'archived'])`. In a raw filter, use `$in` / `$nin`. `in`, scalar `==` / `!=`, `null` and field-to-field comparisons are unchanged.
  
  <!-- adr-0087: registered cel-predicate-list-comparand-refused -->
- c164186: `matchesFilterCondition` refuses an array comparand under `$ne` and in the equality position (`{ field: [...] }`, `$eq: [...]`) with `INVALID_FILTER` / 400, before any record is judged (#19886).
  
  **BREAKING** — an accept-set narrowing, shipped by `@objectstack/formula` and `@objectstack/plugin-security` as `minor` under the repo's launch-window convention for accept-set narrowings. The hand-migration prescription is registered under protocol major 18 as `rls-predicate-array-comparand-refused`.
  
  Clause-②: no (narrowing)
  
  **Security fix for row-level write checks.** This evaluator is what `@objectstack/plugin-security` runs against the post-image of an insert or update to enforce a row-level `check`. It compared strictly, and no stored value ever equals an array, so:
  
  - a `check` written `record.status != ['closed', 'archived']`, or `!=` against a `current_user` membership array, lowered to `{ status: { $ne: [...] } }` and matched **every** post-image;
  - a `check` written `!(record.status == ['closed', 'archived'])` lowered to `{ $not: { status: [...] } }` and did the same.
  
  Every write such a policy was written to refuse was admitted and stored. The positive `record.status == ['open', 'pending']` already refused every write (403).
  
  The message withholds the field, the operator and the value, because the filter is usually an access policy the caller did not write, and the comparand may be a resolved membership set.
  
  **What to change.** A `check` or `using` predicate that means "one of these values" or "none of these values" is spelled with `in`: `record.status in ['open', 'pending']`, or `!(record.status in ['closed', 'archived'])`. Those, scalar `!=` / `==`, `null`, `Date` comparands and `{ $field }` references evaluate exactly as before.
  
  <!-- adr-0087: registered rls-predicate-array-comparand-refused -->
- 4d7e740: A row-level or sharing-rule predicate whose comparison is handed something other than one value is refused at the CEL lowering or at the write-check evaluator, instead of admitting writes and reads it was written to refuse (#19886).
  
  **BREAKING** — an accept-set narrowing, shipped by `@objectstack/formula`, `@objectstack/plugin-security`, `@objectstack/plugin-sharing`, `@objectstack/lint` and `@objectstack/objectql` as `minor` under the repo's launch-window convention for accept-set narrowings. The hand-migration prescription is registered under protocol major 18 as `cel-predicate-one-value-comparand-refused`.
  
  Clause-②: no (narrowing)
  
  **Security fix for RLS write checks and reads.** Each shape below was measured through the real plugin-security on driver-sql and driver-memory:
  
  - `!(record.status in [['closed', 'archived']])` (a list nested in an `in` list) admitted and stored every write the `check` was written to refuse, and a `using` read returned every row on driver-memory.
  - `current_user.org_user_ids != 'x'` and `current_user.org_user_ids > 'a'` (a membership set on a comparison with no field) folded to "no restriction": every write admitted, every row read, on every driver.
  - `record.status > ['m']` compared the list as the string `'m'` on the write check, while the analytics read scope bound the whole list as one SQL parameter. `record.reviewer_id > current_user` compared the whole caller object as a string and admitted and stored every write; in this release the RLS compiler's comparand faces (#20212) already drop that policy, and this change refuses it at the lowering for every caller of the compiler.
  - `record.status != record.tags`, its negation `!(record.status == record.tags)`, and the mirror `record.tags != record.status`, with `tags` a `json` field or a `multiple` lookup, admitted and stored every write.
  
  What changes:
  
  - `@objectstack/formula`: `compileCelToFilter` refuses, with `unsupported`, a list comparand under every comparison (the ordering operators now included, and on the constant-fold branch, whichever side), the `current_user` root or a key resolving to an object under an ordering operator, and an `in` list whose member is itself a list. The authoring shape check (`isPushdownableCel`, `isSupportedRlsExpression`) reports each literal form; a resolved value is refused per request. `matchesFilterCondition` refuses, with `INVALID_FILTER` / 400, an array under `$gt` / `$gte` / `$lt` / `$lte`, an array member of `$in` / `$nin`, and a `{ $field }` comparison (`$eq`, `$ne` or an ordering operator) whose column holds a list or an object on the record being judged, on either side. The message withholds the field, the operator and the value.
  - `@objectstack/plugin-security`: the RLS compiler drops a policy the compiler refuses and fails closed when no other policy applies (`RLS_DENY_FILTER`: reads return no rows, `check` writes are refused 403, and `getReadFilter` hands the analytics read scope the deny scope). A `check` comparing a field with a list-holding column is refused 400 and stores nothing.
  - `@objectstack/plugin-sharing`: a declared sharing rule with such a `condition` is skipped at bootstrap and never seeded.
  - `@objectstack/lint`: the literal forms are reported as `rls-predicate-unenforceable`, and an ordering comparison against a membership set through the reference pass.
  - `@objectstack/objectql`: a `having` comparison against a `{ $field }` column whose aggregated row holds a list is refused 400 where the row carries the list itself (driver-memory); driver-sql rows carry the stored JSON text and compare as before.
  - `@objectstack/spec`: the migration registry carries the entry.
  
  The stage 2a changeset's sentence that `{ $field }` references evaluate as before no longer holds for a column holding a list or an object: that comparison is now refused.
  
  **What to change.** "One of these values" is `record.status in ['open', 'pending']`, and "none of these values" is `!(record.status in ['closed', 'archived'])`, with the list flat. An ordering takes one bound (`record.status > 'm'`); a range is two comparisons joined by `&&`. Compare against one key of the caller (`record.reviewer_id > current_user.id`). A field compared with a `json` or `multiple` field has no pushdown form: compare with a single-valued column, or move the condition into a validation rule or hook. In a raw filter, use `$in` / `$nin` with flat lists and one bound per ordering operator.
  
  Not changed: a field compared with a `json` or `multiple` field still lowers and is not reported at authoring time, because the lowering sees the predicate's text and not the object's field types; driver-memory still answers a `{ $field }` comparison on a read without evaluating the reference.
  
  <!-- adr-0087: registered cel-predicate-one-value-comparand-refused -->
- de091b5: A row-level write check that orders a field against a bound (`>`, `>=`, `<`, `<=`) is refused with `INVALID_FILTER` / 400 when that field holds a list or an object on the record being written, instead of comparing the list's string form and admitting the write (#19886).
  
  **BREAKING** — an accept-set narrowing, shipped by `@objectstack/formula` and `@objectstack/plugin-security` as `minor` under the repo's launch-window convention for accept-set narrowings. The hand-migration prescription is registered under protocol major 18 as `rls-predicate-stored-list-ordering-refused`.
  
  Clause-②: no (narrowing)
  
  **Security fix for RLS write checks.** Measured through the real plugin-security on driver-sql and driver-memory: `record.tags > 'a'`, with `tags` a `json` field holding `['m']`, compared `'m' > 'a'` and admitted and stored the write. `record.meta < 'a'` with `meta` holding `{ a: 1 }` compared `'[object Object]' < 'a'` and did the same, and so did a `multiple` lookup. `using` stands in as the check for a policy that declares no `check`, so the same predicate in `using` was enforced the same way on writes. No shipped row-level or sharing-rule predicate orders a field at all.
  
  What changes:
  
  - `@objectstack/formula`: `matchesFilterCondition` refuses `$gt` / `$gte` / `$lt` / `$lte` and `$between` on a field whose value on the record is a list or a plain object, whatever the comparand, with the same `INVALID_FILTER` / 400 and the same message as the stage 2d refusals. The refusal is per record: a record whose `json` field holds one scalar is compared as before. `null` and `Date` values are unchanged, and so is every equality against a stored list (`$eq`, `$ne`, implicit equality, `$in`, `$nin`). `$between` is not produced by the CEL lowering, so it reaches this only through a filter passed to `matchesFilterCondition` directly.
  - `@objectstack/plugin-security`: a check insert or by-id update whose post-image holds a list or an object in an ordered field is refused 400 and stores nothing. That includes a by-id update that edits another field of a row whose stored `json` column holds a list, because the post-image merges the stored row.
  - `@objectstack/spec`: the migration registry carries the entry. The stage 2a entry `rls-predicate-array-comparand-refused` now ends "Scalar != and ==, null, Date comparands, and { $field } references between single-valued columns evaluate exactly as before", which is true since stage 2d.
  
  Three moves, named:
  
  1. **The write check now matches driver-sql's read.** driver-sql refuses every ordering comparison, and `$between`, on a column it stores as JSON text, by declared type (400, #7398). The in-process check now refuses the same predicate on the same row (400).
  2. **driver-memory's read parts from the write check.** driver-memory, a test driver, compares a stored list element by element on a read and keeps returning those rows (`record.tags > 'a'` reads a row holding `['m']`), while the check now refuses writing it. This is declared on #15104, as for stage 2d's `{ $field }` half.
  3. **A list written into a scalar field under an ordering check now answers 400.** `status: ['m']` into a `text` field under `record.status > 'a'`, or `amount: [500]` into a `number` field under `record.amount > 10`, was admitted, and driver-sql stored it as the text `'["m"]'` / `'[500]'`. It is now refused before anything is stored.
  
  **The explain answer.** `security/explain` evaluates the business RLS predicate in-process on the fetched record, so it now answers `INVALID_FILTER` / 400 where the record holds a list or an object under an ordering predicate (this stage). It already answered 400 for a `{ $field }` comparison against a list-holding column (stage 2d). For both, per operation:
  
  | explain operation | driver | the enforced operation answers | same as explain's 400? |
  |---|---|---|---|
  | `read` | driver-sql | 400 `INVALID_FILTER` (the driver's refusal) | yes |
  | `update` | driver-memory | 400 `INVALID_FILTER` (the post-image check) | yes |
  | `update` | driver-sql | 403 `PERMISSION_DENIED`: the pre-image gate fails closed on the driver's 400 | no — both deny |
  | `read` | driver-memory | the rows its element-wise read admits | no — the test driver's read |
  
  Explain itself is unchanged.
  
  **What to change.** Order a single-valued column (`record.priority > 2`), or test membership in the list with `in` (`record.status in ['open', 'pending']`). A `json` or `multiple` field has no ordering.
  
  <!-- adr-0087: registered rls-predicate-stored-list-ordering-refused -->
- 560b724: A row-level or sharing-rule predicate comparing with `!=` / `==` against the bare `current_user` root is refused at the CEL lowering instead of lowering against the whole caller context object (#19959).
  
  **BREAKING** — an accept-set narrowing, shipped by `@objectstack/formula`, `@objectstack/plugin-security`, `@objectstack/plugin-sharing` and `@objectstack/lint` as `minor` under the repo's launch-window convention for accept-set narrowings. The hand-migration prescription is registered under protocol major 18 as `cel-predicate-variable-root-comparand-refused`.
  
  Clause-②: no (narrowing)
  
  **Security fix for RLS write checks.** A policy written `record.owner_id != current_user` (or `== current_user`, or `!(record.owner_id == current_user)`) named the variable root alone, which resolved to the whole caller context, and lowered to `{ owner_id: { $ne: <that object> } }` (or the bare object, or `$not` around it). A strict compare never equals an object, so a `check` so written admitted and stored every insert and by-id update it was written to refuse, a USING-only such policy admitted every insert, and explain reported the read as narrowed with the caller's membership sets echoed in its `readFilter`. A constant comparison such as `current_user != 'guest'` folded to no restriction.
  
  - `@objectstack/formula`: `compileCelToFilter` refuses `==` / `!=` whose operand is the bare variable root (`unsupported`), in both of its modes, so the authoring shape check (`isPushdownableCel`, `isSupportedRlsExpression`) reports it before any request. A variable that resolves to an object is refused per request; a `Date` still passes.
  - `@objectstack/plugin-security`: the RLS compiler drops such a policy and fails closed when no other policy applies (`RLS_DENY_FILTER`: reads return no rows, `check` writes are refused 403, explain answers `denies`).
  - `@objectstack/plugin-sharing`: a declared sharing rule with such a `condition` is still skipped at bootstrap, now with reason `unsupported` instead of `unresolved-variable`.
  - `@objectstack/lint`: the shape is reported as `rls-predicate-unenforceable` on either RLS clause, where it was silent, and as `sharing-rule-unlowerable-condition` on a sharing condition, where it was `sharing-rule-runtime-variable-condition`.
  - `@objectstack/spec`: the migration registry carries the entry.
  
  **What to change.** Compare against the key the predicate means: `record.owner_id != current_user` becomes `record.owner_id != current_user.id` (or `current_user.organization_id`, `current_user.email`); a membership test is `record.owner_id in current_user.org_user_ids`. Scalar keys, `in`, `null`, literals and field-to-field comparisons are unchanged.
  
  <!-- adr-0087: registered cel-predicate-variable-root-comparand-refused -->
- 93cfc3f: `$like` / `$ilike`: `_` matches exactly one Unicode code point on every face, so an emoji or any other character outside the Basic Multilingual Plane is one `_`, as SQL `LIKE` and SQLite `GLOB` count it (#20143).
  
  The SQLite faces (`driver-sql` on better-sqlite3, `driver-sqlite-wasm`, `driver-turso` local and remote) already answered by code points. The JavaScript faces did not: they compiled the spec's `likePatternToRegexSource` with no regular-expression flags, so `_` read one UTF-16 code unit, which is half of an emoji. The same REST filter returned a different row set depending on which driver backed the object. Measured at `e7f69dbb` over values holding `😀` (U+1F600) and `𝒜` (U+1D49C), 48 answer cells on the JS faces differed from the SQLite faces; after this change, none do.
  
  - **`@objectstack/spec`**: a new export, `likePatternToRegExp(pattern, foldAscii?)`, compiles the translation with the `u` flag, the one compilation in which `_` is one code point. `matchesLikePattern` evaluates it. `likePatternToRegexSource` is unchanged and still exported; its source means one code point per `_` only under `u`. The `$like` description now says that a character is one Unicode code point.
  - **`@objectstack/formula`**: `matchesFilterCondition` answers `$like` / `$ilike` by code points, through the spec's `matchesLikePattern`. Its own CEL `size()` already counted code points.
  - **`@objectstack/driver-memory`**: all three `$like` doors (the `$like` filter and the AST `like` / `ilike` node through mingo, and the reference matcher) answer by code points.
  
  The answer set moves in both directions on those three faces, only for values holding a character outside the BMP:
  
  | pattern | a stored `😀` | `a😀b` | `a😀😀b` |
  |---|---|---|---|
  | `_` | now matches | — | — |
  | `__` | no longer matches | — | — |
  | `a_b` | — | now matches | — |
  | `a__b` | — | no longer matches | now matches |
  
  `$ilike` moves the same way. No pattern is newly refused and no refusal is lifted. Values made only of characters inside the BMP answer exactly as before.
  
  Clause-②: yes (narrowing)
  
  <!-- adr-0087: not-required (no-migration-prescription) Nothing an author writes is removed or renamed: no spec key, no export and no config field moves, and no stored metadata representation changes, so `objectstack migrate meta` has nothing to rewrite. What moves is the row set a `$like` / `$ilike` query returns on the JavaScript faces, and only for stored values holding a character outside the BMP, where it now equals what every SQLite face already returned. -->
- 862b6ce: Expression refusals now carry a stable `code` and typed `params` beside their English `message`, so a localized author surface can render its own words: `validateExpression` (every entry in `errors[]` and `warnings[]`), `collectCelRootIdentifiers` (its `ok: false` arm), `predicateSlotRefusal` and `structuralConditionRefusal` (#20291)
  
  Clause-②: yes
  
  Until now the only thing a refusal said was an English sentence, and a designer running in another locale could only show it verbatim, beside its own translated headings. Each refusal now also names its code — one of a closed, kebab-case set — and the values its sentence interpolates, so a consumer keys a catalogue row to the code and fills it from the params. The `message` is the same sentence, byte for byte; nothing is removed or renamed, so no existing reader changes.
  
  - `@objectstack/formula` exports `EXPRESSION_REFUSAL_CODES` (the closed set as a frozen list) and the types `ExpressionRefusalCode`, `ExpressionRefusalParams` (code → params), `ExpressionRefusal`, `ExprValidationCode`, `CelRootsRefusalCode`, `CelFieldRole`, `ExpressionSourceKind` and `CelRootIdentifiersResult`. `ExprValidationError` gains `code` and `params`.
  - `@objectstack/spec/automation` exports `FLOW_SLOT_REFUSAL_CODES` (the closed set of both flow-slot refusal producers, as a frozen list) and the types `FlowSlotRefusalCode`, `FlowSlotRefusalParams` (code → params), `PredicateSlotRefusalCode`, `PredicateSlotRefusal`, `PredicateSlotValueKind`, `StructuralConditionRefusalCode`, `StructuralConditionRefusal` and `StructuralConditionValueKind`. `predicateSlotRefusal` returns `PredicateSlotRefusal` and `structuralConditionRefusal` returns `StructuralConditionRefusal`; each is its previous `{ message, source }` plus `code` and `params`.
  - Narrowing on `code` narrows `params`. A code never changes once published: a reworded message keeps its code, and a new refusal gets a new one.
  - A `detail` param is the CEL or template engine's own diagnostic, in English, passed through as the message carries it.
  - These are authoring diagnostics returned as values, not ADR-0112 request error codes, which is why they are kebab-case.
  - The `validate_expression` MCP tool forwards `validateExpression`'s `errors` and `warnings` as they are, so each entry in its answer now also carries `code` and `params`.
- aeb0557: fix(security)!: the RLS write check refuses a field-to-field comparison the read refuses — one comparison class, one answer per policy (#20355)
  
  Clause-②: yes (narrowing)
  
  <!-- adr-0087: registered rls-predicate-cross-class-field-comparison-refused -->
  
  **BREAKING** — an accept-set narrowing on the row-level write check, shipped as `minor`
  under the launch-window convention (`check-changeset-no-major` refuses `major` until GA;
  breaking-ness is carried by this banner and the ADR-0087 disposition above, not by the
  level). The hand-migration prescription is registered under protocol major 18 as
  `rls-predicate-cross-class-field-comparison-refused`, one ADR-0087 D3 entry for the whole
  family: the authoring arm `os validate` gained in #20347 and this write-check arm.
  
  **What changed.** A row-level policy that compares two fields of no shared comparison
  class — `record.status != record.amount` (text and a number), `record.status !=
  record.photo` (text and a file field), `record.status != record.is_open` (text and a
  formula field), `record.status != record.meta` (text and a json field) — already had
  every read it scopes refused with `INVALID_FILTER` / 400 on the SQL drivers, because
  driver-sql compiles a column-to-column comparison only within one class. The write
  check did not know the rule: it compared the two raw values in-process, so an insert
  or update the policy's `check` judges (or its `using`, standing in as the check) was
  admitted and stored whenever that comparison happened to hold. Measured through
  plugin-security and ObjectQL on SQLite, sqlite-wasm and PostgreSQL. The write check
  now refuses the comparison too, with the read's envelope, `INVALID_FILTER` / 400, for
  every insert (single or array), by-id update and predicate update it judges, and
  nothing is stored. The same-class comparisons it always compared are compared as
  before. The 400 names no column of the policy; the server log names the policy and
  both columns. A comparison against a json or `multiple` field is refused by its
  declared type now, where it used to be judged by the value each record held.
  
  **`@objectstack/formula`.** `matchesFilterCondition(record, filter, options?)` takes an
  optional third argument: `options.fields`, the object's declared columns (`type` and
  `multiple` per field name). Given it, every `{ $field }` comparison between two
  declared columns is judged by `crossFieldComparisonVerdict` from
  `@objectstack/spec/data` before any record is read, and one the platform defines no
  answer for throws `INVALID_FILTER` / 400. Without it the evaluator behaves exactly as
  before. Two new exports go with it: `findCrossFieldClassRefusal(filter, fields)`, the
  pure judgement, and `crossFieldClassRefusalCarriedBy(error)`, which reads the refused
  comparison off the error for a server-side log.
  
  **`@objectstack/driver-sql`.** `crossFieldComparisonClass` reads the same export
  (`crossFieldColumnVerdict`) instead of keeping its own copy of the classification, and
  layers above it only its internal type aliases. Every read answers as before.
  
  **`@objectstack/lint`.** The `rls-predicate-unenforceable` finding for such a
  comparison now states the write answer the runtime gives: the in-process write check
  refuses it by the same classification and stores nothing.
  
  **If a policy of yours is refused.** The platform defines no comparison between those
  two columns on any path, so the policy never protected a read either. Compare a field
  only with a field of the same class — a number with a number, text with text, a
  boolean with a boolean, a date with a date, a datetime with a datetime, a time of day
  with a time of day — or, if the two columns do hold comparable values, correct the
  declaration of the one declared with the wrong type. `os validate` names every such
  comparison.
- fb38607: feat(drivers,formula,objectql): the engine's filter faces answer the staged `$empty` operator (#20444)
  
  Clause-②: yes (widening)
  
  `$empty: true | false` is declared by `@objectstack/spec` (`FieldOperatorsSchema`) with a per-type meaning: a text-like field is empty when it is null or `''`, a multi-value field (multiselect, checkboxes, tags, or a select / radio / lookup / user / file / image with `multiple: true`) when it is null or `[]`, and every other type only when it is null. `$empty: false` is the exact complement. Until now every face in this list refused it (`INVALID_FILTER` / 400), except `matchesFilterCondition`, which answered `false` for every record. **A driver or evaluator called directly now answers it:**
  
  - **By the field's declared type**, through the spec's one expansion (`expandEmptyOperator`): `driver-sql`'s filter compiler (and so `driver-sqlite-wasm` and `driver-turso`'s local transport, which inherit it), `driver-turso`'s remote transport, `driver-memory`'s query path (`find` / `count` / `update` / `delete`) and `driver-mongodb`'s `translateFilter` (its `find`, its aggregate `$match`). The declaration is the one each driver already receives — `initObjects` / `registerObjectMetadata` / `registerExternalObject` on the SQL family, `syncSchema` on the others. On SQL a multi-value field's empty list is tested as stored JSON per dialect (SQLite `json_array_length` behind a `json_valid` guard, PostgreSQL a `jsonb` comparison, MySQL `JSON_LENGTH`), never as an equality comparand.
  - **By value** — null, a missing value, `''` and `[]` are empty (`isEmptyFilterValue`) — on the faces that read no field declaration: `@objectstack/formula`'s `matchesFilterCondition` (the RLS write-side `check`), `driver-memory`'s reference matcher, and `@objectstack/objectql`'s `having` and per-aggregation `filter`. In `having`, a `count` or `sum` holding `0` is not empty.
  
  **Refused, never guessed** (`INVALID_FILTER` / 400): `$empty` on a field whose declaration the driver does not hold (a table built outside its registration, a builtin column such as `id`, a field with no `type`, or `translateFilter` / `RemoteTransport` used standalone without a declaration), a multi-value field on a SQL dialect the driver does not model, and a flag that is not a boolean. `driver-memory`'s analytics (cube) face refuses `$empty` as an operator it cannot compile, as it does `$null`.
  
  New optional API: `translateFilter(where, temporalKind?, valueShape?)` in `@objectstack/driver-mongodb` takes a declared-value-shape resolver (type `ValueShapeResolver`), and `buildAggregationPipeline` a `valueShape` option; `RemoteTransport.setDeclaredValueShapeResolver` in `@objectstack/driver-turso`, which `TursoDriver` wires. `@objectstack/spec`'s shared `FILTER_LOGIC_CASES` table gains seven `$empty` cases: a backend that runs it answers `$empty` or goes red, and its harness must declare the fixture's columns.
  
  `$empty` stays staged: it is not in `FILTER_OPERATORS`, so the engine's front door still refuses it until the flip card adds it, and the view operators `is_empty` / `is_not_empty` still lower to `$null`.
- 5505646: fix(formula): the strict declaredness env declares `SCOPE_ROOTS` as `dyn`, so a bare reference behind a root name is no longer masked (#16412)
  
  <!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is renamed, retired or re-typed: no `packages/spec` key changes its name, its type or its optionality, no stored shape moves, and every view, form, flow and formula parses byte-identically to before. `objectstack migrate meta` therefore has nothing to rewrite, and this changeset carries no rewrite instructions. What narrows is the ACCEPT SET of published CHECKERS at build time: `validateExpression` and two `@objectstack/lint` rules now report a bare field reference they previously left unjudged, which is the same verdict each of them already returns for that identifier when it is written first in the same predicate. The sources that newly report are already broken at RUN time and were before this change: a bare identifier in a record-scoped site resolves to nothing, the expression evaluates to null and a visibility predicate falls open, which is #1928's class. The remedy is per-source and the diagnostic already names it in full, naming the identifier and the namespace it belongs under; there is no authored artifact and no stored representation for a migration to act on. -->
  
  **BREAKING** in the accept-set sense — an accept-set narrowing on published
  CHECKERS, in the same sense as a route that starts refusing a request it should
  always have refused — landing in the launch window as `minor` on both packages (during the window the bump level is
  not the carrier of breaking-ness; this paragraph and the disposition above
  are). Nothing that was already reported stops being reported, and no source
  that is correct starts being reported.
  
  `firstUndeclaredReference` asks cel-js's checker for the first undeclared
  identifier in a source. That checker returns exactly ONE error, and the helper
  acts only on `Unknown variable: X`, so whenever the first error is of another
  class every undeclared reference behind it in the same source went unjudged and
  the helper answered `null` — which is also the value that means "every
  reference is rooted". Four published call sites read that answer, and none of
  them can tell the two readings apart.
  
  The widest way to reach that state was a disagreement between two environments
  in this package about the same names. The strict env declared every
  `SCOPE_ROOTS` member (`data`, `config`, `record`, `result`, `item`, `event`,
  `input`, `user`, …) as `map`, while the permissive env that `celEngine.compile`
  type-checks in leaves them `dyn`. `map` has no `==`, `<` or `+` overload, so an
  ordinary comparison on one of those names compiled clean and then faulted `no
  such overload` in the strict env only — taking the single error slot and
  silencing everything behind it. An author reaches it by naming an object field
  or a flow variable after a namespace root and reading it bare, which on a
  metadata-editing form is not even a coincidence: that layer binds the row under
  edit as `data`.
  
  The strict env now declares those roots `dyn`, which is what the list's own
  doc-comment already claimed it was for — member access, arithmetic and
  comparison on a root all deferring to runtime — and which `map` delivered only
  the first of. The two environments agree about these names, so the class cannot
  arise rather than being compensated for downstream.
  
  What starts reporting, measured on each published surface:
  
  - `@objectstack/formula` `validateExpression` with `scope: 'record'` — a bare
    reference behind a root name is the hard error it always was for the same
    identifier written first (`ok` was `true` with zero errors; it is now `false`).
  - `@objectstack/formula` `validateExpression` with `scope: 'flattened'` — the
    did-you-mean warning reaches a misspelled field behind a root name.
  - `@objectstack/lint` `visibility-bare-identifier` — a bare identifier behind a
    root name in a `visibleWhen` predicate is a finding. Per that rule's own
    message the console otherwise falls open and the element renders
    unconditionally.
  - `@objectstack/lint` flow-variable shadowing — a shadowed field read behind a
    root name is warned. That rule's documented blind spot is now name-local, as
    its wording always claimed: the colliding name itself is still not reported.
  
  ⚠️ One published answer also WIDENS, and it is not a reporting surface.
  `inferExpressionType` (`@objectstack/formula`, re-exported from the package
  root; read by `@objectstack/mcp` as `validate_expression.inferredType`) infers a
  formula's coarse value type through `inferCelType`, which shares this same
  strict environment. While the roots were `map` there was no `==`, `<` or `+`
  overload for them, so an expression using a namespace root as a DIRECT OPERAND
  did not type-check at all and the answer was `'unknown'`. With the roots `dyn`
  those expressions type-check and the answer is the truthful CEL type:
  `result + 1` and `record ? 1 : 2` → `'number'`, `record == "x"` → `'boolean'`,
  `data == "x" ? "a" : "b"` → `'text'`, uniformly for every name on the list. No
  answer changes from one concrete type to another and nothing narrows to
  `'unknown'` — `size(record)` and `"a" in record` still answer, and a root that
  is only the base of a member access (`record.amount > 100`) never consulted this
  declaration. A consumer that keys off a concrete type therefore sees strictly
  more expressions classified, never a different classification; for the
  motivating consumer that means a formula written as `data == "x" ? "a" : "b"` is
  now correctly seen as text rather than as unprovable. Pinned on both sides in
  `validate.test.ts`.
  
  ⛔ Two first-error classes are NOT closed by this, and both stay pinned. A CEL
  TYPE name (`type`, `string`, `int`, …) is declared by CEL itself, so no
  declaration this package makes can reach it; measured on the strict env, the
  message for `type == 'grid'` is byte-identical under a `map` and a `dyn` root
  declaration. And `has()` handed a non-select argument still faults its own
  class, which `@objectstack/lint`'s visibility rule masks at its own call site
  (#16118) and which nothing else masks.
  
  The narrowing this helper is built on is unchanged: it still acts only on
  `Unknown variable`, so `type(record.x) == string`, comprehension macros, guard
  idioms, optional chaining and stdlib calls report nothing, and a widening of
  that regex onto the overload message remains refused.

### Patch Changes

- de62769: `SCOPE_ROOTS`'s docblock says it is a **baseline**, not a per-surface accept set, and points at where the per-surface verdict actually lives
  
  The exported `SCOPE_ROOTS` constant carried a docblock that made **a false statement about itself**. Its opening line read *"Namespace roots that a `record`-scoped CEL site may legitimately reference"* — which, read alone, is exactly the per-surface accept-set reading. Ninety lines below, the companion block asserted *"This list is a 'never faults' BASELINE, not a per-surface contract — **the doc-comment above says so**"*. The doc-comment above did not say so; it said close to the opposite.
  
  **This is not a docs nit, and the evidence is a card.** The accept-set reading is what a downstream seat took away, and it generated a cross-repo card filed against this package (this one) about a lint/runtime disagreement that is not a disagreement at all: the baseline declares a root, the per-surface gate refuses it, and both are correct.
  
  - **The opening line now states the contract it actually is**: the roots the strict check env declares, so that naming one is never itself a fault — and explicitly ⛔ *not* a claim that any surface **binds** the root.
  - **It points at the per-surface authority by name**: `@objectstack/lint`'s `fieldRuleRootIssue`, judged against that surface's own closed `FIELD_RULE_BOUND_ROOTS` (`record` / `previous` / `parent`). A reader asking "may THIS surface reference this root?" is now sent one hop to the symbol that answers it, instead of reading the answer off this list.
  - **It names `data` as the standing example** of a root this list declares and the field-rule surface does not bind — the two answers doing their separate jobs, ⛔ not something to repair by editing this list.
  - **The self-reference is now true.** The companion block cites `SCOPE_ROOTS`'s own doc-comment, which now opens by saying exactly what the citation claims it says.
  
  ⛔ **Zero behaviour change.** `SCOPE_ROOTS` keeps all **27** members, byte for byte — no member is added, removed or reordered, and ⛔ `app` is not added (objectstack#16420 closed `not_planned` on that and this does not reopen it). Narrowing was refuted by measurement rather than by preference: six `*.form.ts` metadata-form modules in this repo carry live `data.` predicates. The diff is comment lines only.
  
  **This publishes, which is why it is `patch` rather than `skip-changeset`.** `@objectstack/formula`'s `files[]` ships `dist`, and this TSDoc is emitted into the built declarations — measured on the built artifact at three readings: the new text's distinctive phrase present at 1 in both `dist/index.d.ts` and `dist/index.d.mts`, an untouched neighbouring sentence from the same docblock present at 1 as the lit control, and a fabricated phrase at 0 as the dark control. The companion block is a plain `/* */` comment attached to no declaration and reads 0 in `dist` — it is the half that does not ship, and the half that does is the half that was wrong.
- 7465eeb: fix(formula,objectql): the two refusals a traversing validation rule on an optional lookup meets now name the repairs that work — a `conditional` wrapper or `required: true` (#20007)
  
  Clause-②: no
  
  An author who wants to refuse a write when an OPTIONAL lookup is set and its related record is secret writes `record.line != null && record.line.kind == 'secret'`. Two refusals then sent them in a circle:
  
  1. That expression reads `line` both through the relationship and as a plain value, which cannot be served, and is refused. The refusal said to "compare the id explicitly" and write `record.line.id` for the value comparison.
  2. `record.line.id != null && record.line.kind == 'secret'` reads through `line` too, so an order with no line is refused before the rule is evaluated, as "no single related record". That refusal said to "guard the rule on the reference being set" and named no spelling for the guard.
  
  Which writes are refused is unchanged, and so are the error, the `rule_violation` field error and its `constraint` (`reason: 'unevaluable'` and the fault). `@objectstack/lint` passes the formula refusal through unchanged, so it shows the new text too. Only the prescriptions change. Both now name the two spellings measured to work for an optional reference, and the guard is worded exactly as in the delete-cleanup refusal:
  
  ```text
  … To compare the id, write `record.line.id` for the value comparison, and keep
  `record.line.<related field>` for the traversal. `record.line.id` is not a null guard: it
  reads through `line` too, and a rule that reads through an empty `line` rejects the write
  instead of being skipped. If the plain value tests for empty, take that test out of this
  expression. To skip the rule while `line` is empty, guard it on `line` being set: make it
  the `then` of a `conditional` rule whose `when` is `record.line != null`. To refuse an
  empty `line`, make `line` required (`required: true`).
  ```
  
  ```text
  … A predicate resolves ONE hop through a single reference. To skip the rule while `line`
  is empty, guard it on `line` being set: make it the `then` of a `conditional` rule whose
  `when` is `record.line != null` — `record.line.id != null` inside the rule is no guard, as
  it reads through `line` too. To refuse an empty `line`, make `line` required
  (`required: true`). For a multi-value reference, test it with a macro (`exists`, `size`)
  instead of reading through it.
  ```
  
  The repair as an author writes it, measured end to end on insert and update. It accepts an order with no line or a public line, and refuses a secret line with the rule's own message:
  
  ```ts
  validations: [{
    name: 'no_secret_line_when_set', type: 'conditional',
    message: 'Only checked while the order names a line.',
    when: 'record.line != null',
    then: { name: 'no_secret_line', type: 'script', message: 'An order may not carry a secret line.',
            condition: "record.line.kind == 'secret'" },
  }]
  ```
  
  With `required: true` on `line` instead, an order with no line is refused at the field (`required`), and the rule still judges one with a line.
- Updated dependencies [863c7c4]
- Updated dependencies [0f95f43]
- Updated dependencies [825d70f]
- Updated dependencies [6057357]
- Updated dependencies [a60e04d]
- Updated dependencies [abc4b83]
- Updated dependencies [7382c5d]
- Updated dependencies [ea2940d]
- Updated dependencies [7d0f911]
- Updated dependencies [48f5200]
- Updated dependencies [245f360]
- Updated dependencies [d0f1845]
- Updated dependencies [9dcdb77]
- Updated dependencies [6175da8]
- Updated dependencies [0283cb9]
- Updated dependencies [324968e]
- Updated dependencies [7843663]
- Updated dependencies [ce57857]
- Updated dependencies [744a0a3]
- Updated dependencies [c7d4825]
- Updated dependencies [4844840]
- Updated dependencies [fe71032]
- Updated dependencies [74eaab8]
- Updated dependencies [0b788da]
- Updated dependencies [f7a3495]
- Updated dependencies [97f4f8c]
- Updated dependencies [482d34d]
- Updated dependencies [7a25a3e]
- Updated dependencies [839d1b0]
- Updated dependencies [2fc092b]
- Updated dependencies [2dfe070]
- Updated dependencies [6059b29]
- Updated dependencies [88a072e]
- Updated dependencies [d4a1a28]
- Updated dependencies [3d8779d]
- Updated dependencies [0bd7dae]
- Updated dependencies [d34f9b6]
- Updated dependencies [57343f7]
- Updated dependencies [271d6bb]
- Updated dependencies [1e20f81]
- Updated dependencies [38472ce]
- Updated dependencies [8b48903]
- Updated dependencies [2d235bc]
- Updated dependencies [aaacf1d]
- Updated dependencies [9dacf61]
- Updated dependencies [146c291]
- Updated dependencies [4db1bf1]
- Updated dependencies [e0e4a56]
- Updated dependencies [7aae005]
- Updated dependencies [bdb247d]
- Updated dependencies [d5c91dd]
- Updated dependencies [0e51278]
- Updated dependencies [48203ff]
- Updated dependencies [ada2869]
- Updated dependencies [d88a47d]
- Updated dependencies [2f1a6f6]
- Updated dependencies [23fc5d6]
- Updated dependencies [2d34f32]
- Updated dependencies [7b1e4a4]
- Updated dependencies [d7c0241]
- Updated dependencies [9e3c485]
- Updated dependencies [e1796ad]
- Updated dependencies [8271c81]
- Updated dependencies [c9eb773]
- Updated dependencies [fbc12be]
- Updated dependencies [ec2ede0]
- Updated dependencies [4342c99]
- Updated dependencies [132dd13]
- Updated dependencies [d285bf0]
- Updated dependencies [dfeba25]
- Updated dependencies [9059a94]
- Updated dependencies [0a88a80]
- Updated dependencies [2c1011b]
- Updated dependencies [12bb672]
- Updated dependencies [97233b9]
- Updated dependencies [c199772]
- Updated dependencies [f5a7250]
- Updated dependencies [1a2bb9e]
- Updated dependencies [eea7ccc]
- Updated dependencies [097d268]
- Updated dependencies [182bbde]
- Updated dependencies [5ce3705]
- Updated dependencies [24d622b]
- Updated dependencies [0252320]
- Updated dependencies [2eb4724]
- Updated dependencies [e04a0af]
- Updated dependencies [6b97a20]
- Updated dependencies [75237a9]
- Updated dependencies [497655f]
- Updated dependencies [ada7012]
- Updated dependencies [3a9ad22]
- Updated dependencies [6d2571f]
- Updated dependencies [2bf6ef1]
- Updated dependencies [092d460]
- Updated dependencies [09e16a5]
- Updated dependencies [98bd798]
- Updated dependencies [cbcae14]
- Updated dependencies [8261ff7]
- Updated dependencies [24489f1]
- Updated dependencies [fc28c1d]
- Updated dependencies [6d64785]
- Updated dependencies [00c332b]
- Updated dependencies [b3b43b6]
- Updated dependencies [d93400f]
- Updated dependencies [b1d3945]
- Updated dependencies [134b410]
- Updated dependencies [84e6b05]
- Updated dependencies [cb1f274]
- Updated dependencies [5c28cc7]
- Updated dependencies [b0eb9a5]
- Updated dependencies [e233db9]
- Updated dependencies [176b035]
- Updated dependencies [a83dbb6]
- Updated dependencies [d3a2331]
- Updated dependencies [51297e9]
- Updated dependencies [2d892dd]
- Updated dependencies [156792e]
- Updated dependencies [5ba2ec3]
- Updated dependencies [abb01f1]
- Updated dependencies [e64ae15]
- Updated dependencies [02bdeaa]
- Updated dependencies [66abef3]
- Updated dependencies [25c9a83]
- Updated dependencies [ee5812a]
- Updated dependencies [68fea8b]
- Updated dependencies [c049e74]
- Updated dependencies [bb9794a]
- Updated dependencies [d402e32]
- Updated dependencies [63a8eb4]
- Updated dependencies [9a910c4]
- Updated dependencies [adabccf]
- Updated dependencies [340b6dc]
- Updated dependencies [99fcb4a]
- Updated dependencies [55095cc]
- Updated dependencies [0f1cd83]
- Updated dependencies [a3d4c59]
- Updated dependencies [1aa5026]
- Updated dependencies [2b80461]
- Updated dependencies [2bdb81f]
- Updated dependencies [b9d5422]
- Updated dependencies [c7448dc]
- Updated dependencies [627382b]
- Updated dependencies [0b31d90]
- Updated dependencies [4b58dcf]
- Updated dependencies [c23cfb3]
- Updated dependencies [559041d]
- Updated dependencies [e0d0553]
- Updated dependencies [5100c42]
- Updated dependencies [596090e]
- Updated dependencies [5380daa]
- Updated dependencies [00b38d7]
- Updated dependencies [47a9002]
- Updated dependencies [7056ca5]
- Updated dependencies [731f020]
- Updated dependencies [5eebc9e]
- Updated dependencies [72c1640]
- Updated dependencies [5e5ec9f]
- Updated dependencies [170fd83]
- Updated dependencies [922923b]
- Updated dependencies [2cac363]
- Updated dependencies [fc91239]
- Updated dependencies [e6c34f6]
- Updated dependencies [062f5cd]
- Updated dependencies [5d8319f]
- Updated dependencies [43f4766]
- Updated dependencies [8e8ea99]
- Updated dependencies [a484966]
- Updated dependencies [021755a]
- Updated dependencies [b929e0a]
- Updated dependencies [dbd4744]
- Updated dependencies [14a762f]
- Updated dependencies [b146102]
- Updated dependencies [75c0dac]
- Updated dependencies [9bb059d]
- Updated dependencies [07c6f82]
- Updated dependencies [502f179]
- Updated dependencies [f20fe29]
- Updated dependencies [362035c]
- Updated dependencies [7e0bfce]
- Updated dependencies [c120dbd]
- Updated dependencies [32b5831]
- Updated dependencies [74554a3]
- Updated dependencies [e56112c]
- Updated dependencies [aeaaa44]
- Updated dependencies [43460b9]
- Updated dependencies [44a2332]
- Updated dependencies [f34dda6]
- Updated dependencies [488f4f5]
- Updated dependencies [15f9284]
- Updated dependencies [a4ca69a]
- Updated dependencies [1ff3a8f]
- Updated dependencies [61dd96f]
- Updated dependencies [b971924]
- Updated dependencies [6afa59d]
- Updated dependencies [e37ea4d]
- Updated dependencies [8f6d831]
- Updated dependencies [fa29803]
- Updated dependencies [b01bdbc]
- Updated dependencies [adbdbc5]
- Updated dependencies [6cc8dcd]
- Updated dependencies [ba77509]
- Updated dependencies [408ca2e]
- Updated dependencies [ec292cf]
- Updated dependencies [dc0ab6a]
- Updated dependencies [19e58e2]
- Updated dependencies [7e1b048]
- Updated dependencies [342808c]
- Updated dependencies [b3615f1]
- Updated dependencies [0b4022b]
- Updated dependencies [a60c913]
- Updated dependencies [5c5b67f]
- Updated dependencies [3f9e2ea]
- Updated dependencies [77f54bf]
- Updated dependencies [ccccdcc]
- Updated dependencies [48c91e9]
- Updated dependencies [2b52a5b]
- Updated dependencies [0f057b6]
- Updated dependencies [3875ae6]
- Updated dependencies [1c16889]
- Updated dependencies [1912237]
- Updated dependencies [fc29c74]
- Updated dependencies [95fb417]
- Updated dependencies [4ec3987]
- Updated dependencies [5b9402d]
- Updated dependencies [2cf9db7]
- Updated dependencies [dc1b986]
- Updated dependencies [655e8c0]
- Updated dependencies [041c8cf]
- Updated dependencies [e3277c3]
- Updated dependencies [cc6dfd9]
- Updated dependencies [7536721]
- Updated dependencies [9df3934]
- Updated dependencies [0b83e01]
- Updated dependencies [ebc6afe]
- Updated dependencies [6696056]
- Updated dependencies [0e06f3b]
- Updated dependencies [c1dfa52]
- Updated dependencies [2548ba5]
- Updated dependencies [9282578]
- Updated dependencies [ecf90b2]
- Updated dependencies [90ff10a]
- Updated dependencies [2bbebf5]
- Updated dependencies [369bcbe]
- Updated dependencies [3bd28e2]
- Updated dependencies [9347c1f]
- Updated dependencies [c164186]
- Updated dependencies [e7344f0]
- Updated dependencies [4d7e740]
- Updated dependencies [de091b5]
- Updated dependencies [6aa3188]
- Updated dependencies [ae7a35a]
- Updated dependencies [cf55914]
- Updated dependencies [17bd318]
- Updated dependencies [681868c]
- Updated dependencies [2274894]
- Updated dependencies [e462186]
- Updated dependencies [b5853da]
- Updated dependencies [4ac9319]
- Updated dependencies [560b724]
- Updated dependencies [16c5473]
- Updated dependencies [b276d44]
- Updated dependencies [3f86dc5]
- Updated dependencies [172b4cf]
- Updated dependencies [67c98f6]
- Updated dependencies [b98fbc2]
- Updated dependencies [e7f69db]
- Updated dependencies [84156c7]
- Updated dependencies [e0f17a3]
- Updated dependencies [0bf85ea]
- Updated dependencies [1df29df]
- Updated dependencies [8a44ce7]
- Updated dependencies [ca753c0]
- Updated dependencies [8ecbe0f]
- Updated dependencies [6a4aec7]
- Updated dependencies [e4471e6]
- Updated dependencies [e8fcf55]
- Updated dependencies [8d1f7ab]
- Updated dependencies [cfc3bcf]
- Updated dependencies [dd1b803]
- Updated dependencies [03d6cb0]
- Updated dependencies [9e7824a]
- Updated dependencies [08c8484]
- Updated dependencies [93cfc3f]
- Updated dependencies [6ac33a5]
- Updated dependencies [443b2f4]
- Updated dependencies [7e7fab7]
- Updated dependencies [b09ce67]
- Updated dependencies [4df101c]
- Updated dependencies [6a6a17b]
- Updated dependencies [733822c]
- Updated dependencies [a91d12a]
- Updated dependencies [bea6d2e]
- Updated dependencies [f415bcf]
- Updated dependencies [5f9d7d7]
- Updated dependencies [31d281d]
- Updated dependencies [569d4d2]
- Updated dependencies [9e9bb46]
- Updated dependencies [0d7ed5a]
- Updated dependencies [2aa25ef]
- Updated dependencies [0e1afe8]
- Updated dependencies [288611e]
- Updated dependencies [dfd8e39]
- Updated dependencies [28ad7e4]
- Updated dependencies [e6b7d8c]
- Updated dependencies [40b315b]
- Updated dependencies [f2c7eef]
- Updated dependencies [7e36a3c]
- Updated dependencies [5a6267f]
- Updated dependencies [0bbe400]
- Updated dependencies [862b6ce]
- Updated dependencies [80153f5]
- Updated dependencies [26daf0b]
- Updated dependencies [826f327]
- Updated dependencies [7e5246d]
- Updated dependencies [b810ddb]
- Updated dependencies [7dc45eb]
- Updated dependencies [17e4f52]
- Updated dependencies [dcd3bce]
- Updated dependencies [2d91c9a]
- Updated dependencies [b285508]
- Updated dependencies [2c31070]
- Updated dependencies [7db1332]
- Updated dependencies [aeb0557]
- Updated dependencies [1c1b8c8]
- Updated dependencies [05077d4]
- Updated dependencies [ba5927f]
- Updated dependencies [75b2169]
- Updated dependencies [de8c973]
- Updated dependencies [65352b7]
- Updated dependencies [e956924]
- Updated dependencies [2304b16]
- Updated dependencies [c7ad16f]
- Updated dependencies [48efe91]
- Updated dependencies [fb38607]
- Updated dependencies [dc07593]
- Updated dependencies [e967cbd]
- Updated dependencies [8255a51]
- Updated dependencies [d1c01ff]
- Updated dependencies [9e1689f]
- Updated dependencies [b057434]
- Updated dependencies [5f392f0]
- Updated dependencies [a362e0e]
- Updated dependencies [f26fb8e]
- Updated dependencies [0da638c]
- Updated dependencies [041d9fd]
- Updated dependencies [b8ec127]
- Updated dependencies [e81c4e5]
- Updated dependencies [28f9277]
- Updated dependencies [929d9e3]
- Updated dependencies [8a5240a]
- Updated dependencies [c1d54db]
- Updated dependencies [c7af6bd]
- Updated dependencies [1f0b565]
- Updated dependencies [23aa83c]
- Updated dependencies [357f499]
- Updated dependencies [80aef80]
- Updated dependencies [65ad77d]
- Updated dependencies [a61ae59]
- Updated dependencies [fb59fb5]
- Updated dependencies [a54ecaa]
- Updated dependencies [854639b]
- Updated dependencies [44c917a]
- Updated dependencies [613d35a]
- Updated dependencies [e08c8b0]
- Updated dependencies [0ee32ed]
- Updated dependencies [58b36fa]
- Updated dependencies [4792049]
- Updated dependencies [53ec0b1]
- Updated dependencies [0a56d3b]
- Updated dependencies [f8e5790]
- Updated dependencies [d2c1d19]
- Updated dependencies [681871e]
- Updated dependencies [54e8234]
- Updated dependencies [d127f9b]
- Updated dependencies [4bbf766]
- Updated dependencies [c17b494]
- Updated dependencies [d414e2b]
- Updated dependencies [af98a04]
- Updated dependencies [43cbe14]
- Updated dependencies [c86d351]
- Updated dependencies [6e3e546]
- Updated dependencies [c4d1759]
- Updated dependencies [f7a9740]
- Updated dependencies [96451ec]
- Updated dependencies [9cdffbe]
- Updated dependencies [331a1a2]
- Updated dependencies [9788f1e]
- Updated dependencies [3cf6449]
- Updated dependencies [3cf6449]
- Updated dependencies [2bd53f1]
- Updated dependencies [5f9f846]
- Updated dependencies [5d527f7]
- Updated dependencies [5bf2330]
- Updated dependencies [9165d5c]
- Updated dependencies [d9e1587]
- Updated dependencies [07150b3]
- Updated dependencies [143c715]
- Updated dependencies [fb2bccf]
- Updated dependencies [d2badf7]
- Updated dependencies [d64bcb6]
- Updated dependencies [d4f5232]
- Updated dependencies [396eae3]
- Updated dependencies [ecdfc94]
- Updated dependencies [f04be62]
- Updated dependencies [de1a611]
- Updated dependencies [4fba503]
- Updated dependencies [db76982]
- Updated dependencies [5cf58eb]
- Updated dependencies [66e266c]
- Updated dependencies [3b1dab9]
- Updated dependencies [7607076]
- Updated dependencies [1555ed4]
- Updated dependencies [776d64c]
- Updated dependencies [03b19d9]
- Updated dependencies [6154165]
- Updated dependencies [199002b]
- Updated dependencies [ab450f4]
- Updated dependencies [21ab410]
- Updated dependencies [025588a]
- Updated dependencies [a49e8ae]
- Updated dependencies [f3e3d59]
- Updated dependencies [9bd4344]
- Updated dependencies [51efbf1]
- Updated dependencies [9c44eed]
- Updated dependencies [bbca441]
- Updated dependencies [7cd5874]
- Updated dependencies [3cb84d0]
- Updated dependencies [119a02b]
- Updated dependencies [eea8787]
- Updated dependencies [7887077]
- Updated dependencies [29dd1a6]
  - @objectstack/spec@17.5.0

## 17.4.0

### Minor Changes

- 098cbb7: `validateExpression` now refuses a non-string expression `source` through `errors[]`, instead of throwing a raw `TypeError` that wiped out the caller's located reporting.
  
  `validateExpression(role, input)` accepts `string | { dialect?, source? }`, and read the envelope's `source` unguarded — `if (!source.trim())`. `ExprInput` declares `source?: string`, but every production call site casts, because the value comes out of **metadata**, where a declaration is a claim about stored data and not a guarantee about it. An envelope whose `source` was present and not a string therefore threw `TypeError: source.trim is not a function` out of a validator whose own docblock promises it never throws.
  
  **The defect was not "it throws" — it was that it threw the wrong kind and bypassed a whole located-reporting contract.** `AutomationEngine.validateFlowExpressions` collects located findings and throws one assembled error naming the flow, the node, the slot and the source (ADR-0032 §1d); `@objectstack/lint`'s stack walk attributes every finding to the hook, sharing rule, action or field it came from. An exception raised *inside* the shared validator skipped both, so the author was handed an internal message naming none of them. Measured before the fix, on a stack whose `hooks[].condition` was `{ source: { nested: 1 } }`: the whole `objectstack validate` run died on `source.trim is not a function`. After: one located `error` reading ``hook 'gate_hook' (lead) condition``.
  
  The guard sits at `toSource`, the entry `validateExpression` and `inferExpressionType` share — **once**, not in each caller's own `try`/`catch`, which is the tolerant-consumer shape Prime Directive #12 forbids. `validateExpression` returns `ok: false` with one `ExprValidationError` naming what was found and both authorable forms; `inferExpressionType` answers `'unknown'`, its existing "cannot prove a type".
  
  **No exported symbol or signature moves** — measured by diffing the built `dist/index.d.ts` before and after: 39 exported declarations on both sides, and `validateExpression`'s declaration byte-identical. What changes is behaviour at a published entry, which is why this is `minor` rather than `patch`: an input that previously produced **no verdict at all** now produces a rejection.
  
  **What does not change.** Absent, `null`, empty and whitespace-only sources still read as "not authored" (`ok: true`), an `{ ast }` envelope carrying no `source` is still admitted (its admission is `ExpressionSchema`'s rule, not this entry's), and a malformed *string* still gets its own diagnostic — the brace trap, the dialect mismatch, the unknown function — never the shape refusal. No input that previously returned `ok: true` now returns `ok: false`, and none that returned `ok: false` now returns `ok: true`.
  
  A caller that relied on catching the `TypeError` would need to read `result.ok` instead. None does: all nine production call sites (`@objectstack/lint` ×4, its docs gate ×2, `@objectstack/service-automation` ×3) read `.errors`/`.warnings` directly, and the one call site inside a `try` (`@objectstack/mcp`'s `validate_expression` tool) has a handler-level catch that degrades to an error result and declares its `expression` parameter `z.string()`.

### Patch Changes

- 86c75f4: `firstUndeclaredReference` now documents the side of its contract it was silent about: it can false-NEGATIVE, and a `null` is "nothing was reported", not "every reference is rooted".
  
  The existing sentence — "Acts ONLY on cel-js's `Unknown variable: X` fault, so it cannot false-positive on arithmetic/comparison overloads" — is true, and stays. What it never said is what that narrowing costs. cel-js's checker returns exactly ONE error, so when the first one is of another class every undeclared reference behind it in the same source goes unjudged and the helper answers `null` — the same value that means the source is clean. A contract that declares only which error it cannot make reads as making neither.
  
  No behaviour changes. This is the contract text, and it ships: the amended block is JSDoc on a published export, so it is emitted into `@objectstack/formula`'s `dist/index.d.ts` and `dist/index.d.mts` (measured — the declaration file grew 53.45 KB to 55.99 KB) and is what a consumer reads on hover.
  
  What the amendment adds, all of it measured rather than reasoned:
  
  - **The masking is positional, not name-keyed.** The masked name is not the one that triggered the first error, so excluding the trigger's own name does not reach it. `data == 'x' && status == 'q'` answers `null`; the same two names in the other order answer `"status"`.
  - **`celEngine.compile()` is not a gate against it.** `compile` type-checks in the permissive environment, where every unlisted name is `dyn`. The strict environment here declares `SCOPE_ROOTS` as `map`, so a root — or an object field sharing one of those names (`data`, `config`, `result`, `item`, `event`, `input`, `user`, …) — used as the operand of an operator with no `map` overload faults HERE and nowhere else. A caller that only reaches the helper on a clean compile is therefore not protected by its own gate.
  - **The CEL type-name class is the same shape.** `type == 'grid'` is already pinned as a blind spot in `@objectstack/lint`'s `visibility-bare-identifier` suite, but pinned per NAME; the masking it causes is source-wide.
  - **What closing it would take, and why that is not this change.** Widening the regex onto the overload message is the false positive the narrowing buys off (`type(record.x) == string` is legitimate CEL). Reporting past the first error needs a re-check loop over a neutralised source, or a checker entry returning more than one error — cel-js 8.0.0 has neither; its `TypeCheckResult` carries a single `error`. Both change what every consuming rule reports, so the oracle's shape is a design decision.
  
  `@objectstack/lint` carries a second comment-only correction, to `flow-variable-scope`'s account of the same oracle. Its "known, deliberate blind spot" note bounded the under-report to a flow variable named after a `SCOPE_ROOTS` member; measured, the bound does not hold — such a name in an operand position terminates the discovery loop on iteration 0 and every shadow in that source is lost, whatever it is named. That block sits on an internal function, so unlike the `formula` half it reaches no published declaration file; the entry is here because the package is touched and published.
- Updated dependencies [fe0d9a4]
- Updated dependencies [ecd2158]
- Updated dependencies [f2b5e46]
- Updated dependencies [ed7243d]
- Updated dependencies [6ba0db4]
- Updated dependencies [625b0c3]
- Updated dependencies [233222e]
- Updated dependencies [07f40e5]
- Updated dependencies [ceb4877]
- Updated dependencies [e9fcd6b]
- Updated dependencies [90e7e6d]
- Updated dependencies [2bdabe6]
- Updated dependencies [ca326b5]
- Updated dependencies [8f404a5]
- Updated dependencies [68437d4]
- Updated dependencies [abb140c]
- Updated dependencies [8333a6c]
- Updated dependencies [3e3ecb0]
- Updated dependencies [3030369]
- Updated dependencies [d5d8d50]
- Updated dependencies [e08892d]
- Updated dependencies [ae05f2e]
- Updated dependencies [b548e43]
- Updated dependencies [c463d03]
- Updated dependencies [64bd6a3]
- Updated dependencies [13c48c2]
- Updated dependencies [132742f]
- Updated dependencies [85a2459]
- Updated dependencies [50dc214]
- Updated dependencies [e89fa92]
- Updated dependencies [e9fcd6b]
- Updated dependencies [8976ea1]
- Updated dependencies [56fe8c2]
- Updated dependencies [acabd24]
- Updated dependencies [ab50c8f]
- Updated dependencies [6491463]
- Updated dependencies [89cf4d6]
- Updated dependencies [21c5dcb]
- Updated dependencies [6d4d5d3]
- Updated dependencies [ed5d557]
- Updated dependencies [bca21f7]
- Updated dependencies [e9fcd6b]
- Updated dependencies [1a7a7c9]
- Updated dependencies [e9fcd6b]
- Updated dependencies [ef3a138]
- Updated dependencies [68d5dfd]
- Updated dependencies [3e21cf0]
- Updated dependencies [4cfc93b]
- Updated dependencies [efd6b43]
- Updated dependencies [859ded3]
- Updated dependencies [fa125f3]
- Updated dependencies [74628d9]
- Updated dependencies [a646120]
- Updated dependencies [6f1ce7d]
- Updated dependencies [7778115]
- Updated dependencies [2c753fe]
- Updated dependencies [52804cd]
- Updated dependencies [3f89967]
- Updated dependencies [53cf263]
- Updated dependencies [21aabbc]
- Updated dependencies [9c270bb]
- Updated dependencies [76c8c5a]
- Updated dependencies [a84e1ce]
- Updated dependencies [bf1054a]
- Updated dependencies [d8d2776]
- Updated dependencies [222dc0f]
- Updated dependencies [e9fcd6b]
- Updated dependencies [32c917d]
- Updated dependencies [f9a3c32]
- Updated dependencies [f502898]
- Updated dependencies [af7edfe]
- Updated dependencies [b60f48b]
- Updated dependencies [c78c918]
- Updated dependencies [cf9bda4]
- Updated dependencies [784cb92]
- Updated dependencies [7629f4d]
- Updated dependencies [51df9fd]
- Updated dependencies [a7da4de]
- Updated dependencies [de0bcdd]
- Updated dependencies [70f7d6d]
- Updated dependencies [c677cda]
- Updated dependencies [554a160]
- Updated dependencies [f7da71e]
- Updated dependencies [7f745c3]
- Updated dependencies [5eb24f8]
- Updated dependencies [2a3decc]
- Updated dependencies [cc00df2]
- Updated dependencies [f4e6adf]
- Updated dependencies [ee4a59b]
- Updated dependencies [4db3c61]
- Updated dependencies [5ca314a]
- Updated dependencies [e0af1a8]
- Updated dependencies [414c1fc]
- Updated dependencies [22c0279]
- Updated dependencies [0db2947]
- Updated dependencies [92b5d7f]
- Updated dependencies [613bfbd]
- Updated dependencies [abae16a]
- Updated dependencies [094b8fd]
- Updated dependencies [c7aca0d]
- Updated dependencies [c1d8f98]
- Updated dependencies [8e0b297]
- Updated dependencies [5f7fa1d]
- Updated dependencies [87f0ccc]
- Updated dependencies [aedbaef]
- Updated dependencies [c5d6803]
- Updated dependencies [10d05bb]
- Updated dependencies [69602e5]
- Updated dependencies [c3ce76c]
- Updated dependencies [7936b29]
- Updated dependencies [46803fa]
- Updated dependencies [c2a336c]
- Updated dependencies [9f890d3]
- Updated dependencies [0bb2318]
- Updated dependencies [f7db8f4]
- Updated dependencies [1ecee3e]
- Updated dependencies [9408b7f]
- Updated dependencies [e9fcd6b]
- Updated dependencies [9bcd9be]
- Updated dependencies [b398ad2]
- Updated dependencies [99261a7]
- Updated dependencies [81b426f]
- Updated dependencies [001af1c]
- Updated dependencies [fb77aa5]
- Updated dependencies [581d8f8]
- Updated dependencies [f81afe3]
- Updated dependencies [40a44b9]
- Updated dependencies [7a7fb03]
- Updated dependencies [8fd246d]
  - @objectstack/spec@17.4.0

## 17.3.0

### Minor Changes

- 4a4a35d: feat(formula): `matchesFilter` resolves the `addDays` offset of a `{ $field }` reference
  
  A reference carrying `addDays` — an integer literal or a nested `{ $field }` reference to a
  numeric column (dot-paths walked, as for `$field`) — resolves to the referenced value
  shifted by that many whole days, in the shape it arrived in: a `YYYY-MM-DD` calendar day
  stays a calendar day (so a `$lte` still covers the whole shifted day), an ISO instant keeps
  its time of day, a `Date` stays a `Date`. A NULL offset contributes zero days; a NULL
  referenced column — or a value that cannot be read as a date, or an offset that is not a
  number — makes the comparison false for every operator, `$ne` included, so `$not` re-admits
  the row. A fractional offset value is truncated toward zero, the same reading the SQL
  dialects apply. Pinned against the same rows the SQL drivers' conformance corpus carries.
- 038f333: feat(lint,formula): refuse a visibility predicate that calls a function the CEL environment does not register (#13594)
  
  An accept-set narrowing at `objectstack validate` and at the runtime publish
  door, ruled by the maintainer on 2026-08-31 (director batch #21) on a censused
  premise.
  
  **The hole.** `validate-visibility-predicates` — the gate that judges
  `visibleWhen` on view form sections/fields and page components — was
  deliberately parse-only. So a predicate that parses perfectly and calls a
  function that does not exist passed CLEAN, measured side by side with two
  controls that fired:
  
  ```text
  source                       lint gate (before)   validateExpression
  totallyBogusFn(1,2)          CLEAN                ok=false
  record.x.nosuchmethod('a')   CLEAN                ok=false
  country === "USA"            syntax               ok=false   <- control
  status == 'active'           bare-identifier      ok=true    <- control
  ```
  
  The runtime fault it hides is the worst-shaped one the platform has: on a view
  or page surface it falls OPEN (the element renders unconditionally, identical
  to carrying no predicate at all), and on an action surface — evaluated with
  `throwOnError: true` — it falls CLOSED, so the action disappears for every
  user *including one who holds the grant*, behind a single deduped
  `console.warn` (objectui#4421). A plausible-looking function name that does not
  exist is exactly what a generator invents.
  
  **What changed.**
  
  - `@objectstack/formula` publishes `firstUnknownFunctionCall(source)` — the
    function-EXISTENCE verdict, isolated from everything else cel-js's `check()`
    has an opinion about. The oracle is the evaluation environment's own
    registration set, read through the same `buildEnv` seam `celEngine.compile`
    and `celEngine.evaluate` build with — never the advertised
    `CEL_STDLIB_FUNCTIONS` catalog, which lists 35 of the 72 registered names and
    would have refused 37 functions that resolve and evaluate today (`type`,
    `map`, `filter`, `split`, `getFullYear`, `json`, …).
  - `@objectstack/lint` gains `visibility-predicate-unknown-function`
    (**error**), covering both call forms — global (`totallyBogusFn(1,2)`) and
    receiver/member (`record.x.nosuchmethod('a')`). The message quotes the
    engine's own `found no matching overload for '…'` verbatim so publish time
    and run time read as one system, and offers **no** "did you mean" suggestion:
    nearest-name matching over the function namespace was measured to answer
    `min` for `can`.
  
  **Scoped supersession, not a widening.** The module's parse-only ruling stands
  for everything except function existence. A registered name called with wrong
  arguments (`upper(1, 2)`), a registered name called in the wrong position
  (bare `split('a,b')`), the CEL-type blind spot (`type == 'grid'`) and every
  operator-overload fault (`1 + 'a'`) are all still unreported — `type(record.x)
  == string` and every other legal `dyn` predicate is untouched. Refusing an
  unregistered call cannot be a false positive: the validation and runtime
  environments are the same builder (53 probes, 0 divergence), so the call this
  refuses is a call that would have faulted.
  
  **Migration.** A refused predicate names a function that does not exist and
  never evaluated — replace it with an advertised callable, or precompute the
  value into a formula field on the object and test that field. The census found
  **0** host-registered extra CEL functions across the reachable corpus
  (objectstack `packages/`/`examples/`/`apps/`, objectui, one shipped host app),
  with a firing positive control, and this repo's `examples/**` and `apps/**`
  sweep produces **0** new refusals. `cloud`, `objectos` and published
  third-party apps were NOT MEASURED — if a host in one of those registers extra
  CEL functions, its predicates are refused; that gap was declared before the
  ruling and accepted with it.

### Patch Changes

- 713f83f: fix(formula): fail closed on a null MEMBER of a resolved membership array in the CEL pushdown (#13496)
  
  `compileCelToFilter` already fails closed when a `current_user.*` variable
  resolves to `undefined`/`null` — the module's docblock calls it "the no active
  org fail-closed path" and it is pinned for the SCALAR case. `lowerMembership`
  did not apply the same discipline one level in: it checked only
  `Array.isArray(value)` and emitted the list verbatim, so a null MEMBER of a
  resolved membership array went straight into a security `$in`. The one shape
  that IS a permission predicate was the one shape that did not fail closed.
  
  `lowerMembership` now refuses a `null`/`undefined` member of a **variable-resolved**
  membership array with the same `unresolved-variable` reason the scalar path
  uses, which the RLS path already turns into the deny sentinel.
  
  Maintainer ruling, 2026-08-31 (quoted unchanged): 「membership 数组中的 null
  **成员**触发与 null 标量同款处置——`unresolved-variable` / deny sentinel,⛔ 不
  strip、不静默清洗。」
  
  **Why refuse rather than strip.** Stripping the unresolved member is safe in
  POSITIVE polarity only. `not in` is a supported, pinned member of the pushdown
  subset (`!(x in y)` lowers to `$not` wrapping `$in`), and `$in: []` matches
  nothing on every backend — so `$not { $in: [] }` matches the WHOLE table.
  Stripping therefore inverts into fail-OPEN exactly where the predicate is a
  blocklist, and it silently deletes a blocklist entry in the mixed
  `['u1', null]` case. Refusing needs no polarity awareness at all: it throws
  before any `$not` wrapper is built. Both polarities are pinned.
  
  **No shipped behaviour changes.** No first-party provider puts a null into a
  membership array — `resolve-authz-context.ts` filters non-strings out of
  `org_user_ids`, and the kernel spec declares `org_user_ids: z.array(z.string())`
  — so the refused shape was never a declared-valid input. This makes the
  implementation match the declaration rather than narrowing it. A fully resolved
  list, an empty list (`$in: []`, a legitimate declared predicate) and the
  authoring-time `isPushdownableCel` shape gate are all unchanged and pinned so.
  
  Out of scope, deliberately: an AUTHORED literal null inside a list
  (`record.status in ['lost', null]`) is a declared predicate, not an unresolved
  variable, and what such a filter should select is a separate open question. It
  is untouched, with a pin recording that.
- 77d4b3c: Pin `CEL_STDLIB_FUNCTIONS` to the real CEL `Environment` as a declared subset
  
  The exported catalog advertises 35 function names while the evaluation
  environment resolves 72, and nothing asserted the relationship between them. A
  new drift pin (`cel-stdlib-drift.test.ts`) reads the authoritative environment
  through `Environment.getDefinitions()` and holds three directions: every
  advertised name is registered **and bare-callable**; every bare-callable
  function `registerStdLib` adds is advertised; and the bare-callable built-ins
  deliberately withheld (`bytes`, `dyn`, `type`, `uint`) are exactly a declared
  list, so a cel-js upgrade cannot add one unnoticed.
  
  The catalog's contents are unchanged. Its docblock now records the measured
  decomposition of the 72 and states the membership rule that makes the gap
  deliberate rather than stale: 33 of the 72 are callable only on a receiver
  (`s.split(',')`), and every consumer spends an entry as a bare call.
- 1d8ad0f: fix(formula): a stdlib function written as a method gets the bare call shape, not the dialect (#14203)
  
  `validateExpression` refused `record.name.upper()` correctly and then handed the
  author the generic dialect trailer — "`predicate`s are bare CEL (e.g.
  `record.rating >= 4`)" — advice that cannot succeed on a source that already IS
  bare CEL and parses fine. The third instance of the same defect family as the
  `bounds` class (#7073) and the unknown-name class (#13821), and the one neither
  of them could cover: #13821's arm fires only when the name is ABSENT from
  `CEL_STDLIB_FUNCTIONS`, and `upper` is present, so this class had no
  prescription at all. The name is right; the call SHAPE is wrong.
  
  It is a high-frequency AI-author mistake, not an exotic one: method-call syntax
  is what almost every other language uses for string operations, so a generator
  that knows `upper` exists reaches for `record.name.upper()` before
  `upper(record.name)`. The remedy is one sentence and it is mechanical — the
  correct spelling is derivable from the fault itself:
  
  ```
  invalid CEL predicate: found no matching overload for 'dyn.upper()'
  
  >    1 | record.name.upper()
           ^ — `upper` is callable bare, not as a method — a CALL-SHAPE fault, not
  a dialect mistake, so re-spelling the expression will not fix it. Write
  `upper(record.name)` instead. The callable names this platform advertises for
  authoring (the `functions` list `introspectScope` returns,
  `CEL_STDLIB_FUNCTIONS`) take their subject as an argument; only cel-js's own
  receiver methods (`record.name.split(',')`) are written after a dot.
  ```
  
  The spelling is assembled from the SOURCE, because cel-js's message names the
  receiver's TYPE (`dyn.upper()`) and never the author's expression. When the
  receiver is not a plain dotted chain (`record.tags[0].upper()`,
  `(a + b).upper()`, `'lit'.upper()`) the message names the call shape —
  `upper(…)` with the receiver as its first argument — rather than inventing a
  spelling it cannot derive.
  
  The arm is keyed on membership of the bare-callable catalog plus the
  environment's own record of the receiver form, never on the call shape alone.
  Two classes therefore keep exactly the behaviour they had:
  
  - the 33 receiver-only names cel-js registers (`split`, `map`, `getFullYear`)
    are correct ONLY after a dot — `record.name.split(',')` type-checks and never
    reaches this arm;
  - the seven advertised names registered BOTH ways (`contains`, `endsWith`,
    `matches`, `size`, `startsWith`, `string`, `trim`) keep the existing trailer
    when a receiver call of them faults, because the fault there is the arguments
    (`record.name.contains()`), and a bare rewrite would fault just as hard.
  
  No change to `CEL_STDLIB_FUNCTIONS`, to the registered environment, or to what
  `validateExpression` accepts: the receiver call was refused before this change
  and is refused after it. Only the sentence the author is told to act on changes.
- 9738c35: fix(formula): an unknown-function refusal names the function and points at the callable set (#13821)
  
  `validateExpression` refused an unknown CEL function correctly and then handed
  the author a prescription that could not succeed: "`predicate`s are bare CEL
  (e.g. `record.rating >= 4`)" — advice to write bare CEL, on a source that
  already is bare CEL and parses fine. An unknown-function fault is graded `type`
  by the engine's own `check()`, so it fell through `bracesHint` (null, no brace)
  and out to that generic dialect trailer.
  
  This is the second leg of the repair #7073 / PR #7209 made for the `bounds`
  class, for the same reason `boundsHint`'s doc-comment gives: an author who obeys
  the last sentence they were given — an LLM author above all — rewrites the
  dialect, learns nothing, and comes back with the same unresolvable name. The
  last sentence pointed at the one thing that was already correct.
  
  The `type` class now gets its own prescription, which **names the function that
  did not resolve** and points at the callable set `introspectScope` publishes:
  
  ```
  invalid CEL predicate: found no matching overload for 'dyn.nosuchmethod(string)'
  
  >    1 | record.x.nosuchmethod('a')
           ^ — `nosuchmethod` is not a callable name here — a NAME fault, not a
  dialect mistake, so re-spelling the expression will not fix it. The callable
  names this platform advertises for authoring are the `functions` list
  `introspectScope` returns (`CEL_STDLIB_FUNCTIONS`) — pick one of those, or
  precompute the value in a stored field and reference that field instead.
  ```
  
  The front half is unchanged: it is cel-js's own vocabulary and matches the
  runtime fault exactly. Only the trailer after the dash is new. `CEL_STDLIB_FUNCTIONS`
  itself is untouched, and the message names no member count — what the catalog
  contains is being adjudicated separately, and a sentence asserting a size would
  be falsified by that ruling.
  
  **The did-you-mean suggestion is thresholded, and the threshold is the point.**
  Against this catalog the shared `nearestName` budget answers
  `nearestName('can', CEL_STDLIB_FUNCTIONS)` with `'min'` — two edits on a
  three-character name, a jump from a permission verb to a numeric function. That
  suggestion is worse than silence: an author who takes it writes
  `min(object, verb)`. This class therefore narrows locally to at most one edit per
  three characters of the longer name, keeping the case that makes suggesting
  worthwhile (`isBlnk` → `isBlank`) and refusing the measured hazard (`can` → no
  suggestion). Both cases are pinned. The shared `nearestName` budget is unchanged,
  so field-name suggestions are unaffected.
  
  Message-only. The refusal fires on exactly the same inputs it did before — no
  rule id, severity, match set or gate behaviour changed. Faults in the `type`
  class that name no unresolvable call keep the existing trailer: an operator or
  ternary mismatch (`1 + 'a'`), and a real function handed arguments no overload
  accepts (`upper(1, 2)`, which produces the same cel-js message shape) — calling
  `upper` "not a callable name" would replace a useless sentence with a false one.
- Updated dependencies [809d417]
- Updated dependencies [387e231]
- Updated dependencies [f794e4e]
- Updated dependencies [cae2169]
- Updated dependencies [b812a54]
- Updated dependencies [2d4fa75]
- Updated dependencies [0e4e51b]
- Updated dependencies [e84bbf6]
- Updated dependencies [effae80]
- Updated dependencies [d62f990]
- Updated dependencies [c45d8e6]
- Updated dependencies [2e3e8c7]
- Updated dependencies [e621291]
- Updated dependencies [40a93b5]
- Updated dependencies [d5b330d]
- Updated dependencies [dda969c]
- Updated dependencies [1f45690]
- Updated dependencies [277948f]
- Updated dependencies [8bdd955]
- Updated dependencies [f3bbbef]
- Updated dependencies [4f24e9d]
- Updated dependencies [474242f]
- Updated dependencies [63cd487]
- Updated dependencies [bd4aa4e]
- Updated dependencies [803eaab]
- Updated dependencies [f8e8f03]
- Updated dependencies [eae824e]
- Updated dependencies [f6fa22c]
- Updated dependencies [8a483b3]
- Updated dependencies [97bcd99]
- Updated dependencies [df59de0]
- Updated dependencies [96e25a8]
- Updated dependencies [f75a38a]
- Updated dependencies [7a25e7d]
- Updated dependencies [1fa05a6]
- Updated dependencies [c85a265]
- Updated dependencies [dcb10a5]
- Updated dependencies [773a999]
- Updated dependencies [35dffea]
- Updated dependencies [776a098]
- Updated dependencies [5060877]
- Updated dependencies [4f6325d]
- Updated dependencies [52954c0]
- Updated dependencies [2aa8456]
- Updated dependencies [93809a3]
- Updated dependencies [7c0d0c3]
- Updated dependencies [daae7aa]
- Updated dependencies [8dc22d6]
- Updated dependencies [279431e]
- Updated dependencies [948dd6b]
- Updated dependencies [3b4c56c]
- Updated dependencies [ae8edd2]
- Updated dependencies [e25403c]
- Updated dependencies [64baa68]
- Updated dependencies [9fa70d7]
- Updated dependencies [09db64a]
- Updated dependencies [92916e7]
- Updated dependencies [a84f3ea]
- Updated dependencies [f2eaae8]
- Updated dependencies [c09451b]
- Updated dependencies [ba64877]
- Updated dependencies [7345308]
- Updated dependencies [79b6a22]
- Updated dependencies [30d96ab]
- Updated dependencies [f658793]
- Updated dependencies [c95ad19]
- Updated dependencies [e58ea8b]
- Updated dependencies [4a17645]
- Updated dependencies [3795c5f]
- Updated dependencies [8ab926b]
- Updated dependencies [7317cf2]
- Updated dependencies [e25e839]
- Updated dependencies [5997207]
- Updated dependencies [8b13cc8]
- Updated dependencies [4a4a35d]
- Updated dependencies [86e765a]
- Updated dependencies [1d7e76a]
- Updated dependencies [53dc739]
- Updated dependencies [fd289be]
- Updated dependencies [03bf7b1]
- Updated dependencies [f90e820]
- Updated dependencies [18d816a]
- Updated dependencies [e8bd715]
- Updated dependencies [b91c351]
- Updated dependencies [a28a3c0]
- Updated dependencies [daeaaf9]
- Updated dependencies [c459da6]
- Updated dependencies [e914733]
- Updated dependencies [f887e52]
- Updated dependencies [881f8d8]
- Updated dependencies [3bfa1e6]
- Updated dependencies [901355c]
- Updated dependencies [34ce8e7]
- Updated dependencies [33681ea]
- Updated dependencies [4635f3e]
- Updated dependencies [ee3595c]
- Updated dependencies [b2eab95]
- Updated dependencies [93940d4]
- Updated dependencies [3a04b01]
- Updated dependencies [45b9051]
- Updated dependencies [b9e9227]
- Updated dependencies [d395692]
- Updated dependencies [5894d30]
- Updated dependencies [a3765f6]
- Updated dependencies [e22158f]
- Updated dependencies [7404925]
- Updated dependencies [0c2334f]
- Updated dependencies [778c59f]
- Updated dependencies [d2619fd]
- Updated dependencies [6acb11a]
- Updated dependencies [33c5fd3]
- Updated dependencies [20b0fdb]
- Updated dependencies [905019b]
- Updated dependencies [a286411]
- Updated dependencies [98c0d33]
- Updated dependencies [368a82e]
- Updated dependencies [a3d5724]
- Updated dependencies [93ea19b]
- Updated dependencies [9ee2dcf]
- Updated dependencies [8cb96ec]
- Updated dependencies [8f10a79]
- Updated dependencies [6269a55]
- Updated dependencies [0fb8760]
- Updated dependencies [e5ce2ed]
- Updated dependencies [be21955]
- Updated dependencies [bc56e18]
- Updated dependencies [be21955]
- Updated dependencies [a9ee989]
- Updated dependencies [4d0d944]
- Updated dependencies [15d58db]
- Updated dependencies [d63b014]
- Updated dependencies [9abe4e4]
- Updated dependencies [2cc7122]
- Updated dependencies [50d6c92]
- Updated dependencies [9e0ba21]
- Updated dependencies [311433f]
- Updated dependencies [3e5ad08]
- Updated dependencies [9abe4e4]
- Updated dependencies [b7131f3]
- Updated dependencies [e5812fa]
- Updated dependencies [7085f90]
- Updated dependencies [dee4dd4]
- Updated dependencies [ce7e497]
- Updated dependencies [51ecb2f]
- Updated dependencies [9086761]
- Updated dependencies [42a117b]
- Updated dependencies [1401ae7]
- Updated dependencies [4297fe7]
- Updated dependencies [e398863]
- Updated dependencies [d16df74]
- Updated dependencies [f11fc61]
- Updated dependencies [e808890]
- Updated dependencies [8f79379]
- Updated dependencies [e6ca40e]
- Updated dependencies [0c77ea4]
- Updated dependencies [52954c0]
- Updated dependencies [89eb997]
- Updated dependencies [aa5994e]
- Updated dependencies [be93457]
- Updated dependencies [a65db76]
- Updated dependencies [15eb2c9]
- Updated dependencies [5691b07]
- Updated dependencies [2a6122b]
- Updated dependencies [225e769]
- Updated dependencies [8af88dd]
- Updated dependencies [fb5fbb8]
- Updated dependencies [d7b3963]
- Updated dependencies [b72db01]
- Updated dependencies [dce5cd4]
- Updated dependencies [177ebdc]
- Updated dependencies [8d237b4]
- Updated dependencies [2d2e6f0]
- Updated dependencies [2d8dd8d]
- Updated dependencies [22d573e]
- Updated dependencies [b5a2398]
- Updated dependencies [348860c]
- Updated dependencies [5383fa6]
- Updated dependencies [5b3ff63]
- Updated dependencies [1a6a19c]
- Updated dependencies [527e050]
- Updated dependencies [dd33bf9]
- Updated dependencies [4cb2a90]
- Updated dependencies [74a7804]
- Updated dependencies [53d3689]
- Updated dependencies [b3a63d3]
- Updated dependencies [033a34c]
- Updated dependencies [4d25d22]
- Updated dependencies [1ffee51]
- Updated dependencies [5ae4303]
- Updated dependencies [ece4dad]
- Updated dependencies [e9b377e]
- Updated dependencies [146f448]
- Updated dependencies [735f5c7]
- Updated dependencies [a7e18de]
- Updated dependencies [366f895]
- Updated dependencies [dc75ba8]
- Updated dependencies [cce0aa9]
- Updated dependencies [e764507]
- Updated dependencies [cff17af]
- Updated dependencies [39404f3]
- Updated dependencies [ca1965f]
- Updated dependencies [8619f95]
- Updated dependencies [b706af9]
- Updated dependencies [fc9ba76]
- Updated dependencies [0f94cc7]
- Updated dependencies [a11c1a5]
- Updated dependencies [71f9cd1]
- Updated dependencies [ee17d86]
- Updated dependencies [cdbd920]
- Updated dependencies [18c432e]
- Updated dependencies [3c418c4]
- Updated dependencies [fa8715a]
- Updated dependencies [a933ed7]
- Updated dependencies [b3ca463]
- Updated dependencies [a933ed7]
- Updated dependencies [0d4a6a8]
- Updated dependencies [518d5e5]
- Updated dependencies [6643ba1]
- Updated dependencies [eeba2ef]
- Updated dependencies [ec4c4d2]
- Updated dependencies [424f73c]
- Updated dependencies [cccbe51]
- Updated dependencies [a8d6b1d]
- Updated dependencies [e4a7695]
- Updated dependencies [87075b1]
- Updated dependencies [fc58a99]
- Updated dependencies [14cfc00]
- Updated dependencies [1c6f7b4]
- Updated dependencies [e854a53]
- Updated dependencies [dfebfc8]
- Updated dependencies [d028b37]
- Updated dependencies [122ef38]
- Updated dependencies [4a37870]
- Updated dependencies [428f9b2]
- Updated dependencies [aa7ff56]
- Updated dependencies [c41b42e]
- Updated dependencies [c4db311]
- Updated dependencies [750fff5]
- Updated dependencies [c19035e]
- Updated dependencies [ececf7a]
- Updated dependencies [d173125]
- Updated dependencies [8eeca27]
- Updated dependencies [8425c17]
- Updated dependencies [a5ef1d8]
- Updated dependencies [772d5de]
- Updated dependencies [ce80ec2]
- Updated dependencies [b372318]
- Updated dependencies [97a2263]
- Updated dependencies [29d0676]
- Updated dependencies [0169d49]
- Updated dependencies [6bd3231]
- Updated dependencies [d2b5ba8]
- Updated dependencies [b799ac5]
- Updated dependencies [8f74307]
- Updated dependencies [d23dc08]
- Updated dependencies [644ad50]
- Updated dependencies [0da7cd2]
- Updated dependencies [28a5c3e]
- Updated dependencies [4bc18e5]
  - @objectstack/spec@17.3.0

## 17.2.0

### Patch Changes

- Updated dependencies [6936d07]
- Updated dependencies [59eb04d]
- Updated dependencies [9f05b7d]
- Updated dependencies [7d2d112]
- Updated dependencies [5fa0d72]
- Updated dependencies [02b3b07]
- Updated dependencies [914c413]
- Updated dependencies [55809a0]
- Updated dependencies [52db1d1]
- Updated dependencies [5649efb]
- Updated dependencies [2306a76]
- Updated dependencies [e5ea701]
- Updated dependencies [a40dcc1]
- Updated dependencies [def0d3e]
- Updated dependencies [8d0bb79]
- Updated dependencies [5acb58d]
- Updated dependencies [2e3cf95]
- Updated dependencies [4c93387]
- Updated dependencies [a037f7c]
- Updated dependencies [3ee8ddf]
- Updated dependencies [16cef97]
- Updated dependencies [a79bd35]
- Updated dependencies [6ceaa4b]
- Updated dependencies [15ea214]
- Updated dependencies [de19489]
- Updated dependencies [c684d00]
- Updated dependencies [923c424]
- Updated dependencies [1ec36b7]
- Updated dependencies [5f2e54c]
- Updated dependencies [189373b]
- Updated dependencies [35ad101]
- Updated dependencies [ceb33a9]
- Updated dependencies [73d9795]
- Updated dependencies [8012960]
- Updated dependencies [f34f56b]
- Updated dependencies [f399618]
- Updated dependencies [75e9301]
- Updated dependencies [2810695]
  - @objectstack/spec@17.2.0

## 17.1.0

### Patch Changes

- Updated dependencies [56656aa]
- Updated dependencies [07e630e]
- Updated dependencies [2f65b1b]
- Updated dependencies [720ee95]
- Updated dependencies [f287435]
- Updated dependencies [9aa8890]
- Updated dependencies [7c9c1dd]
- Updated dependencies [75b7c24]
- Updated dependencies [d5552ca]
- Updated dependencies [d9813a9]
- Updated dependencies [8640fb2]
- Updated dependencies [2420641]
- Updated dependencies [2ad91c3]
- Updated dependencies [f57fb38]
- Updated dependencies [00777a0]
- Updated dependencies [d491625]
- Updated dependencies [420804d]
- Updated dependencies [716ac9b]
- Updated dependencies [62b1427]
- Updated dependencies [7ea1372]
- Updated dependencies [23abe27]
- Updated dependencies [985a9cd]
- Updated dependencies [a8189ae]
- Updated dependencies [26e70fb]
- Updated dependencies [42b05af]
- Updated dependencies [2b292ce]
- Updated dependencies [abcf853]
- Updated dependencies [8b9eba5]
- Updated dependencies [d575779]
- Updated dependencies [94f7ef8]
- Updated dependencies [c5ac5e4]
- Updated dependencies [a777944]
- Updated dependencies [dd88e1c]
- Updated dependencies [856527c]
- Updated dependencies [870f710]
- Updated dependencies [79c46da]
- Updated dependencies [7ff3975]
- Updated dependencies [29d055b]
- Updated dependencies [65589d6]
- Updated dependencies [2c86fe3]
- Updated dependencies [e196c6a]
- Updated dependencies [4ab7523]
- Updated dependencies [19539b4]
- Updated dependencies [11b779e]
- Updated dependencies [739fe5b]
- Updated dependencies [4bfe1a5]
- Updated dependencies [2065e31]
- Updated dependencies [b69d0f5]
- Updated dependencies [4d47afe]
- Updated dependencies [e4e5c6e]
- Updated dependencies [9a56784]
- Updated dependencies [d00d2f6]
- Updated dependencies [df0c12d]
- Updated dependencies [d31785f]
- Updated dependencies [c308a4f]
- Updated dependencies [e2899f6]
- Updated dependencies [3851f87]
- Updated dependencies [2a29caa]
- Updated dependencies [09a6eee]
- Updated dependencies [1a7f907]
- Updated dependencies [cd455c8]
- Updated dependencies [30d3752]
- Updated dependencies [c80e7ae]
- Updated dependencies [09a9a8a]
- Updated dependencies [07026cf]
- Updated dependencies [5d4f3d5]
- Updated dependencies [4d80e8b]
- Updated dependencies [30b1c63]
- Updated dependencies [079b457]
- Updated dependencies [e43b211]
- Updated dependencies [890b38f]
- Updated dependencies [8bee54b]
- Updated dependencies [7a537ce]
- Updated dependencies [593c4bf]
- Updated dependencies [ff08691]
- Updated dependencies [60e0f90]
- Updated dependencies [90c5285]
- Updated dependencies [7901b2d]
- Updated dependencies [56bca91]
- Updated dependencies [79394d7]
- Updated dependencies [730fd9a]
- Updated dependencies [44bc51d]
- Updated dependencies [73cfddf]
- Updated dependencies [d634e66]
  - @objectstack/spec@17.1.0

## 17.0.0

### Minor Changes

- f6cd635: fix(formula): the CEL pushdown compiler parses through the canonical front end, so `DEFAULT_LIMITS` finally apply to RLS/sharing predicates (#6132)

  `cel-to-filter.ts` — the ONE canonical CEL → `FilterCondition` pushdown compiler
  (ADR-0058 D1/D2/D6), consumed by the RLS path (`plugin-security`'s
  `RLSCompiler`), the sharing seeder (`plugin-sharing`), and the analytics SQL
  backend — kept a **private, limitless** parse environment of its own:

  ```ts
  new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true });
  ```

  no `limits`, no stdlib, no `rewriteNullableTernary`. That made the pushdown path
  the one place on the platform that answered a _different_ question from
  `celEngine.compile()` about what parses. Measured: a 300-term addition, a
  60-level parenthesis nest and a 200-element list literal all parsed there while
  the interpreter refused each one outright (`Exceeded maxAstNodes (256)` /
  `maxDepth (32)` / `maxListElements (64)`). Escalated: an 80-term conjunction, a
  40-level nest and a 200-element `$in` all reached **real pushdown SQL**,
  silently — and `isSupportedRlsExpression`, the ADR-0056 D4 authoring gate, was a
  thin wrapper over the same limitless environment, so it was no independent check
  either.

  It now parses through `parseCelToAstWithReason` — #4812's canonical entry, with
  `DEFAULT_LIMITS`, the stdlib and the #3306 null-guard rewrite. "What parses" has
  one answer again.

  **Within the limits nothing moves, and that is measured, not asserted.** Across
  the 710 sources of the pushdown corpus that both front ends accept, the only AST
  difference is `rewriteNullableTernary`'s `dyn(…)` wrap on the three null-guard
  ternaries — and a ternary faults on its own `?:` node before the lowerer
  descends into a branch, so verdict _and_ detail come out byte-identical. Pinned
  in `cel-to-filter-parse-convergence.test.ts`, which rebuilds the old environment
  to compare against.

  **Over the limits, behaviour changes — in two dated steps.**

  - **Now, during `17.0.0-rc.x` (`rc-grace`):** an over-limit predicate **still
    compiles** — nothing that enforces today stops enforcing on this upgrade — and
    emits one WARN per predicate naming the bound that was exceeded
    (`maxAstNodes` / `maxDepth` / `maxListElements` / …), the platform's value for
    it, and what the predicate itself measures (cel-js's own accounting: the
    smallest bound it parses under), plus what will happen at GA.
  - **At v17.0.0 GA (`fail-closed`):** the same predicate is **refused** —
    `{ ok: false, reason: 'parse-error', detail: 'Exceeded maxAstNodes (256)' }` —
    and the RLS path turns that into `RLS_DENY_FILTER`, i.e. zero rows, fail
    closed. A sharing rule with such a condition is not seeded.

  **The flip is one line.** `CEL_PUSHDOWN_LIMITS_MODE` in
  `packages/formula/src/cel-pushdown-limits.ts` — the single dated switch,
  shipping as `'rc-grace'`, to be set to `'fail-closed'` at the v17.0.0 GA release
  (i.e. when this package's version leaves `17.0.0-rc.x`). Both positions are
  exercised in CI today, in `@objectstack/formula` and in
  `@objectstack/plugin-security` (where the `RLS_DENY_FILTER` outcome lives), so
  the GA half is proven before it ships rather than after. Two tests are written
  to go red on that line so the flip cannot be silent.

  **If you author RLS or sharing predicates:** a predicate over any of these
  bounds is already refused everywhere else on the platform (`os build`,
  `os validate`, the interpreter). Split it, or move the logic into a hook/action
  body (`ScriptBody { language: 'js' }`), before upgrading past the rc line. The
  WARN names the predicate and its measure so you can find them.

  **New public surface**, for consumers that must _report_ a refusal rather than
  merely react to one:

  - `parseCelToAstWithReason(source, opts?)` — the reason-carrying sister entrance
    to `parseCelToAst`. Same front end, same verdict, but it distinguishes
    `'parse'` (not valid CEL) from `'bounds'` (valid CEL, over budget) and names
    the exceeded limit, its platform value, and the source's measure. Graded by
    the same by-class/by-code classifier `celEngine.compile` uses (#6223) — never
    by error prose. `parseCelToAst` is unchanged and still collapses every refusal
    to `null`.
  - `CelParseResult`, `CelBoundsOverrun`, `CelLimitKey`, `ParseCelToAstOptions`.
  - `CEL_PUSHDOWN_LIMITS_MODE`, `celPushdownLimitsMode()`,
    `setCelPushdownLimitsModeForTests()`, `CelPushdownLimitsMode`.

  `@objectstack/lint` needs no change, at either position of the switch. Its two
  enforceability gates read `isSupportedRlsExpression` and `compileCelToFilter`,
  both downstream of this switch, and both suites pin "the lint verdict IS the
  consumer's verdict" in both directions — so authoring-time reporting flips with
  the runtime by construction. An over-limit sharing `condition` is in fact
  already an authoring **error** today (`expression-invalid`, from the general
  expression rule, quoting `Exceeded maxAstNodes (256)`), because that rule has
  always gone through the canonical front end.

- 0f17114: fix(driver-sql,driver-memory,formula)!: `{ field: {} }` 一律拒收 —— 零个操作符的字段约束不再在四个后端有三个答案 (#5240)

  `{ a: {} }`(一个字段,后面跟零个操作符)是 `FilterConditionSchema` 今天**声明合法**的形状,
  而同一个 filter 在同仓四条路径上有三个答案:

  | 路径                                | 改前                                                                                            | 改后                          |
  | ----------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------- |
  | `driver-sql`,顶层 plain map         | 抛 `INVALID_FILTER`(#5041 的比较数闸门)                                                         | 抛 `INVALID_FILTER`(专用消息) |
  | `driver-sql`,`$and`/`$or`/`$not` 内 | 遍历零个操作符 → 不产出任何 SQL → **TRUE(匹配全表)**                                            | 抛 `INVALID_FILTER`           |
  | `driver-memory`                     | 实时路径经 mingo 变成「字段深等于空文档」;参考匹配器落到 `JSON.stringify` 结构相等 → 顺带 FALSE | 抛 `INVALID_FILTER`           |
  | `@objectstack/formula`              | `keys.length === 0` 显式 fail-closed → FALSE                                                    | 抛 `INVALID_FILTER`           |

  于是 `{ $or: [ { a: {} }, { b: 2 } ] }` 在 SQL 上编译成 `(b = 2)` —— 既不是「零约束即 TRUE」
  该给的全表,也不是两个 JS 后端给的 FALSE,而是**子句被 knex 连同空分组一起丢掉**的结果;
  而 `driver-sql` 自己内部就不自洽:同一个 `{ a: {} }` 写在顶层被响亮拒收,包进一层 `$or`
  就变成静默的 TRUE。

  维护者拍板取**拒收**(不取 TRUE、不取 FALSE):这个形状几乎必然是编写期事故 ——
  筛选器记下了字段却没记下操作符,或生成的元数据把操作符弄丢了 —— 让它在编写期就炸,
  好过在某个后端上安静地多返回或少返回几行。与 #5041 已在 driver-sql 顶层建立的先例一致,
  本次只是把同一道闸门补进组合子内部。四个后端(第四个是继承 `SqlDriver` 的
  `driver-sqlite-wasm`)现在给出同一个 `INVALID_FILTER` / 400,消息里指名出事的位置
  (如 `filter.$or[0].stage`)。

  **⚠️ 可观察的行为变更 —— RLS `check` 求值路径。** `@objectstack/formula` 的
  `matchesFilterCondition` 是 `plugin-security` 对 insert/update **后像**执行行级 `check`
  的那条路径(没有查询可下推,这个求值器就是执行本身)。它改为抛出后,落在 #4775
  「求不出值 = 该次操作失败」的既定姿态上。这不只是「拒绝得更响」——有一类结果直接翻转:

  | `check` 策略                                    | 改前                                  | 改后                     |
  | ----------------------------------------------- | ------------------------------------- | ------------------------ |
  | `{ a: {} }`                                     | FALSE → 写入被拒(403)                 | 抛出 → 该次写入失败(400) |
  | `{ $or: [ { a: {} }, { owner: '{userId}' } ] }` | FALSE 被另一析取项吸收 → 写入**放行** | 抛出 → 该次写入失败      |
  | `{ $not: { a: {} } }`                           | `!false` → 写入**放行**               | 抛出 → 该次写入失败      |

  后两行是**原本能成功、现在会失败**的写入。这是拍板的目的而非副作用:一条含
  `{ field: {} }` 的权限规则,是一条作者弄丢了操作符的规则,它的含义不该取决于四个后端里
  哪一个在求值。升级后请检查 `check`/`using` 策略里是否存在零操作符的字段约束——
  错误消息会指名位置。

  同一条改动也让 `@objectstack/driver-memory` 的两个过滤面(经 mingo 的实时查询路径,
  与跨后端一致性套件所用的 `memory-matcher` 参考匹配器)第一次对这个形状给出同一个答案。

  非空形状**逐字符不变**:普通比较、`$in`、`$or`/`$and` 组合、`$not` 的 #5146 NULL-safe 改写,
  编译出的 SQL 文本与匹配结果都与改前相同;`{}`(零个键的**节点**,#5134 的布尔单位元)
  与 `{ field: {} }` 是两个不同形状,前者的语义不受本次影响。

  注:本次收紧的是**实现**。`packages/spec` 的 `FilterConditionSchema` 仍然声明这个形状合法
  (非递归半边是 `z.record(z.string(), z.unknown())`),即实现现在比已声明的契约更严;
  契约收窄与 `FILTER_LOGIC_CASES` 补条归 spec 车道另行处理。

- 2fa4ca1: Dynamic approver routing for approval nodes (#3447 P2) — three new declarative capabilities:

  **`expression` approvers.** A new approver type whose CEL expression resolves WHO approves at node entry, over exactly three roots: `current.*` (the record's live state), `trigger.*` (the submit-time snapshot) and `vars.*` (flow variables, incl. upstream node outputs). `record` and bare field names are rejected before evaluation — on this platform `record` always means "the record at event time", which is ambiguous at an approval node — with error messages that prescribe the correct spelling. The optional `resolveAs: 'user' | 'department' | 'position' | 'team'` re-expands each resolved id through the same graph lookups the static types use; with `behavior: 'per_group'` each intermediate value (e.g. each returned department) forms its own sign-off group. A missing key fails the node loudly; only a present-but-empty result counts as an empty slate.

  **`onEmptyApprovers` policy.** What an empty resolved slate does, node-level, for all approver types: `admin_rescue` (default — request opens for privileged takeover, the #3424 behaviour), `fail` (node fails), or `auto_approve` (skip the request, continue down the `approve` edge with `output.autoApproved = true`). To support auto-approve, the automation engine now honours `NodeExecutionResult.branchLabel` on the synchronous completion path — the field existed but was only ever consumed via resume signals.

  **Decision outputs.** `decide(..., { outputs })` hands structured data from the approver to the flow: the author declares allowed keys on the node (`decisionOutputs`), approvers fill values only, and accepted outputs resume the run as `<nodeId>.<key>` variables — a later approval node's expression can read `vars.<nodeId>.picked_departments`, closing "the previous approver picks the next step's approvers" without a record-field detour. Undeclared keys reject the decision; `decision`/`requestId` are reserved. Multi-approver tallies now always pin to the open-time approver snapshot (previously unanimous re-resolved at each decision against the payload snapshot).

  Also: `collectCelRootIdentifiers` is exported from `@objectstack/formula` (shared by the new `os lint` rules and the runtime pre-check, so they can never drift), resolution inputs are audited on the request snapshot as `__resolvedFrom`, and three new lint rules gate expressions, empty-slate policies and reserved output keys at author time.

- 58f3220: 新增规范 parse-to-AST 入口 `parseCelToAst(source)`,并 re-export AST 节点类型 `CelAstNode`(#4812)。

  `parseCelToAst` 与 `compile` / `evaluate` / `collectCelRootIdentifiers` 共用同一条前端链路
  ——#3306 的 `rewriteNullableTernary` 重写、`DEFAULT_LIMITS` 边界、以及注册了 stdlib 的
  `unlistedVariablesAreDyn: true` 环境 —— 因此全仓对「什么能解析」只有一个答案。此前消费方
  若自建 `new Environment(...)`,拿到的是一份**不带 limits** 的答案:它会解析、并进而推理
  `compile()` 直接拒绝的表达式。

  `parseCelToAst` 只做 parse,不做 check(后者是 `compile()` 的职责):解析成功但类型检查失败的
  表达式(大量 `dyn` 操作数的谓词即是)仍然会拿到 AST。解析失败返回 `null` 而不抛错。

  `CelAstNode` 的 re-export 补上了一个既有缺口:`lowerCelAst` 一直接收 cel-js 的 `ASTNode`,
  而该类型从未导出,消费方只能越过本包直接依赖 `@marcbachmann/cel-js` —— 这正是第二个解析入口
  的成因。

- 3f8817a: feat(spec,drivers,objectql,analytics,formula): `$icontains` reaches every JS evaluation face (#6520)

  The other half of #5702. That change implemented `$icontains` on the SQL family
  and correctly left the spec's `FILTER_OPERATORS` alone; this one adds the
  operator to that array and gives every remaining evaluation face an arm, in ONE
  change, because those two steps cannot be separated.

  **Why one PR.** `FILTER_OPERATORS` is not a word list, it is a runtime allowlist:
  `driver-memory`'s shape gate derives from it, and its matcher's `default:` arm
  assumes the gate already refused anything unimplemented. Measured on a branch
  that added the name early (#5701): the gate stopped refusing, the matcher fell
  through, and `match({ name: 'zzz' }, { name: { $icontains: 'acme' } })` returned
  `true` — the predicate silently dropped, every row matched. A dropped predicate
  does not narrow a query, it WIDENS it, and on an RLS read scope that is a
  permission bypass rather than a degraded feature (#3948). So the word list
  travels with the evaluators or not at all.

  **What now answers it**, all folding the same domain: `driver-memory` (query
  path, reference matcher, and the analytics/cube face), `driver-mongodb`,
  `objectql`'s `having`, `@objectstack/formula`'s `matchesFilterCondition` (the RLS
  write-side `check`), and `service-analytics`' three SQL compilers (the RLS
  lowering, the native-SQL strategy, and the `/analytics/sql` echo).

  **The fold is ASCII-only, and that is the contract, not an implementation
  detail** (#4706 Q1 = A). `$icontains: 'café'` does not match `CAFÉ`. Every face
  reads one shared definition — `foldAsciiCase` /
  `asciiCaseInsensitiveContains` / `asciiCaseInsensitiveRegexSource`, new exports
  on `@objectstack/spec/data` — because the two obvious per-package spellings are
  both wrong in the same direction: `toLowerCase()` folds the whole Unicode range,
  and so does a `RegExp` built with the `i` flag. SQLite folds ASCII only and three
  of the five drivers are SQLite underneath, so a Unicode fold on a JS face would
  re-open exactly the divergence the ruling closed. The pattern-binding faces
  (mingo, mongo) therefore emit one `[Aa]` character class per ASCII letter and
  pass NO flags; mongo's `$icontains` is the one arm in its family that does not
  set `$options: 'i'`.

  The comparand keeps the rules its SQL twin has: matched LITERALLY (`%`, `_` and
  regex metacharacters are ordinary characters), and refused when empty or
  non-string — an empty comparand matches every row, which is a predicate that
  constrains nothing.

  **User-visible effect.** A filter using `$icontains` now behaves the same on the
  in-memory double and on SQL, so an app whose tests run on one and whose
  production runs the other stops getting two answers from one filter. Downstream,
  #5814 (better-auth `Where.mode: 'insensitive'`) no longer hits a 400 on the
  memory double.

  Not changed, and still tracked: the `$contains` family still folds Unicode on
  `driver-memory`'s query path and `driver-mongodb` (#6682) — both remain DEBT rows
  in `scripts/check-driver-conformance.mjs`, now naming one open requirement each
  instead of two. `formula`'s unknown-operator posture stays a silent, fail-closed
  `false` (it governs a write-side check, where an unevaluable condition denies
  rather than widens); the decision and its limits are documented on
  `matches-filter.ts`, and no operator the spec DECLARES is answered that way any
  more.

- d063a96: fix(spec,drivers,formula,client): `like`/`ilike` stop being folded onto `$contains` at the wire (#7536)

  A `like` predicate that arrived over HTTP was rewritten into a substring search
  before any driver saw it, because `AST_OPERATOR_MAP` (`data/filter.zod.ts`)
  carried `'like': '$contains'`. `$contains` LIKE-escapes its comparand and wraps
  it in `%…%`, which breaks a `like` in **both** directions at once. Measured in
  QA run #7463 against showcase on SQLite:

  | filter                          | before                                                                              | now                               |
  | ------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------- |
  | `["name","like","%Industries"]` | `200`, **0 rows** — the `%` bound as a literal percent sign                         | the rows ENDING WITH `Industries` |
  | `["name","like","Industries"]`  | a substring match, **byte-identical to the `$contains` control**                    | an EXACT match                    |
  | `["name","ilike","…"]`          | `400` — `ilike` had no lowering at all, so `isFilterAST()` refused the whole filter | the case-insensitive twin         |

  The second row is the tell: `like` and `$contains` producing the same bytes
  means `like` was not reaching the driver as a pattern at all.

  The file already documented the contract being violated. `canonicalAstOperator`,
  thirty lines below the map entry, carried a hand-written exemption for
  `like`/`ilike` whose comment read: _"they are NOT substring matches at the
  driver: driver-sql passes them to SQL verbatim, so the caller binds the
  wildcards. Folding them onto `contains` would silently wrap the value in `%…%`
  and change what the query means."_ That exemption only ever shaped its own
  output; the lowering the wire path takes had none. A consequence worth naming:
  driver-sql's `like`/`ilike` handling has been unreachable from the wire since
  #5158.

  ## What changed

  **New operators `$like` / `$ilike`** on `StringOperatorSchema` and
  `FieldOperatorsSchema`. The comparand IS the pattern: `%` matches any sequence,
  `_` matches exactly one character, a backslash escapes either, and the pattern
  must cover the WHOLE value — so a pattern with no wildcards is an exact
  comparison, not a substring search. `$like` is case-SENSITIVE (the #4706 Q2 = A
  contract its `$contains` sibling answers); `$ilike` folds ASCII case and nothing
  else (Q1 = A), so `café` does not match `CAFÉ`.

  `AST_OPERATOR_MAP` now lowers `like` → `$like` and `ilike` → `$ilike`. `ilike`
  enters the AST vocabulary for the first time — it previously had no entry, so
  `isFilterAST()` refused it. `canonicalAstOperator`'s hand-written exemption is
  retired: the generic round-trip answers `like`/`ilike` by construction now, so
  the special case is gone along with the reason it existed.

  The pattern language is defined **once**, in the spec, and shared by every face
  that needs it — `hasDanglingLikeEscape`, `likePatternToRegexSource`,
  `matchesLikePattern` and `likePatternToGlobPattern`. Six faces implementing one
  pattern language separately is the `#3948` shape reached through translation
  instead of vocabulary.

  **Which backends answer, and which refuse.** `$like`/`$ilike` are deliberately
  NOT in `FILTER_OPERATORS`, the runtime allowlist several packages derive
  acceptance from — adding a name there before every face has an arm turns a loud
  refusal into a silently DROPPED predicate, which is the widening measured in
  #5701 and ruled on in #3948.

  | face                                                                 | `$like` / `$ilike`                                                                                  |
  | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
  | `driver-sql` (and `driver-sqlite-wasm`, which inherits its compiler) | **answers** — `LIKE` on Postgres/MySQL, `GLOB` on SQLite                                            |
  | `driver-turso`, both transports                                      | **answers** — the remote transport compiles independently, holds to the local one by a parity suite |
  | `driver-memory`, both faces                                          | **answers** — the in-memory double must not 400 for a filter that works in production               |
  | `@objectstack/formula` (`matchesFilterCondition`)                    | **answers** — so a write-side RLS `check` agrees with the read-side SQL                             |
  | `driver-mongodb`, objectql `having`, `service-analytics`             | **refuse**, loudly, in the ADR-0112 `INVALID_FILTER` envelope                                       |

  The refusals are the point rather than a gap: #7536 exists because a `like` was
  silently given `$contains`' meaning, and a face that quietly answers a different
  question is worse than one that refuses. Clearing the remainder means arms on
  those faces in one PR — the #6520 direction.

  **Why SQLite gets `GLOB`.** `$like` is case-exact and SQLite's `LIKE` folds
  ASCII unconditionally, which cannot be switched off per statement
  (`PRAGMA case_sensitive_like` is connection-global). That is #6518's finding,
  and the operator it landed on. Because GLOB speaks a different pattern language
  (`*`/`?`, and `%`/`_` are ordinary characters), the pattern is TRANSLATED rather
  than escaped — including GLOB's own metacharacters, which are ordinary to LIKE:
  an unescaped `*` in a GLOB pattern is the same filter bypass an unescaped `%` is
  under LIKE (#5567).

  **Refused rather than given a meaning:** a pattern ending in a lone unpaired
  backslash. No reading survives every backend — Postgres rejects such a pattern
  outright, GLOB has no escape character at all — so it is refused at the door on
  every face, by one shared test.

  ## ⚠️ Behaviour changes

  1. **`like` now means `LIKE`.** If you were relying on `like` behaving as a
     substring search — the defect — write `contains` instead. A wildcard-free
     `like` is now an exact match.
  2. **`like`/`ilike` on `driver-mongodb`, objectql `having` and analytics now
     return `400 INVALID_FILTER`** where a (wrong) substring answer came back
     before. Write `$contains`/`$icontains` on those backends. `driver-memory` is
     deliberately NOT in that list — it implements the operators, because an
     application whose tests run on the in-memory double and whose production runs
     SQL must not meet a 400 in test for a filter that works in production.
  3. **`@objectstack/client`'s `.contains()`, `.startsWith()` and `.endsWith()`
     emit different operators.** They used to build a `like` tuple by gluing
     wildcards onto the caller's value (`[field, 'like', '%' + value + '%']`),
     which was wrong twice over: the wire folded `like` onto `$contains`, which
     escaped the glued `%` back into a literal, so `.contains('name','Corp')`
     searched for the text `%Corp%` and matched only rows containing percent
     signs. And once `like` reaches the driver as a real pattern, the glue becomes
     the _other_ bug — a `%` or `_` inside the caller's own value would silently
     become a wildcard. They now emit `contains` / `starts_with` / `ends_with`,
     whose comparand is text. `.like()` is unchanged and finally works; `.ilike()`
     is new.

     Note the case semantics this corrects on paper too: `.contains()`'s docblock
     claimed "case-insensitive", but the `$contains` family is case-SENSITIVE by
     contract (#4706 Q2 = A). Use `.ilike()` for a case-insensitive pattern.

- 6965160: feat(lint): view/page 可见性谓词的裸标识符构建期闸门 —— 坏谓词发不出去(#6128)

  新增 **error 级** 规则 `visibility-bare-identifier`:view/page 的可见性谓词
  (`visibleWhen` 及其两个已弃用别名 `visibleOn` / `visibility`)里引用了任何绑定根都解析不到的
  顶层标识符时,`os validate` / `os build` / `os lint` 一律拒收。写成 `status == 'active'`
  而不是 `record.status == 'active'` 的谓词,从此发不出去。

  按 #5149 维护者 2026-08-06 裁决的构建期半边落地(运行时 warn-once 半边已由 objectui#3541 合入)。
  本仓传统的准确表述是:fail-open 或 fail-closed 都可以裁,**静默不可以**。谓词失败仍然 fail-open
  (已发货 app 行为不变),但坏谓词不再能进入产物。

  **为什么现有两道闸都放行**(#5149 Repro 1 实测,已写进规则注释,防后人误并):
  ADR-0032 的标识符闸(`validate-expressions.ts`)解析 record 作用域的裸引用,但它的遍历只覆盖
  objects / flows / actions / sharingRules / hooks,**从不走 views 与 pages**;ADR-0089 D3b
  只判**有根**的谓词根错层(runtime 面的 `data.`、metadata 面的 `record.`),**无根**的谓词两边都不匹配。
  两闸之间正好漏掉「作者按文档示例写了裸字段名 → 谓词永远解析失败 → 控制台 fail-open 静默显示」。

  **判定由两个既有 oracle 合成,本包不自建 CEL 环境**(#4812 的教训):声明性判定取
  `@objectstack/formula` 的 `firstUndeclaredReference`(即 `validateExpression` 给 record 作用域
  裸引用定罪的同一个严格环境),AST 取规范入口 `parseCelToAst`。AST 先收集所有处于**接收者位置**
  的标识符(`a.b` / `a?.b` / `a['b']` / `a.exists(…)`)并在检查前声明它们,于是只剩「当作裸值引用」
  的标识符会被判 —— 未知**根**(`my_record.x`)交还给 ADR-0089 D3b,不在本规则射程内。

  **与 #4953(全量 vs 稀疏绑定)的边界**:#4953 实测同一求值器在两种绑定下语义相反
  (`has(record.a)` 全量 true / 稀疏 false;`record.a != null` 全量 false / 稀疏 FAULT)。本规则
  **按构造与该分叉无关** —— 它从不追问某个 KEY 在已绑定的根上是否存在,只追问标识符有没有根,
  而无根标识符在两种绑定下都解析不到。`has(record.x)` / `record.x != null` 等守卫写法在本闸门下
  一律绿,无论 #4953 最终怎么裁;已加测试钉住这条边界。

  **遍历按实测修正,否则规则生来即死**:`os build` 跑 `examples/app-showcase` 得到的唯一一条
  view 表单谓词落在 `views[0].formViews.edit.sections[0].fields[6].visibleWhen` —— 运行时 app 形状下
  `views[]` 条目是**视图容器**(`ViewSchema` 声明的自有键就是 `list` / `form` / `listViews` /
  `formViews`),`sections` 在下一层。原遍历只读 `views[].sections`,在这份 stack 上报告「干净」。
  现在覆盖容器的 `form` 与每个 `formViews.<key>`,以及仍然直接携带 `sections` 的 `defineForm` 形状;
  pages 改走共享的 `walkPageComponents`(regions、slotted 页的 `slots`、以及 `properties` 里的
  `page:tabs` / `page:accordion` / `page:card` 子树都随之覆盖,source-authored 页按其既有语义跳过)。
  `objects[].views` 明确不读 —— 该键已被 schema 立碑拒绝,读它只会造出一条永不触发的幽灵检查。
  两条既有 ADR-0089 D3b advisory 随遍历一并变得真正可达。

  注册表 tier `advisory` → `gating`(#5762 的先例):tier 声明并非自述,
  `authoring-rule-wiring.test.ts` 会读规则源码核对。

  已知盲点(已钉测试、方向安全):字段名与 CEL **类型名**相同时(`type` / `int` / `string` / `list`
  / `map` / `timestamp` …)不判 —— CEL 自身声明这些标识符,`type == 'grid'` 到检查器那里是类型
  overload 错误而非未知变量;改读 overload 消息会误杀合法的 `type(record.x) == string`。语法不通过
  的谓词同样不判,交还给拥有该判定的闸门。两者都是漏判,永远不会变成误红。

  仓内 `app-todo` / `app-crm` / `app-showcase` 三个示例 `os validate` 全部通过、零 visibility finding,
  无需修改任何示例内容。

  `@objectstack/formula` 侧:公开导出 `firstUndeclaredReference`(理由与既有的
  `collectCelRootIdentifiers` 一致 —— 绑定根集合不同的消费方需要的是同一个答案,替代方案是在消费方
  自建严格 `Environment`,而那正是 #4812 从本包消费方手里拿掉的私有前端)。

- bf1edef: feat(formula,lint): wire ADR-0056 D4's RLS authoring gate, from the runtime's own predicate (#4983)

  `isSupportedRlsExpression` has carried the same docblock since ADR-0056 D4:
  "exposed so an authoring-time gate (`objectstack compile`) can REJECT a
  predicate the runtime would silently drop … A `false` here means 'this
  predicate will never enforce'." It had **no non-test consumer anywhere** — the
  function written to fix declared-but-never-read was itself declared and never
  read. This lands the consumer, in two steps that had to happen in this order.

  **1. `sqlPredicateToCel` and `isSupportedRlsExpression` move FROM
  `@objectstack/plugin-security` (`src/rls-compiler.ts`) TO `@objectstack/formula`
  (`src/rls-predicate.ts`), and are exported from its root.** Executable code
  unchanged — a change of address, not of behaviour; `plugin-security` now imports
  them from `@objectstack/formula` and keeps no copy, so there is still exactly
  one definition. No import path outside the two packages changes: neither symbol
  was ever exported from `@objectstack/plugin-security`'s entry point. The move is
  what makes step 2 possible at all — `@objectstack/lint` may depend on
  `@objectstack/spec` and never on a runtime, so with the predicate living in a
  runtime the gate's only other door was copying the SQL→CEL bridge, whose
  boundary conditions (quoted literals are never rewritten; canonical CEL passes
  through unchanged) _are_ the gate's red/green line. A fork drifting by one
  character rejects policies the runtime executes correctly — the false-positive
  direction, which is worse than the gap. ADR-0058 D1 asks for a single canonical
  shape gate; the bridge is part of that gate.

  **2. New `@objectstack/lint` rule `validateRlsPredicateEnforceability`,
  `error`, on all three authoring commands**, over
  `permissions[].rowLevelSecurity[].using` and `.check`:

  - **`rls-predicate-unenforceable`** — parses as CEL, outside the pushdown
    subset: a function call (`size(...)`, `has(...)`), arithmetic, a ternary, a
    cross-object path (`record.account.region`).
  - **`rls-predicate-unparseable`** — does not parse as CEL even after the legacy
    SQL bridge (`=` → `==`, `IN` → `in`): SQL `AND` / `OR` / `LIKE`, a subquery.
    Its own id because the fix is different — write CEL (`&&`, `||`), not a
    different shape.

  What the gate prevents, measured through `plugin-security` rather than inferred:
  `RLSCompiler` drops the policy and logs one request-time WARN. On the read path,
  when it is the only applicable policy, `compileFilter` returns the
  `RLS_DENY_FILTER` sentinel instead, which is AND-ed onto the where clause — so
  every select / update / delete on the object matches **zero rows**. On the
  ADR-0058 D4 write path the post-image `check` becomes that same sentinel, which
  no record satisfies, so every insert / update fails with `PermissionDeniedError`.
  The runtime fails closed, which is why this was survivable: the result is not a
  hole but a policy that reads as an authorization and behaves as a blanket
  refusal, with nothing at authoring time pointing at the line that caused it.

  Fix a flagged predicate by rewriting it inside the lowerable subset — `==` `!=`
  `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and
  `startsWith` / `endsWith` / `contains` over single-column field paths (ADR-0058
  D2), against a literal or a `current_user.*` value. Two specific migrations:
  `has(x)` / `size(x) > 0` → `x != null` (a function call is correct in an object
  _validation_ rule, which is interpreted, and wrong here, where the predicate is
  compiled to a filter); and a related record's field → denormalise it onto this
  object (formula/rollup) and test that column, since RLS cannot join (ADR-0055).

  Same construction as the sharing-rule gate (#4698): the rule does not model the
  consumer or grep for it — it calls `isSupportedRlsExpression`, the exact
  function `RLSCompiler.compileFilter` consults to decide whether a dropped policy
  earns its warning, so the two verdicts are one boolean by construction, pinned
  in both directions over a shared corpus. Measured before shipping: every RLS
  predicate declared anywhere in this repo — the `plugin-security` platform seeds,
  the examples, the dogfood fixtures, the authoring skill — is supported, so the
  gate turns nothing red that works today. Unlike the sharing-rule gate, CEL
  _syntax_ is reported here rather than deferred to `expression-invalid`:
  `validateStackExpressions` does not walk `rowLevelSecurity` at all, and could not
  judge this field correctly if it did, because `owner_id = current_user.id` is a
  CEL syntax error and a working RLS predicate at the same time.

- 4965bfa: Warn on flow-node `config` keys the node type does not declare (#4045).

  `FlowNodeSchema.config` is `z.record(z.unknown())`, so a misspelled or invented
  config key was accepted in total silence: `visibleIf` instead of `visibleWhen`
  registered cleanly, was never read, and the only symptom was a feature that quietly
  did not happen. That diagnostic vacuum is what made #3528 take three passes and two
  wrong diagnoses to resolve.

  `registerFlow` now compares each node's `config` against its descriptor's
  `configSchema` and warns on anything undeclared, located and with the declared set
  listed:

  ```
  [flow 'lead_conversion'] node 'screen_1' (screen): unknown config key `visibleIf`
    at config.fields[0].visibleIf — It is not declared by this node type's
    configSchema, so nothing reads it. Declared here: name, label, type, required,
    visibleWhen.
  ```

  The walk descends where the schema declares structure and **stops at free-form
  keyValue maps**, whose keys are author data (`filter: { status: 'stale' }`).
  Descending matters: the #3528 typo class lives _inside_ the `screen` field
  repeater, so a top-level-only comparison would miss the exact mistake this exists
  to catch.

  **Warn, never reject.** An undeclared key is an author typo, a key the executor
  genuinely reads that its hand-written `configSchema` never declared (`notify.source`
  was exactly this), or dead config. Only 4 of the 13 schema-carrying builtins have
  been audited for the second population, so hard-failing would gamble on the other
  nine. Tightening to an error is a later, per-key decision once this warning has
  measured the real distribution. Nothing about the published `configSchema` changes,
  so no consumer sees a different shape.

  `@objectstack/formula` now exports `nearestName`, the edit-distance helper already
  used for unknown-field and unknown-role suggestions, so "did you mean?"
  diagnostics share one threshold. It is deliberately a bonus rather than the
  mechanism — `visibleIf` → `visibleWhen` is distance 4 against a threshold of 3, so
  the declared set is always listed instead of only as a fallback.

  Also fixes the first real finding from the new check: `showcase_inquiry_purge`'s
  `get_record` node carried `mode: 'records'`, which no executor reads, with a comment
  crediting it for behaviour that `limit > 1` actually produces.

### Patch Changes

- 2af1988: fix(formula,spec,core): the RLS write-side `check` evaluator honours calendar-day upper bounds (ADR-0053 D-D)

  `@objectstack/formula`'s `matchesFilterCondition` — the evaluator behind RLS
  write-side `check` policies (ADR-0058 D4) — compared a bare `YYYY-MM-DD` `$lte`
  bound literally. On a `datetime` post-image that meant a policy of the shape
  `{ signed_on: { $lte: '{today}' } }` **denied every write made after 00:00**:
  the write-side twin of the read-side data loss #3777 fixed, and the last of the
  platform's filter backends that disagreed about what a bare day means as a
  bound.

  `$lte` and a `$between` max now evaluate half-open against the next calendar
  day, matching the SQL compiler, the memory and mongo drivers, and the analytics
  preview evaluator. Unchanged, per the same semantics table: full-ISO bounds keep
  exact-instant semantics, `$gte`/`$gt`/`$lt` keep their midnight anchoring, and a
  plain `YYYY-MM-DD` value compares identically (string ordering makes the two
  forms equivalent). The evaluator stays fail-closed on a null bound.

  **Where the rule now lives.** `nextUtcCalendarDay` moved from
  `@objectstack/core` to `@objectstack/spec/data` — beside `date-macros.zod.ts`,
  whose vocabulary it interprets. `formula` cannot depend on `core`, and a second
  copy of the rule is exactly the divergence #3777 catalogued; `spec` is the one
  package all six consumers already depend on, so this adds no dependency edge.

  No import changes are required: `@objectstack/core` re-exports the symbol, so
  existing `import { nextUtcCalendarDay } from '@objectstack/core'` keeps working.
  New code should prefer `@objectstack/spec/data`.

- b230e5e: fix(formula): `classifyError` grades a CEL fault by error class + code, never by the message (#6223)

  `EvalResult.error.kind` is author-facing — `@objectstack/objectql`'s `cel-fault`
  puts it in front of the author as `` `${kind}: ${first line}` `` and
  `packages/rest` re-emits it as the HTTP body's `reason`. cel-js embeds the
  author's own **source line** in `message` (`formatErrorWithHighlight`), so a
  classifier that regex-matches that text is matching text the author writes.
  PR #6202 closed the `ParseError` arm this way and left `type` / `runtime` on the
  keyword table pending a per-code audit. This is that audit, and its verdict is
  that the table goes entirely.

  Measured on cel-js 8.0.0 — one `no such overload` **evaluation** fault, four
  field names, three wrong answers:

  ```text
  record.status        > 1  ->  runtime   (right)
  record.parse_status  > 1  ->  parse     (wrong)
  record.syntax_mode   > 1  ->  parse     (wrong)
  record.type_code     > 1  ->  type      (wrong)
  ```

  `parse` is the inverse of the #6133 misdirection: the expression is
  syntactically perfect and failed on the data, and the author was told to go fix
  an expression that has nothing wrong with it.

  `classifyError` now reads only structured contract:

  - `ParseError` -> `bounds` when `code === 'limit_exceeded'`, else `parse`
    (unchanged, from #6202);
  - `EvaluationError` -> `type` for the one declaration-class code
    (`unknown_variable`, the root identifier is not bound in this scope at all),
    else `runtime`;
  - anything that is not a cel-js error -> `runtime`.

  Two findings from the audit worth recording. First, the residual keyword arm was
  **not** dormant: `matches()` is an ObjectStack stdlib binding over `new
RegExp(...)`, so an uncompilable pattern escapes as a native `SyntaxError` whose
  message echoes the pattern — and the pattern can come off the row, not just out
  of the source. `matches(record.name, record.re)` with `re = "type("` was
  graded `type`; with `"Exceeded maxAstNodes("` it was graded `bounds`. A data
  value was picking the error kind. Second, there is deliberately no `TypeError`
  arm: cel-js raises that class only from its non-evaluating `TypeChecker`, which
  runs only inside `Environment#check`, and that method catches it and _returns_
  `{ valid: false, error }`. The check-time `TypeError -> type` mapping already
  lives in `celEngine.compile`, which reads that object.

  Six evaluate-time codes change verdict from `type` to `runtime`
  (`int_conversion_error`, `uint_conversion_error`, `double_conversion_error`,
  `invalid_index_type`, `heterogeneous_list_element`,
  `invalid_comprehension_range`). Each is a fault decided against the row; every
  one of them was graded `type` only because cel-js happens to use the word "type"
  in its prose (`int() type error: cannot convert to int`). Every evaluate-time
  code the engine can reach now has a fixture pinning its `kind`.

- 5d24f4b: fix(formula): the ADR-0032 §1c retry rewrites only the operands that faulted (#7098)

  **A CEL expression could return a silently wrong boolean.** No fault, no log
  line, no failing test — `{ ok: true }` with the wrong answer. If you have
  compound CEL that mixes a numeric comparison with a string equality over
  string-serialized fields, read the "which expressions change answer" list below:
  those expressions answer differently after this fix, and the new answer is the
  right one.

  ## What was wrong

  When a comparison faults on a string-serialized numeric or date field
  (`record.rating >= 4` where `rating` reads back as `"5.0"` — #1530 / #1534),
  ADR-0032 §1c hydrates and retries. The retry hydrated the **entire scope** and
  re-ran the **entire expression**, justified by a docblock claim that it

  > can never change a comparison that already evaluated cleanly — it only rescues
  > one that already faulted.

  That claim was false, and it was load-bearing: it was the stated reason the
  hydration was allowed to be unconditional and scope-wide. The retry knows only
  that the _whole expression_ faulted, not that each sub-comparison did. So:

  ```text
  record.n >= 4 && record.s == "5.0"    with { n: "7", s: "5.0" }
    before -> { ok: true, value: false }        after -> { ok: true, value: true }
  ```

  `record.n >= 4` faults and is correctly rescued. But `record.s` was hydrated to
  the number `5` as well, so the author's deliberate string equality — **true**
  when it was evaluated the first time — became `5 == "5.0"`, which CEL answers
  `false` across types. The expression returned `false`, and nothing reported that
  a clean answer had been overruled.

  ## Which expressions change answer

  Only expressions that **already reached the §1c retry** — i.e. some operand
  faulted `no such overload`. Everything that evaluates without faulting is
  untouched. Within that set, an expression changes answer when it also contains:

  - **a string equality / inequality on a numeric-looking or ISO-date field** —
    `record.n >= 4 && record.s == "5.0"`, and the `!=` and ternary forms. Now
    answers on the string the author wrote.
  - **a string membership test** — `record.s in ["5.0", "x"]`.
  - **the same field compared as a number in one place and as a string in
    another** — `record.n >= 4 && record.n == "7"`. Both answers are now correct
    at once; previously the second was collateral damage from the first.
  - **a numeric-looking string the expression RETURNS rather than compares** —
    `record.n >= 4 ? record.s : "none"` returned the number `5`; it now returns
    the string `"5.0"`. A `Field.formula` of type text was storing a different
    value than the record held.

  One class becomes a **loud fault where it used to be silently rescued**: an
  operand whose value the rewrite cannot read before deciding — bound by a
  comprehension (`record.items.exists(i, i.price > 100)`), or behind a computed
  index. That is the deliberate trade of this fix. Rescuing an operand we cannot
  prove faulted is exactly the defect being closed, so those report the original
  `no such overload` instead of guessing. The reported error is unchanged in shape
  and message.

  ## What replaces it

  The coercion is now **per operand position** — the same discipline
  `rewriteTemporalEquality` already documents ("no field-wide trade-off"), one
  step stricter. The scope is never rewritten; the faulting operand is wrapped in
  `double(…)` or `date(…)` in place. An operand is rewritten only when all three
  hold, which makes the docblock's guarantee true by construction rather than by
  assertion:

  1. the operator **raises** on a string-versus-number/Timestamp pair instead of
     answering one, so the comparison cannot have produced an answer;
  2. the counterpart is a number or a Timestamp **in this scope**, read off the
     values in hand rather than off a static type (every field is `dyn` under
     `unlistedVariablesAreDyn`);
  3. the operand's own value is a §1c serialization artifact — an entirely-numeric
     string or an ISO-8601 date. A zip like `"02134"`, or free text, still faults
     loudly.

  Measured per operator on cel-js 8.0.0 and pinned in the new tests: `<` `<=` `>`
  `>=` `+` `-` `*` `/` `%` **fault** on a mixed pair and are eligible. `==`, `!=`
  and `in` **answer** across types — CEL equality is total — so they already had
  an answer and are never rewritten. That measurement is the root of the defect:
  the string equality above never faulted at all.

  `Field.date` strings not matching a Timestamp under `==` remains owned by
  `rewriteTemporalEquality`, which wraps them statically on the clean path, where
  both sides are known from the source instead of inferred from an unrelated
  conjunct's fault.

  ## Reach

  `celEngine.evaluate` — the only home of this retry — does **not** reach RLS.
  Row-level security compiles its `using` / `check` predicates through
  `compileCelToFilter` (SQL pushdown) and `matchesFilterCondition` (write-side
  post-image), and declared sharing rules do the same; neither calls this
  evaluator. No access-control decision could be inverted by this.

  It does reach write-gating decisions, which is why the behaviour was not
  acceptable as documented: validation-rule predicates and `when` conditionals,
  `readonlyWhen`, hook `condition`s, automation/flow conditions, and formula
  fields and default values. A validation rule is **fail-closed** on a fault
  (#4649) — but a silently flipped boolean is not a fault, so a rule that should
  have rejected a write instead read as "not violated" and let it through.

- 29b94ed: fix(formula): the CEL hydration retry arms off cel-js's structured code, not the phrase "no such overload" (#6679)

  `celEngine.evaluate` catches a fault and asks `isNumericOverloadError` whether to
  hydrate string-serialized numeric / date fields and re-evaluate once — the
  ADR-0032 §1c accommodation for `Field.rating` → `"5.0"` and `Field.date` →
  `"2026-06-20"` (#1530, #1534). That question was answered by
  `/no such overload/i.test(err.message)`: the last message-text read in
  `cel-engine.ts` that armed behaviour after #6223 / PR #6677 closed the same hole
  in `classifyError`. It now reads
  `err instanceof EvaluationError && err.code === 'no_such_overload'`, the same
  class-and-code rule `classifyCelFault` already follows one function below.

  The phrase was reachable from a **native** throw, not only from cel-js. Our
  `matches()` stdlib binding is `new RegExp(String(re)).test(...)`, so an
  uncompilable pattern escapes cel-js unwrapped as a `SyntaxError` echoing the
  pattern verbatim — `Invalid regular expression: /no such overload(/` — which
  matched. The pattern can be written in the source or read off a row via
  `matches(record.name, record.re)`.

  The filing recorded this as observation-class, expecting the consequence to be
  nil because the retry re-throws the original error. Measuring it for the fix
  found one case where it is not nil, so this ships as a fix rather than a
  tolerance removal: when hydration lets the expression short-circuit around the
  throwing call, the spurious retry **succeeds** and returns a value where the
  fault was the right answer.

  ```text
  record.s == "5.0" ? matches(record.name, "no such overload(") : false
    { s: "5.0", name: "x" }   ->  was: ok, false        now: the regex fault
  record.s == "5.0" ? matches(record.name, "(") : false
    { s: "5.0", name: "x" }   ->  the regex fault       (unchanged)
  ```

  Evaluation 1 takes the `matches(...)` branch and throws natively; the phrase
  armed the retry; hydration made `record.s` the number `5`, so `5 == "5.0"` went
  false, the ternary took the other branch, and `matches` was never called. Two
  expressions that differ only in whether a regex literal happens to contain the
  phrase no longer disagree about whether they fault.

  The behaviour change is one-directional and narrow. A genuine cel-js
  `no_such_overload` still arms the retry and every §1c hydration behaves exactly
  as before; only a native throw whose message merely contains the phrase stops
  arming it. Faults are otherwise unchanged — a native throw carries no cel-js
  contract, so it is still reported as `runtime` (#6223).

- 07c68b0: fix(formula): 括号/引号/转义等 parse 期错误不再被误报为 `runtime`

  `celEngine` 的错误分类此前完全靠**错误文案关键词**判定,而 cel-js 8.0.0 的 parse 期错误有约 19 种措辞,只有 3 种含 `parse` / `unexpected` / `syntax`。其余整类 —— 最典型的括号/方括号/花括号不配对(`Expected RPAREN, got EOF`)、未闭合字符串、非法转义、保留字 —— 全部落到默认值 `runtime`。

  `kind` 不是内部字段:它被原样拼进作者可见的写入拒绝文案(`@objectstack/objectql` 的 `rule-validator` / `cel-fault`)与 REST 错误响应体的 `reason`。少写一个右括号的校验规则,作者读到的是 `(runtime: …)` —— 指向数据与求值期,而真正该改的是表达式本身,与 ADR-0032 D1d 的"消息面向自纠"相悖。

  改为按 cel-js 抛出的**错误类**判定:`ParseError` → `parse`(其中 `code: 'limit_exceeded'` 仍 → `bounds`,cel-js 的越界一律由 parser 抛出)。这一层不再读文案,因此也修掉了关键词方案无法修的一格:cel-js 会把**作者自己的源码行**嵌进 `message`(`formatErrorWithHighlight`),于是字段名能决定错误分类 —— 实测 `((record.type_id)` 这条普通的括号不配对,此前被判为 `type`,只因回显的源码里含子串 "type"。

  `type` / `runtime` 两支暂仍走原关键词表:cel-js 的 `TypeChecker` 按**阶段**而非按故障选择错误类(`isEvaluating ? evaluationError : typeError`),同一个 `unknown_variable` 在 check 期是 `TypeError`、在 eval 期是 `EvaluationError`,整体结构化会改变这些既有判定。审计见 #6133。

  kind 词表本身(`parse` / `type` / `runtime` / `bounds` / `dialect`)未变,消费方未改。

- 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)

  The AGENTS.md post-task checklist requires breaking changesets to carry their
  FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
  inside the npm package and is what an upgrading agent greps after the tombstone
  error." That delivery path was severed for 68 of the 69 publishable packages:
  npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
  older npm versions — not `CHANGELOG.md`, and the canonical
  `"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
  10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
  70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
  explicitly.

  The tombstone-error scenario is precisely the one where the repo is out of
  reach — the upgrading agent has `node_modules` and nothing else — so the
  migration text has to ride in the tarball. Every publishable package now
  declares `CHANGELOG.md` in `files`, and the canonical whitelist is
  `["dist", "README.md", "CHANGELOG.md"]`.

  The other half is the gate: `check:published-files` gains a fifth invariant,
  COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
  always-required lint job, so the next package cannot silently sever the path
  again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
  into the canonical set.

  Consumer-visible change: one more file per install (the package's changelog,
  e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
  promised.

- e9b5265: fix(formula,lint): `current_user` becomes a declared root, and its field-level rejection becomes a real rule (#6290)

  `@objectstack/formula` told two stories about one root. `introspectScope` handed
  `current_user` to authors as a legal namespace and `checkRoleCatalog`'s four
  position-membership regexes all lead with it — both correct, because ADR-0068 D1
  makes `current_user` THE canonical spelling and `buildScope` really does mount
  the same `EvalUser` under it. Only `cel-engine.ts`'s `SCOPE_ROOTS` disagreed, so
  the strict environment read the blessed spelling as a BARE FIELD REFERENCE while
  its two aliases (`user`, `ctx`) passed unremarked.

  Three things change.

  **1. `SCOPE_ROOTS` declares `current_user`.** That list is a "never faults"
  baseline, not a per-surface contract, and it now advertises exactly what the
  package advertises elsewhere. A new pin asserts the property directly: every
  root `introspectScope` reports must resolve in the strict env.

  **2. The wrong prescription is gone.** Because the rejection used to fall out of
  the baseline's omission, the author got the GENERIC bare-field diagnostic —
  "Write `record.current_user`". That shape binds on no layer of the platform, so
  an author who followed the message ended up with something strictly worse than
  what they started with, still silent. The field-level verdict now comes from a
  rule of its own in `@objectstack/lint`, which names the real failure (unbound ⇒
  fault ⇒ visibility falls back to `true` ⇒ the field a `current_user` test was
  meant to hide stays visible for everyone, #6146) and prescribes surfaces that
  exist: move the predicate to the option's own `visibleWhen`, declare field-level
  security on a permission set (`fields: { '<object>.<field>': { readable: false } }`),
  or rewrite it against `record`. It covers `visibleWhen`, `readonlyWhen` and
  `requiredWhen`, which share the one evaluator.

  **3. Per-option `visibleWhen` is validated at all.** `validate-expressions.ts`
  walked field-level conditional rules and stopped there, so `SelectOption.visibleWhen`
  — an authorable CEL slot the client filters on AND the server enforces — reached
  compile, validate and run time checked by nobody. A bare field reference, a
  reference to a field that does not exist, a syntax error or a template-dialect
  predicate in an option all shipped in silence, and the option simply never
  offered itself. Options are now walked, located by option value, on the same
  `record` scope as their host field.

  The two surfaces deliberately give opposite verdicts on `current_user`, because
  their evaluators differ: field-level rules go through `evalFieldPredicate`
  (`record` + `previous` + `parent`, never a user), options through
  `resolveCascadingOptions` against the host's predicate scope, which does bind it
  (ADR-0068 / objectui#2284). The showcase's role-gated option
  (`'admin' in current_user.positions`) had never met this rule before and is now
  pinned as the legal usage it is.

  Sweep: `objectstack validate` is clean on all three example apps
  (`app-showcase`, `app-crm`, `app-todo`) with the option walk active — zero new
  findings, including the showcase object that carries both a record-scoped
  cascade and the role-gated option.

- d5e9f6e: 字段级 `*When` 的未绑定根检查:黑名单翻成白名单,并把因果句按槽位分档

  同一段诊断上的两条**正交**分档轴,一次设计通过 —— 分开做会把这段文案写两遍,
  且第二遍推翻第一遍。

  ## 轴一:根集合从 3 项黑名单翻成 3 项白名单(#6713)

  字段级 `visibleWhen` / `readonlyWhen` / `requiredWhen` 实测只绑 `record`、
  `previous`、`parent` 三个根,三处独立证据一致:服务端
  `rule-validator.ts` 的两处绑定(`readonlyWhen` 绑
  `{ record, previous, extra: { parent } }`,`requiredWhen` 绑
  `{ record, previous, ...parentScope }`);客户端 `evalFieldPredicate` 绑
  `record` + `previous` + 调用方 `scope`,而 objectui 全部五个字段级调用点
  (`form.tsx` ×3、`WizardForm.tsx`、`GridField.tsx`)传的 `scope` 只可能是
  `undefined` 或 `{ parent }`;作者端 objectui 的
  `FIELD_RULE_ROOTS = ['record', 'previous', 'parent']`,注释明写 "nothing else"。

  而检查此前是一张**黑名单** —— #6584 一项、#6711 三项
  (`current_user` / `user` / `ctx`)。黑名单在这个面上结构性地追不上
  `SCOPE_ROOTS`:每新增一个根都要有人记得抄过来(`current_user` 自己就是 #6290
  加进去、#6584 才被发现的)。实测有 **21 个根**落在这条缝里,它们同样未绑定、
  同样 fault、而且同样**静默** —— 都在 `SCOPE_ROOTS` 里,所以裸引用检查也从不
  报它们。其中两个是高可信度的作者笔误而非理论成员:

  - `os.user.id` —— ADR-0068 D1 的**第四种**用户拼写(`buildScope` 把同一个
    `EvalUser` 挂在 `current_user` / `user` / `ctx.user` / `os.user` 下),#6711
    收了三种,`os` 这一支没收;
  - `data.status == 'x'` —— `data` 是**元数据表单**里同一个 `visibleWhen` 键的
    **合法**根(`view.zod.ts`:"Root: `record` … in runtime forms, or `data` in
    metadata forms"),两种表单同一个键名、不同的根。

  判定改为 `SCOPE_ROOTS` 成员减去白名单,列表直接从 `@objectstack/formula` 取,
  不在消费端重述 —— 因此 `SCOPE_ROOTS` 将来新增的成员自动被覆盖。

  处方随之**按根分档**:用户根(`current_user` / `user` / `ctx` / `os`)保留原有
  的选项级 `visibleWhen` 与权限集 FLS 两条用户向处方;`data` 给出元数据表单 vs
  运行期表单的解释;其余根给出通用的「改写成 `record` 谓词」。此前只有用户向处方,
  对写了 `data.type == 'select'` 的作者是答非所问。

  ## 轴二:因果句按槽位分档(#6716)

  三个槽位此前共用一句「falls back to VISIBLE … showing for everyone」,而这句话
  只对其中一个精确。三格全部**实测**,每格量了两端:

  - **`visibleWhen` —— 仅客户端、fail-OPEN,原文案正确。** 服务端根本不评估字段级
    `visibleWhen`(`ConditionalFieldDef` 无此成员,`fieldsNeedPrior` 只看
    `requiredWhen || readonlyWhen ||` 选项可见性),唯一裁决来自渲染端,
    `resolveFieldRuleState` 对可见性传 `fallback: true`。
  - **`readonlyWhen` —— 两端方向相反,服务端说了算,原文案是反的。** 服务端
    `isReadonlyWhenLocked` 命中 `unknownVariableOf` 后返回 `true`(#4889 的
    carve-out,其触发条件正是未绑定根这一类),`stripReadonlyWhenFields` 随即把该
    字段从 payload 中删除;客户端 `resolveFieldRuleState` 传 `fallback: false`,
    表单仍渲染为可编辑。按 ADR-0057 D10(server enforces, client is courtesy)以
    服务端为准:作者改了字段、保存报成功、值静默不落库。原文案告诉作者「对所有人
    可见」—— 失败方向与排障方向都相反。
  - **`requiredWhen` —— 两端都 fail-OPEN,且与可见性无关。** 服务端记日志后
    `continue`(#4977 明确没有采用 #4889 的 carve-out),客户端 `fallback: false`,
    两端都不强制,记录带着空字段保存成功。原文案在这里不只是不精确,而是说错了
    字段的哪个属性。

  `conditionalRequired` 在 `FieldSchema` 里是 `retiredKey`(按名字拒绝),解析后的
  编译路径上该分支是惰性的,因此给它一条与槽位无关的通用句,而不是编造第四格测量。

  ## `@objectstack/formula`

  `SCOPE_ROOTS` 改为公开导出。一个绑定**封闭**根集合的面,必须能说出它**不**绑定
  的那些根,而那个补集就是 `SCOPE_ROOTS` 减去该面自己的白名单;消费端手抄的列表
  追不上这张表。注意它不能用 `firstUndeclaredReference` 替代:严格环境同时声明了
  CEL 的**类型名**,`type(record.x) == string` 里的 `string` 会被判成「能解析的根」
  —— 实测按可解析性判定会误杀这条合法谓词(1 例),按 `SCOPE_ROOTS` 成员判定不会。

- cafec0a: `validateExpression`: give an over-budget expression a SIZE prescription instead of the dialect trailer (#7073)

  ADR-0032's shared validator appended one trailer to every `celEngine.compile`
  refusal, byte for byte — `— predicates are bare CEL (e.g. \`record.rating >= 4\`).`
  That sentence is right for a dialect mistake and actively wrong for a **bounds**
  refusal: an 80-clause conjunction is already bare CEL, perfectly good syntax, and
  merely over the platform's parse budget. The author was told to change the one
  thing that was never wrong; an AI author, which obeys the last sentence it was
  handed, rewrites the dialect and regresses. Reported by #6833's measurement.

  The refusal is unchanged — same inputs refused, same `Exceeded maxAstNodes (256)`
  front half from cel-js. Only the prescription is now class-aware: a `bounds`
  verdict (read off the engine's own `error.kind`, with the exceeded bound named by
  `parseCelToAstWithReason`) produces

  > invalid CEL predicate: Exceeded maxAstNodes (256) … — this is valid CEL that
  > exceeds the `maxAstNodes` budget (limit 256) — a SIZE fault, not a dialect
  > mistake, so re-spelling the expression will not fix it. Shrink it (fewer
  > clauses, shallower nesting, fewer list elements), or precompute the heavy part
  > into a stored field and reference that field instead. …

  while a genuine dialect/syntax fault keeps the old trailer verbatim. Fixed once at
  the producer, so all ~10 expression slots benefit — build, metadata registration,
  lint's `validateStackExpressions`, and the `validate_expression` tool. The
  remedies are deliberately slot-generic: the slots' combination semantics differ,
  so PR #6831's RLS-specific "splitting the top-level `&&` widens the grant" is not
  portable and splitting is offered only with a caveat.

  Also documents, text-only, the completeness gap in `cel-pushdown-limits.ts`'s
  "nothing else needs to move at GA": a third lint gate (`validateStackExpressions`)
  covers the same `sharingRules[].condition` and is mode-agnostic, so during the
  rc grace window lint is stricter than the runtime — benign, tightening-direction,
  and self-healing at GA. No behaviour change there.

- 07f1822: fix(formula): `matchesFilterCondition` 的 `$exists` 改读「有值」,与 `$null` 成严格互补

  **行为变更,影响 RLS 写侧 `check` 的判定。** `{ x: { $exists: true } }` 对
  `{ x: null }` 以前答 `true`(键存在),现在答 `false`(没有值)。

  `matchesFilterCondition` 是 RLS `check` 子句(insert/update 的 post-image)的求值器 ——
  写路径上没有查询可以下推,只能逐记录判定。它此前把 `$exists` 读成「键是否存在」
  (`actual !== undefined`),而 `driver-sql` 一直把同一个算子编译成 `IS NOT NULL`。
  于是同一条规则里的 `$exists`,写侧放行的记录读侧看不见。

  2026-08-06 裁定取「有值」,理由是另一种读法在最要紧的地方**无法兑现**:SQL 里列
  **就是** schema,一行不可能「缺一个键」,所以 `driver-sql` 除了 `IS NOT NULL` 别无
  可编译的东西。字段的存在性是 **schema** 的属性,不是**记录**的属性;spec 若声明
  「键是否存在」,就是在承诺两个后端永远交付不了的语义。因此 `driver-sql` 的发射器
  一字未动,移动的是本求值器。

  对齐之后 `$exists` 与 `$null` 在每个后端上都是严格互补:
  `$exists: true` ≡ `$null: false`,`$exists: false` ≡ `$null: true`。
  「键缺失」与「值为 null」在这里是同一个事实 —— 这也正是 `getPath` 对两者本来就
  返回同一个 `undefined` 的原因。

  `$ne` / `$nin` / `$notContains` / `$null` 四个算子本来就是本次裁定的目标语义,
  一字未改。

- cc2de0e: chore(packaging): 20 packages stop publishing their sources, tests and build tooling (#4248)

  These 20 packages declared no `files` field, so npm fell back to packing the
  whole package directory. `npm pack --dry-run` on `@objectstack/plugin-webhooks`
  listed **21 files** — 15 under `src/`, three of them unit tests
  (`auto-enqueuer.test.ts`, `bootstrap-declared-webhooks.test.ts`, …), plus the
  build-time `scripts/i18n-extract.config.ts`. `dist/` lands on top of that at
  publish time rather than instead of it, so consumers were installing the
  TypeScript sources and the test suite alongside the artifact they asked for.

  Each now declares `"files": ["dist", "README.md"]`, matching the 29 packages
  that already did. Nothing a consumer imports moves: every `main` / `types` /
  `exports` target in all 20 already resolved inside `dist/`, which the new
  `check:published-files` guard verifies rather than assumes. The visible change
  is a smaller install and a smaller dependency-scanning surface — `npm pack` on
  `@objectstack/plugin-webhooks` now yields 2 files plus `dist/`.

  The other half of the fix is the gate. Half the packages declaring `files` and
  half not was the #3786 shape — a hand-copied convention with nothing enforcing
  it, where whoever forgets the line gets no signal at all. `check:published-files`
  (new, wired into the always-required `lint` job) holds every non-private
  workspace package to four invariants: `files` is **declared**; it is
  **sufficient** (covers every entry point, so tightening a whitelist cannot ship
  a package that fails to resolve); it is **minimal** (admits no test, test-harness
  config or build script); and anything beyond `dist` + `README.md` is
  **registered** with a reason, reconciled in both directions so a stale exemption
  is an error rather than dead text. `@objectstack/spec` is the one package with
  registered extras — its `.zod.ts` sources, JSON Schemas, liveness ledgers and
  `CHANGELOG.md` are product, not build input.

  This also closes an assumption #4206 was resting on. Excluding `<pkg>/scripts/**`
  from the docs-drift implementation test is sound only while no package publishes
  `scripts/` as runtime code; that held, but it held because someone read all three
  offenders by hand. It is now checked on every PR.

- 078e28b: fix(formula): `==` / `!=` between a date STRING field and a Date-valued binding no longer answers a silent `false` (#7168)

  A mixed-provenance comparison — the shape a hook or validation predicate writes
  every day — returned the wrong boolean with no fault and no log line:

  ```text
  record.due == previous.due
    with { record: { due: "2026-06-20" }, previous: { due: Date(2026-06-20T00:00:00Z) } }
    ->  { ok: true, value: false }        // same field, same instant
  ```

  `previous` arrives from the driver hydrated as a `Date`; `record` arrives from a
  JSON payload as a `"YYYY-MM-DD"` string. cel-js compares a `string` against a
  `google.protobuf.Timestamp` and never matches, so the predicate answered `false`
  — and `!=` on the same pair answered `true`. Nothing errored, so nothing pointed
  at it. This is the failure class that hurts most in an AI-authored filter: the
  wrong answer is shaped exactly like a legitimate one.

  `rewriteTemporalEquality` already fixed this for a temporal **call** counterpart
  (`record.due == today()`, #3183) by coercing the string operand with `date(...)`.
  It now covers a Date-valued **binding** counterpart as well. A binding's runtime
  type is not visible in the AST, so this arm is decided per row against the values
  in the evaluation scope, and its verdict is deliberately never cached against the
  expression source.

  **Comparisons that change answer** — one operand an ISO-8601 date/date-time
  string, the other a binding holding a `Date`:

  - `record.due == previous.due` (same instant) — was `false`, now `true`
  - `record.due != previous.due` (same instant) — was `true`, now `false`
  - either operand order, and a `"…T14:33:00Z"` string against the same instant

  **Comparisons that deliberately do NOT change** — the coercion requires the
  counterpart to be a real `Date` _and_ this operand to be an ISO-8601 string that
  parses, so everything below answers exactly as it did before:

  - two strings — `"2026-06-20" == "2026-06-20"` stays STRING equality
  - two `Date`s — already compared as instants
  - a different calendar day — stays `false`
  - a non-date string against a `Date` (`"hello"`) — stays `false`
  - a **numeric** string against a `Date` (`"5"`) — stays `false`. Load-bearing:
    `new Date("5")` and `new Date("05")` both parse to 2001-05-01, so coercing here
    would invent an equality between two different strings
  - a date-ONLY string against a `Date` carrying wall-clock time — stays `false`.
    `date()` parses, it does not truncate to a calendar day, and those are
    genuinely different instants; truncating both sides would turn a correct
    `false` into a wrong `true` for real datetime comparisons
  - ordering (`<` / `>=`) is untouched — that path is ADR-0032 §1c's retry
  - a string LITERAL counterpart is untouched — it is not a binding

  Cross-type `in` membership (`record.n in [1, 7]` with `n: "7"`) is a separate
  clean-path question and is unchanged, deferred by maintainer ruling on #7168
  pending a measured victim.

- e4c2dc8: Order temporal operands correctly when one side is a JS `Date` on the two
  type-blind filter backends (ADR-0053 D-A3 / #4191).

  `utcInstantMs` joins `nextUtcCalendarDay` in `@objectstack/spec/data`
  (re-exported from `@objectstack/core`): it reads the UTC instant a temporal
  operand denotes, accepting only unambiguous spellings — a `Date`, epoch ms, a
  bare `YYYY-MM-DD`, and an ISO timestamp with or without an explicit zone (a
  zone-naive one being UTC, per D-B2) — and returning `null` for everything
  else, notably a bare wall clock, which denotes no instant.

  Both type-blind evaluators now use it to compare a `Date` against wire text,
  which JS relational operators cannot do: `<` and friends coerce with hint
  `number`, so the `Date` becomes its epoch and the string becomes `NaN`.

  - `formula`'s `matchesFilterCondition` (the RLS write-side `check`) dropped
    every `Date`-valued row in 10 of the 16 shared conformance cases. The
    post-image is the caller's raw write payload, so an SDK write of
    `new Date()` hit this directly, and fail-closed turned it into a **denied
    write**.
  - `service-analytics`' preview evaluator diverged on the same 10 cases in
    BOTH directions, because `String(new Date())` sorts after every `'2026-…'`
    comparand — a drafted chart both lost rows and gained ones, then changed
    its numbers at publish. Rows from a mongo-backed dataset arrive as BSON
    `Date`s, so this was reachable in normal use.

  Comparisons that did not involve a `Date` are unchanged.

- Updated dependencies [50616d9]
- Updated dependencies [430dcc2]
- Updated dependencies [6a67d7a]
- Updated dependencies [333a374]
- Updated dependencies [9fe9c1d]
- Updated dependencies [3d5c090]
- Updated dependencies [e5bd768]
- Updated dependencies [08b5a3d]
- Updated dependencies [e027b3e]
- Updated dependencies [e6ac4bd]
- Updated dependencies [c2429b0]
- Updated dependencies [445a0c2]
- Updated dependencies [d99aeb3]
- Updated dependencies [f6609e6]
- Updated dependencies [4727eb8]
- Updated dependencies [a70358a]
- Updated dependencies [0ecc656]
- Updated dependencies [06772eb]
- Updated dependencies [d4e0809]
- Updated dependencies [80334c7]
- Updated dependencies [f63cd09]
- Updated dependencies [97e7e3c]
- Updated dependencies [ce5242c]
- Updated dependencies [a7163ea]
- Updated dependencies [e6e9379]
- Updated dependencies [5823d59]
- Updated dependencies [3140f9c]
- Updated dependencies [9500ba4]
- Updated dependencies [fa3d0cf]
- Updated dependencies [af5a224]
- Updated dependencies [71f76e1]
- Updated dependencies [37b1346]
- Updated dependencies [99736a0]
- Updated dependencies [fe67e34]
- Updated dependencies [fdb4f50]
- Updated dependencies [270650f]
- Updated dependencies [3aef718]
- Updated dependencies [1bd5652]
- Updated dependencies [14252d3]
- Updated dependencies [7fb436c]
- Updated dependencies [879ea13]
- Updated dependencies [8828b9e]
- Updated dependencies [1ea6bce]
- Updated dependencies [c1dcacd]
- Updated dependencies [ad303ed]
- Updated dependencies [32ccb23]
- Updated dependencies [f5a4ef0]
- Updated dependencies [2d3e255]
- Updated dependencies [a8940e4]
- Updated dependencies [7d7521f]
- Updated dependencies [5dc4d02]
- Updated dependencies [f724f69]
- Updated dependencies [98877c9]
- Updated dependencies [98877c9]
- Updated dependencies [53068c1]
- Updated dependencies [ee58392]
- Updated dependencies [f16e54e]
- Updated dependencies [06be54e]
- Updated dependencies [28ad90e]
- Updated dependencies [76d74ec]
- Updated dependencies [201b31f]
- Updated dependencies [e6b1b69]
- Updated dependencies [259459d]
- Updated dependencies [3f7f14e]
- Updated dependencies [e2616e0]
- Updated dependencies [6fdc5c6]
- Updated dependencies [8b9d71e]
- Updated dependencies [05154a1]
- Updated dependencies [33f5e23]
- Updated dependencies [259af21]
- Updated dependencies [f8644c7]
- Updated dependencies [306ca50]
- Updated dependencies [978fed2]
- Updated dependencies [cfc293f]
- Updated dependencies [587fc91]
- Updated dependencies [de70b42]
- Updated dependencies [9b6fe7c]
- Updated dependencies [fb3d99b]
- Updated dependencies [1986594]
- Updated dependencies [6968885]
- Updated dependencies [eaed61f]
- Updated dependencies [cdfbee2]
- Updated dependencies [ad4af62]
- Updated dependencies [debe2f6]
- Updated dependencies [d44dbfa]
- Updated dependencies [29c6c9d]
- Updated dependencies [d21c001]
- Updated dependencies [ad047d2]
- Updated dependencies [8c711fb]
- Updated dependencies [f1cc3a3]
- Updated dependencies [09e4547]
- Updated dependencies [97b0798]
- Updated dependencies [474fe39]
- Updated dependencies [0bc685a]
- Updated dependencies [b949059]
- Updated dependencies [2826d1e]
- Updated dependencies [be1c52c]
- Updated dependencies [c5ff96d]
- Updated dependencies [5a84d41]
- Updated dependencies [84e7be9]
- Updated dependencies [91f4c78]
- Updated dependencies [ddc2527]
- Updated dependencies [820eff9]
- Updated dependencies [a6c3f38]
- Updated dependencies [debc23a]
- Updated dependencies [0f8ad09]
- Updated dependencies [553a47f]
- Updated dependencies [43a7a8d]
- Updated dependencies [a98085f]
- Updated dependencies [20b1a9e]
- Updated dependencies [344a22a]
- Updated dependencies [4827e91]
- Updated dependencies [8d895ff]
- Updated dependencies [86f7a20]
- Updated dependencies [a3a884d]
- Updated dependencies [cfed092]
- Updated dependencies [203a449]
- Updated dependencies [8f9689f]
- Updated dependencies [73f69dc]
- Updated dependencies [04c56aa]
- Updated dependencies [f6472d7]
- Updated dependencies [57a3bb3]
- Updated dependencies [b3efeb7]
- Updated dependencies [ddd075a]
- Updated dependencies [88154be]
- Updated dependencies [e8dc61e]
- Updated dependencies [9c82146]
- Updated dependencies [5f9a987]
- Updated dependencies [744b8f5]
- Updated dependencies [ac37fc6]
- Updated dependencies [2f3e793]
- Updated dependencies [4820f55]
- Updated dependencies [462d9c4]
- Updated dependencies [78caf51]
- Updated dependencies [7d21581]
- Updated dependencies [37785ed]
- Updated dependencies [62a789b]
- Updated dependencies [2e284b2]
- Updated dependencies [d8e8d9c]
- Updated dependencies [789ad63]
- Updated dependencies [f2445c9]
- Updated dependencies [94e749b]
- Updated dependencies [ea1d916]
- Updated dependencies [2af1988]
- Updated dependencies [1b49eaf]
- Updated dependencies [ae31a19]
- Updated dependencies [e0f300b]
- Updated dependencies [0161c7f]
- Updated dependencies [e900015]
- Updated dependencies [db02d47]
- Updated dependencies [b5bdf48]
- Updated dependencies [23338c3]
- Updated dependencies [12a19a8]
- Updated dependencies [5b843fb]
- Updated dependencies [62b6a2f]
- Updated dependencies [7e5af5c]
- Updated dependencies [5b4780b]
- Updated dependencies [a933452]
- Updated dependencies [9d1d9c7]
- Updated dependencies [8140915]
- Updated dependencies [a019e52]
- Updated dependencies [e8f8f6c]
- Updated dependencies [41dcda3]
- Updated dependencies [7b48cf9]
- Updated dependencies [b5404f4]
- Updated dependencies [64fc6d5]
- Updated dependencies [b4487aa]
- Updated dependencies [1007379]
- Updated dependencies [65ca83a]
- Updated dependencies [0bfdf46]
- Updated dependencies [947d4f9]
- Updated dependencies [f764691]
- Updated dependencies [e120a5a]
- Updated dependencies [e5bd2f6]
- Updated dependencies [e650d67]
- Updated dependencies [04476e7]
- Updated dependencies [67bf2e2]
- Updated dependencies [eaaf03c]
- Updated dependencies [d17df80]
- Updated dependencies [7d0e7b5]
- Updated dependencies [c6d1cb4]
- Updated dependencies [6513c17]
- Updated dependencies [36030ff]
- Updated dependencies [79228cd]
- Updated dependencies [6117f7b]
- Updated dependencies [e533b0b]
- Updated dependencies [cdf4d9a]
- Updated dependencies [aee1806]
- Updated dependencies [c13350b]
- Updated dependencies [c13350b]
- Updated dependencies [2c1988c]
- Updated dependencies [9ca2d85]
- Updated dependencies [c13350b]
- Updated dependencies [891d345]
- Updated dependencies [c8124e5]
- Updated dependencies [a52e2ef]
- Updated dependencies [5293114]
- Updated dependencies [376a061]
- Updated dependencies [c142ced]
- Updated dependencies [211abdb]
- Updated dependencies [b3363e9]
- Updated dependencies [eda599e]
- Updated dependencies [a1a4140]
- Updated dependencies [7c7e246]
- Updated dependencies [2ef1807]
- Updated dependencies [f35cdc5]
- Updated dependencies [d03fe25]
- Updated dependencies [217e2e6]
- Updated dependencies [2672f85]
- Updated dependencies [20bc357]
- Updated dependencies [11066f6]
- Updated dependencies [916af17]
- Updated dependencies [84c86fb]
- Updated dependencies [2a2a9fb]
- Updated dependencies [86a71d1]
- Updated dependencies [c001422]
- Updated dependencies [77022a9]
- Updated dependencies [d5c75e2]
- Updated dependencies [03d26f7]
- Updated dependencies [5966c2a]
- Updated dependencies [2382580]
- Updated dependencies [9ea2bc5]
- Updated dependencies [a2e157c]
- Updated dependencies [95c4227]
- Updated dependencies [2a61116]
- Updated dependencies [52760bf]
- Updated dependencies [5543020]
- Updated dependencies [880d343]
- Updated dependencies [6e82972]
- Updated dependencies [d4df105]
- Updated dependencies [4615a18]
- Updated dependencies [f505689]
- Updated dependencies [d9fa683]
- Updated dependencies [606d577]
- Updated dependencies [4384921]
- Updated dependencies [e2798fa]
- Updated dependencies [3c628ce]
- Updated dependencies [c2d9098]
- Updated dependencies [0fd8556]
- Updated dependencies [3c7bcc0]
- Updated dependencies [4b6cac7]
- Updated dependencies [7631964]
- Updated dependencies [ac471a0]
- Updated dependencies [60ae58e]
- Updated dependencies [7f62706]
- Updated dependencies [667fa44]
- Updated dependencies [37e38d1]
- Updated dependencies [e906126]
- Updated dependencies [ce92674]
- Updated dependencies [08363a0]
- Updated dependencies [444de5b]
- Updated dependencies [a227ed7]
- Updated dependencies [7cb922e]
- Updated dependencies [1d22114]
- Updated dependencies [1eb13a0]
- Updated dependencies [c52e608]
- Updated dependencies [9613396]
- Updated dependencies [3f7b4ff]
- Updated dependencies [74155c7]
- Updated dependencies [b5f9397]
- Updated dependencies [ed77493]
- Updated dependencies [6908830]
- Updated dependencies [8b06bba]
- Updated dependencies [58a03d2]
- Updated dependencies [2bacd1a]
- Updated dependencies [e47b342]
- Updated dependencies [4c54037]
- Updated dependencies [dc530b4]
- Updated dependencies [9f601e8]
- Updated dependencies [6a9dec6]
- Updated dependencies [0f7157b]
- Updated dependencies [4dc1c7d]
- Updated dependencies [d9bef45]
- Updated dependencies [4dfd002]
- Updated dependencies [f549a0d]
- Updated dependencies [51c5227]
- Updated dependencies [82da264]
- Updated dependencies [77be690]
- Updated dependencies [4ed7ed4]
- Updated dependencies [9b9b70f]
- Updated dependencies [f5a9bc2]
- Updated dependencies [e59786e]
- Updated dependencies [2fa4ca1]
- Updated dependencies [bcf1112]
- Updated dependencies [baeb4f0]
- Updated dependencies [29488cc]
- Updated dependencies [881a3cc]
- Updated dependencies [f5a2320]
- Updated dependencies [ad6317b]
- Updated dependencies [811c30c]
- Updated dependencies [a4a85c8]
- Updated dependencies [859cb83]
- Updated dependencies [07a4e26]
- Updated dependencies [9774b78]
- Updated dependencies [8a88885]
- Updated dependencies [deb538f]
- Updated dependencies [b49ccfd]
- Updated dependencies [5b89711]
- Updated dependencies [85d95e7]
- Updated dependencies [08cd163]
- Updated dependencies [0c8a22f]
- Updated dependencies [5f7669e]
- Updated dependencies [becbe53]
- Updated dependencies [b127c8b]
- Updated dependencies [763931e]
- Updated dependencies [ec975f1]
- Updated dependencies [168f60f]
- Updated dependencies [b07d829]
- Updated dependencies [de9af8a]
- Updated dependencies [eb4204b]
- Updated dependencies [a80302a]
- Updated dependencies [a648e96]
- Updated dependencies [a47ac06]
- Updated dependencies [e4c61a7]
- Updated dependencies [cc60165]
- Updated dependencies [474f131]
- Updated dependencies [081aa6f]
- Updated dependencies [91f4c78]
- Updated dependencies [050cd82]
- Updated dependencies [4d552af]
- Updated dependencies [44d677c]
- Updated dependencies [c32944d]
- Updated dependencies [1dd780f]
- Updated dependencies [e8d0c21]
- Updated dependencies [244ca86]
- Updated dependencies [546ab3c]
- Updated dependencies [c4df271]
- Updated dependencies [c8d6f6e]
- Updated dependencies [0b51bb6]
- Updated dependencies [d9971d3]
- Updated dependencies [7dc1067]
- Updated dependencies [4f13be2]
- Updated dependencies [a41ba5c]
- Updated dependencies [189854c]
- Updated dependencies [0e3a226]
- Updated dependencies [92a67f2]
- Updated dependencies [9136327]
- Updated dependencies [bf0ae99]
- Updated dependencies [abeb375]
- Updated dependencies [cb3b6cd]
- Updated dependencies [73b7234]
- Updated dependencies [d2b97c3]
- Updated dependencies [61cc079]
- Updated dependencies [0e96e46]
- Updated dependencies [c1d44f7]
- Updated dependencies [59b794f]
- Updated dependencies [ef4efa8]
- Updated dependencies [cbb6a5c]
- Updated dependencies [fc3a36a]
- Updated dependencies [ab9fb5c]
- Updated dependencies [69787f0]
- Updated dependencies [5d022a1]
- Updated dependencies [042b9ee]
- Updated dependencies [f985b3f]
- Updated dependencies [795b6e1]
- Updated dependencies [d52d4fe]
- Updated dependencies [742cebb]
- Updated dependencies [175d789]
- Updated dependencies [f549a0d]
- Updated dependencies [427344c]
- Updated dependencies [8af76ae]
- Updated dependencies [1d4756e]
- Updated dependencies [720c5ad]
- Updated dependencies [a8d1e24]
- Updated dependencies [b85cc54]
- Updated dependencies [a36db28]
- Updated dependencies [7a8476f]
- Updated dependencies [518ca7a]
- Updated dependencies [41642b0]
- Updated dependencies [4cca74c]
- Updated dependencies [88ef03e]
- Updated dependencies [9a4932a]
- Updated dependencies [3f8817a]
- Updated dependencies [a2443e3]
- Updated dependencies [e1554b1]
- Updated dependencies [9e2caf3]
- Updated dependencies [4856789]
- Updated dependencies [81ce41a]
- Updated dependencies [85e1e4e]
- Updated dependencies [c3f4916]
- Updated dependencies [55dbbba]
- Updated dependencies [33e0385]
- Updated dependencies [dac6a08]
- Updated dependencies [72c3c86]
- Updated dependencies [2d8dba3]
- Updated dependencies [7f1a635]
- Updated dependencies [2205363]
- Updated dependencies [09fe58d]
- Updated dependencies [f9fc874]
- Updated dependencies [d62f8eb]
- Updated dependencies [d0a5ceb]
- Updated dependencies [a7586cd]
- Updated dependencies [4c5e80e]
- Updated dependencies [4b5702a]
- Updated dependencies [011b386]
- Updated dependencies [e18a162]
- Updated dependencies [394b7a1]
- Updated dependencies [ce92674]
- Updated dependencies [cf2c9b7]
- Updated dependencies [d127ff0]
- Updated dependencies [36d90fc]
- Updated dependencies [7777e8f]
- Updated dependencies [9b86cf6]
- Updated dependencies [d063a96]
- Updated dependencies [8825a06]
- Updated dependencies [5087ac6]
- Updated dependencies [677b591]
- Updated dependencies [cf7c694]
- Updated dependencies [ddd0f06]
- Updated dependencies [d77d1b7]
- Updated dependencies [0f9faa2]
- Updated dependencies [2d1ddf0]
- Updated dependencies [354b00f]
- Updated dependencies [3de535b]
- Updated dependencies [fe2e15a]
- Updated dependencies [5b79a34]
- Updated dependencies [502564d]
- Updated dependencies [603cab8]
- Updated dependencies [c757854]
- Updated dependencies [471839d]
- Updated dependencies [507b92a]
- Updated dependencies [b508244]
- Updated dependencies [df95346]
- Updated dependencies [3dede58]
- Updated dependencies [c6b6bb4]
- Updated dependencies [594508e]
- Updated dependencies [7cf42fe]
- Updated dependencies [5966c2a]
- Updated dependencies [0045682]
- Updated dependencies [7309c81]
- Updated dependencies [2f59da0]
- Updated dependencies [d56012f]
- Updated dependencies [f78dd83]
- Updated dependencies [a2cd18a]
- Updated dependencies [9051802]
- Updated dependencies [20bc1ec]
- Updated dependencies [1c625ca]
- Updated dependencies [2f8328c]
- Updated dependencies [2a6c279]
- Updated dependencies [8c8f0df]
- Updated dependencies [8ad609c]
- Updated dependencies [bbee302]
- Updated dependencies [90c2b15]
- Updated dependencies [4638aaa]
- Updated dependencies [0222d3c]
- Updated dependencies [08863dd]
- Updated dependencies [f293d45]
- Updated dependencies [56664f5]
- Updated dependencies [71f205d]
- Updated dependencies [f067930]
- Updated dependencies [414395b]
- Updated dependencies [42eeb7d]
- Updated dependencies [31cbe90]
- Updated dependencies [6b7129a]
- Updated dependencies [97ace2a]
- Updated dependencies [26e1029]
- Updated dependencies [0a936ea]
- Updated dependencies [90bbf25]
- Updated dependencies [023c00b]
- Updated dependencies [eb91eba]
- Updated dependencies [42da73d]
- Updated dependencies [01e124d]
- Updated dependencies [ef7b5ef]
- Updated dependencies [9514767]
- Updated dependencies [8f20201]
- Updated dependencies [155507e]
- Updated dependencies [643b7c7]
- Updated dependencies [7bba90b]
- Updated dependencies [8813b90]
- Updated dependencies [108ba8d]
- Updated dependencies [2a5f04a]
- Updated dependencies [4f740b0]
- Updated dependencies [7ce02eb]
- Updated dependencies [b4ad984]
- Updated dependencies [a9f32df]
- Updated dependencies [aeb9b27]
- Updated dependencies [7d27da0]
- Updated dependencies [1a15893]
- Updated dependencies [b70e534]
- Updated dependencies [7e05d8e]
- Updated dependencies [8f1851e]
- Updated dependencies [61ea810]
- Updated dependencies [2233a85]
- Updated dependencies [67452d1]
- Updated dependencies [089767f]
- Updated dependencies [a13827e]
- Updated dependencies [66d99ec]
- Updated dependencies [cb43296]
- Updated dependencies [b61afc1]
- Updated dependencies [79021fc]
- Updated dependencies [7733604]
- Updated dependencies [40e420f]
- Updated dependencies [62dd69a]
- Updated dependencies [d13004a]
- Updated dependencies [e15e679]
- Updated dependencies [2ab1257]
- Updated dependencies [0fc6219]
- Updated dependencies [061406d]
- Updated dependencies [e4c8b6c]
- Updated dependencies [acb10f6]
- Updated dependencies [605e190]
- Updated dependencies [c6c59f1]
- Updated dependencies [b0e78a8]
- Updated dependencies [f31cc8d]
- Updated dependencies [f343dc4]
- Updated dependencies [8269e32]
- Updated dependencies [74f7339]
- Updated dependencies [a6c35a2]
- Updated dependencies [c2f1002]
- Updated dependencies [4cc4fb7]
- Updated dependencies [97b6658]
- Updated dependencies [2c26040]
- Updated dependencies [f758cec]
- Updated dependencies [5b47ab5]
- Updated dependencies [b09d8d9]
- Updated dependencies [b09d8d9]
- Updated dependencies [8675db6]
- Updated dependencies [b09d8d9]
- Updated dependencies [27358d5]
- Updated dependencies [1c3da1f]
- Updated dependencies [c1f344b]
- Updated dependencies [3eb1b2b]
- Updated dependencies [9c93465]
- Updated dependencies [a34fd2e]
- Updated dependencies [ebb209c]
- Updated dependencies [76bcb83]
- Updated dependencies [59b85c0]
- Updated dependencies [889ae47]
- Updated dependencies [4f4c3fb]
- Updated dependencies [78f0be8]
- Updated dependencies [6e357ed]
- Updated dependencies [d6938bf]
- Updated dependencies [35f7fb4]
- Updated dependencies [0410522]
- Updated dependencies [63b33e6]
- Updated dependencies [f163028]
- Updated dependencies [814db6d]
- Updated dependencies [a5302c7]
- Updated dependencies [31e0be9]
- Updated dependencies [4bfd455]
- Updated dependencies [ffd2ce2]
- Updated dependencies [2a44c1d]
- Updated dependencies [7084313]
- Updated dependencies [f07808c]
- Updated dependencies [7ffc3d3]
- Updated dependencies [88346ba]
- Updated dependencies [4631592]
- Updated dependencies [62f8017]
- Updated dependencies [32ff033]
- Updated dependencies [a831df1]
- Updated dependencies [f752ee3]
- Updated dependencies [a1b61e0]
- Updated dependencies [cd6b9f2]
- Updated dependencies [2cb6d3c]
- Updated dependencies [af2a095]
- Updated dependencies [5ac93d4]
- Updated dependencies [695cfbd]
- Updated dependencies [0e043d8]
- Updated dependencies [93f267f]
- Updated dependencies [7445149]
- Updated dependencies [ec796d5]
- Updated dependencies [071d0dc]
- Updated dependencies [0024abf]
- Updated dependencies [8dd98bf]
- Updated dependencies [e87fea1]
- Updated dependencies [c65e529]
- Updated dependencies [0848bea]
- Updated dependencies [d51bed2]
- Updated dependencies [dadd1ad]
- Updated dependencies [acbf364]
- Updated dependencies [3ca34c1]
- Updated dependencies [7adc841]
- Updated dependencies [239c3a3]
- Updated dependencies [b8b3c64]
- Updated dependencies [2f2e63c]
- Updated dependencies [4845f85]
- Updated dependencies [486d526]
- Updated dependencies [94a0bbc]
- Updated dependencies [d6bfb3d]
- Updated dependencies [8a9c079]
- Updated dependencies [7b005b4]
- Updated dependencies [cc3555e]
- Updated dependencies [a2266a6]
- Updated dependencies [d25a0ec]
- Updated dependencies [89d7b35]
- Updated dependencies [94f7b6a]
- Updated dependencies [5c94f83]
- Updated dependencies [ea936f3]
- Updated dependencies [0c0fbd9]
- Updated dependencies [667b83e]
- Updated dependencies [f3141d8]
- Updated dependencies [7687f7b]
- Updated dependencies [5a84d41]
- Updated dependencies [fd3013a]
- Updated dependencies [85ec26d]
- Updated dependencies [73e576f]
- Updated dependencies [f6476fc]
- Updated dependencies [69ac82c]
- Updated dependencies [4ac12ef]
- Updated dependencies [833ed84]
- Updated dependencies [a18abf3]
- Updated dependencies [c6a4eeb]
- Updated dependencies [1659072]
- Updated dependencies [f450ae7]
- Updated dependencies [abceb0d]
- Updated dependencies [627b188]
- Updated dependencies [8d4eae7]
- Updated dependencies [c5a5996]
- Updated dependencies [0c302a7]
- Updated dependencies [b88f5e8]
- Updated dependencies [65a3a84]
- Updated dependencies [6633337]
- Updated dependencies [21676eb]
- Updated dependencies [e9cb9ab]
- Updated dependencies [42cc219]
- Updated dependencies [d7e0b42]
- Updated dependencies [3510e4a]
- Updated dependencies [f00d8d4]
- Updated dependencies [5326b36]
- Updated dependencies [aa4b90d]
- Updated dependencies [ccd9397]
- Updated dependencies [503be86]
- Updated dependencies [54299ca]
- Updated dependencies [ae490ef]
- Updated dependencies [e124711]
- Updated dependencies [dc61def]
- Updated dependencies [bca935b]
- Updated dependencies [c54c822]
- Updated dependencies [8dcc0f5]
- Updated dependencies [75b9e51]
- Updated dependencies [f61c8cf]
- Updated dependencies [e3ef52b]
- Updated dependencies [0a2f233]
- Updated dependencies [8621cdd]
- Updated dependencies [251e888]
- Updated dependencies [07f1822]
- Updated dependencies [e336549]
- Updated dependencies [3bb9340]
- Updated dependencies [1e604c4]
- Updated dependencies [04fab5e]
- Updated dependencies [183b4c4]
- Updated dependencies [7f713b6]
- Updated dependencies [d40f43a]
- Updated dependencies [2fdb36e]
- Updated dependencies [6f23667]
- Updated dependencies [cde1975]
- Updated dependencies [0bc685a]
- Updated dependencies [20526f5]
- Updated dependencies [efedd28]
- Updated dependencies [5d21a48]
- Updated dependencies [5278e11]
- Updated dependencies [c5eef1d]
- Updated dependencies [e5e7ee0]
- Updated dependencies [23dba62]
- Updated dependencies [e0f300b]
- Updated dependencies [761a0ba]
- Updated dependencies [c960170]
- Updated dependencies [19365b7]
- Updated dependencies [ba98e26]
- Updated dependencies [b7ed26d]
- Updated dependencies [a2ebea2]
- Updated dependencies [800bdb0]
- Updated dependencies [9d4dfc4]
- Updated dependencies [1059965]
- Updated dependencies [def5919]
- Updated dependencies [60b672e]
- Updated dependencies [6b441a8]
- Updated dependencies [ce0cfe9]
- Updated dependencies [04f1182]
- Updated dependencies [be87153]
- Updated dependencies [dd0f681]
- Updated dependencies [60f0dd8]
- Updated dependencies [a87c5cd]
- Updated dependencies [a47f338]
- Updated dependencies [b3a3d83]
- Updated dependencies [7a55913]
- Updated dependencies [35accbf]
- Updated dependencies [6038de7]
- Updated dependencies [fc5f536]
- Updated dependencies [5647006]
- Updated dependencies [e654bfd]
- Updated dependencies [01a7337]
- Updated dependencies [b45c71e]
- Updated dependencies [f8cfbb4]
- Updated dependencies [6e6c872]
- Updated dependencies [2598216]
- Updated dependencies [11949fc]
- Updated dependencies [2c7e62d]
- Updated dependencies [eb95d97]
- Updated dependencies [b098b0e]
- Updated dependencies [4d00b13]
- Updated dependencies [1363084]
- Updated dependencies [fa5758e]
- Updated dependencies [38f7e4f]
- Updated dependencies [eb7613c]
- Updated dependencies [c57f3cf]
- Updated dependencies [ecc9110]
- Updated dependencies [e4c2dc8]
- Updated dependencies [97faca3]
- Updated dependencies [57bab76]
- Updated dependencies [c89d18c]
- Updated dependencies [1bd2795]
- Updated dependencies [f7bd4e2]
- Updated dependencies [361bd5b]
- Updated dependencies [aac90a5]
- Updated dependencies [3da3da5]
- Updated dependencies [1e6ab15]
- Updated dependencies [b90086a]
- Updated dependencies [8186a70]
- Updated dependencies [a329cca]
- Updated dependencies [c87ef70]
- Updated dependencies [3cb0618]
- Updated dependencies [32a0874]
- Updated dependencies [6eec18c]
- Updated dependencies [4d7bebf]
- Updated dependencies [821ac7a]
- Updated dependencies [8f81731]
- Updated dependencies [7055c22]
- Updated dependencies [785a748]
- Updated dependencies [3af0354]
- Updated dependencies [866ff16]
- Updated dependencies [5a85e67]
- Updated dependencies [8b50cb3]
- Updated dependencies [a0fdc56]
- Updated dependencies [b95577a]
- Updated dependencies [0dcbc11]
- Updated dependencies [d88f3e9]
- Updated dependencies [ad5fe25]
- Updated dependencies [c183a12]
- Updated dependencies [83c161f]
- Updated dependencies [d8c4957]
- Updated dependencies [b9f930b]
- Updated dependencies [f24cb83]
- Updated dependencies [5dbbb92]
- Updated dependencies [ea90179]
- Updated dependencies [1818998]
- Updated dependencies [ce92674]
- Updated dependencies [5ef0b5b]
- Updated dependencies [8c2db68]
- Updated dependencies [22b5e54]
- Updated dependencies [0166bd5]
- Updated dependencies [8064b07]
- Updated dependencies [09ee21c]
- Updated dependencies [4a56dbd]
- Updated dependencies [289d04a]
- Updated dependencies [f549a0d]
- Updated dependencies [48fbacb]
- Updated dependencies [06df4fa]
- Updated dependencies [3fc2e48]
- Updated dependencies [c9b809f]
- Updated dependencies [e8f435c]
- Updated dependencies [32386f8]
- Updated dependencies [9b702dc]
- Updated dependencies [ab16331]
- Updated dependencies [41610f6]
- Updated dependencies [69f1dfd]
- Updated dependencies [bbe05de]
- Updated dependencies [355e951]
- Updated dependencies [a1dd1e4]
- Updated dependencies [dadb43f]
  - @objectstack/spec@17.0.0

## 17.0.0-rc.6

### Minor Changes

- f6cd635: fix(formula): the CEL pushdown compiler parses through the canonical front end, so `DEFAULT_LIMITS` finally apply to RLS/sharing predicates (#6132)

  `cel-to-filter.ts` — the ONE canonical CEL → `FilterCondition` pushdown compiler
  (ADR-0058 D1/D2/D6), consumed by the RLS path (`plugin-security`'s
  `RLSCompiler`), the sharing seeder (`plugin-sharing`), and the analytics SQL
  backend — kept a **private, limitless** parse environment of its own:

  ```ts
  new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true });
  ```

  no `limits`, no stdlib, no `rewriteNullableTernary`. That made the pushdown path
  the one place on the platform that answered a _different_ question from
  `celEngine.compile()` about what parses. Measured: a 300-term addition, a
  60-level parenthesis nest and a 200-element list literal all parsed there while
  the interpreter refused each one outright (`Exceeded maxAstNodes (256)` /
  `maxDepth (32)` / `maxListElements (64)`). Escalated: an 80-term conjunction, a
  40-level nest and a 200-element `$in` all reached **real pushdown SQL**,
  silently — and `isSupportedRlsExpression`, the ADR-0056 D4 authoring gate, was a
  thin wrapper over the same limitless environment, so it was no independent check
  either.

  It now parses through `parseCelToAstWithReason` — #4812's canonical entry, with
  `DEFAULT_LIMITS`, the stdlib and the #3306 null-guard rewrite. "What parses" has
  one answer again.

  **Within the limits nothing moves, and that is measured, not asserted.** Across
  the 710 sources of the pushdown corpus that both front ends accept, the only AST
  difference is `rewriteNullableTernary`'s `dyn(…)` wrap on the three null-guard
  ternaries — and a ternary faults on its own `?:` node before the lowerer
  descends into a branch, so verdict _and_ detail come out byte-identical. Pinned
  in `cel-to-filter-parse-convergence.test.ts`, which rebuilds the old environment
  to compare against.

  **Over the limits, behaviour changes — in two dated steps.**

  - **Now, during `17.0.0-rc.x` (`rc-grace`):** an over-limit predicate **still
    compiles** — nothing that enforces today stops enforcing on this upgrade — and
    emits one WARN per predicate naming the bound that was exceeded
    (`maxAstNodes` / `maxDepth` / `maxListElements` / …), the platform's value for
    it, and what the predicate itself measures (cel-js's own accounting: the
    smallest bound it parses under), plus what will happen at GA.
  - **At v17.0.0 GA (`fail-closed`):** the same predicate is **refused** —
    `{ ok: false, reason: 'parse-error', detail: 'Exceeded maxAstNodes (256)' }` —
    and the RLS path turns that into `RLS_DENY_FILTER`, i.e. zero rows, fail
    closed. A sharing rule with such a condition is not seeded.

  **The flip is one line.** `CEL_PUSHDOWN_LIMITS_MODE` in
  `packages/formula/src/cel-pushdown-limits.ts` — the single dated switch,
  shipping as `'rc-grace'`, to be set to `'fail-closed'` at the v17.0.0 GA release
  (i.e. when this package's version leaves `17.0.0-rc.x`). Both positions are
  exercised in CI today, in `@objectstack/formula` and in
  `@objectstack/plugin-security` (where the `RLS_DENY_FILTER` outcome lives), so
  the GA half is proven before it ships rather than after. Two tests are written
  to go red on that line so the flip cannot be silent.

  **If you author RLS or sharing predicates:** a predicate over any of these
  bounds is already refused everywhere else on the platform (`os build`,
  `os validate`, the interpreter). Split it, or move the logic into a hook/action
  body (`ScriptBody { language: 'js' }`), before upgrading past the rc line. The
  WARN names the predicate and its measure so you can find them.

  **New public surface**, for consumers that must _report_ a refusal rather than
  merely react to one:

  - `parseCelToAstWithReason(source, opts?)` — the reason-carrying sister entrance
    to `parseCelToAst`. Same front end, same verdict, but it distinguishes
    `'parse'` (not valid CEL) from `'bounds'` (valid CEL, over budget) and names
    the exceeded limit, its platform value, and the source's measure. Graded by
    the same by-class/by-code classifier `celEngine.compile` uses (#6223) — never
    by error prose. `parseCelToAst` is unchanged and still collapses every refusal
    to `null`.
  - `CelParseResult`, `CelBoundsOverrun`, `CelLimitKey`, `ParseCelToAstOptions`.
  - `CEL_PUSHDOWN_LIMITS_MODE`, `celPushdownLimitsMode()`,
    `setCelPushdownLimitsModeForTests()`, `CelPushdownLimitsMode`.

  `@objectstack/lint` needs no change, at either position of the switch. Its two
  enforceability gates read `isSupportedRlsExpression` and `compileCelToFilter`,
  both downstream of this switch, and both suites pin "the lint verdict IS the
  consumer's verdict" in both directions — so authoring-time reporting flips with
  the runtime by construction. An over-limit sharing `condition` is in fact
  already an authoring **error** today (`expression-invalid`, from the general
  expression rule, quoting `Exceeded maxAstNodes (256)`), because that rule has
  always gone through the canonical front end.

- 3f8817a: feat(spec,drivers,objectql,analytics,formula): `$icontains` reaches every JS evaluation face (#6520)

  The other half of #5702. That change implemented `$icontains` on the SQL family
  and correctly left the spec's `FILTER_OPERATORS` alone; this one adds the
  operator to that array and gives every remaining evaluation face an arm, in ONE
  change, because those two steps cannot be separated.

  **Why one PR.** `FILTER_OPERATORS` is not a word list, it is a runtime allowlist:
  `driver-memory`'s shape gate derives from it, and its matcher's `default:` arm
  assumes the gate already refused anything unimplemented. Measured on a branch
  that added the name early (#5701): the gate stopped refusing, the matcher fell
  through, and `match({ name: 'zzz' }, { name: { $icontains: 'acme' } })` returned
  `true` — the predicate silently dropped, every row matched. A dropped predicate
  does not narrow a query, it WIDENS it, and on an RLS read scope that is a
  permission bypass rather than a degraded feature (#3948). So the word list
  travels with the evaluators or not at all.

  **What now answers it**, all folding the same domain: `driver-memory` (query
  path, reference matcher, and the analytics/cube face), `driver-mongodb`,
  `objectql`'s `having`, `@objectstack/formula`'s `matchesFilterCondition` (the RLS
  write-side `check`), and `service-analytics`' three SQL compilers (the RLS
  lowering, the native-SQL strategy, and the `/analytics/sql` echo).

  **The fold is ASCII-only, and that is the contract, not an implementation
  detail** (#4706 Q1 = A). `$icontains: 'café'` does not match `CAFÉ`. Every face
  reads one shared definition — `foldAsciiCase` /
  `asciiCaseInsensitiveContains` / `asciiCaseInsensitiveRegexSource`, new exports
  on `@objectstack/spec/data` — because the two obvious per-package spellings are
  both wrong in the same direction: `toLowerCase()` folds the whole Unicode range,
  and so does a `RegExp` built with the `i` flag. SQLite folds ASCII only and three
  of the five drivers are SQLite underneath, so a Unicode fold on a JS face would
  re-open exactly the divergence the ruling closed. The pattern-binding faces
  (mingo, mongo) therefore emit one `[Aa]` character class per ASCII letter and
  pass NO flags; mongo's `$icontains` is the one arm in its family that does not
  set `$options: 'i'`.

  The comparand keeps the rules its SQL twin has: matched LITERALLY (`%`, `_` and
  regex metacharacters are ordinary characters), and refused when empty or
  non-string — an empty comparand matches every row, which is a predicate that
  constrains nothing.

  **User-visible effect.** A filter using `$icontains` now behaves the same on the
  in-memory double and on SQL, so an app whose tests run on one and whose
  production runs the other stops getting two answers from one filter. Downstream,
  #5814 (better-auth `Where.mode: 'insensitive'`) no longer hits a 400 on the
  memory double.

  Not changed, and still tracked: the `$contains` family still folds Unicode on
  `driver-memory`'s query path and `driver-mongodb` (#6682) — both remain DEBT rows
  in `scripts/check-driver-conformance.mjs`, now naming one open requirement each
  instead of two. `formula`'s unknown-operator posture stays a silent, fail-closed
  `false` (it governs a write-side check, where an unevaluable condition denies
  rather than widens); the decision and its limits are documented on
  `matches-filter.ts`, and no operator the spec DECLARES is answered that way any
  more.

- 6965160: feat(lint): view/page 可见性谓词的裸标识符构建期闸门 —— 坏谓词发不出去(#6128)

  新增 **error 级** 规则 `visibility-bare-identifier`:view/page 的可见性谓词
  (`visibleWhen` 及其两个已弃用别名 `visibleOn` / `visibility`)里引用了任何绑定根都解析不到的
  顶层标识符时,`os validate` / `os build` / `os lint` 一律拒收。写成 `status == 'active'`
  而不是 `record.status == 'active'` 的谓词,从此发不出去。

  按 #5149 维护者 2026-08-06 裁决的构建期半边落地(运行时 warn-once 半边已由 objectui#3541 合入)。
  本仓传统的准确表述是:fail-open 或 fail-closed 都可以裁,**静默不可以**。谓词失败仍然 fail-open
  (已发货 app 行为不变),但坏谓词不再能进入产物。

  **为什么现有两道闸都放行**(#5149 Repro 1 实测,已写进规则注释,防后人误并):
  ADR-0032 的标识符闸(`validate-expressions.ts`)解析 record 作用域的裸引用,但它的遍历只覆盖
  objects / flows / actions / sharingRules / hooks,**从不走 views 与 pages**;ADR-0089 D3b
  只判**有根**的谓词根错层(runtime 面的 `data.`、metadata 面的 `record.`),**无根**的谓词两边都不匹配。
  两闸之间正好漏掉「作者按文档示例写了裸字段名 → 谓词永远解析失败 → 控制台 fail-open 静默显示」。

  **判定由两个既有 oracle 合成,本包不自建 CEL 环境**(#4812 的教训):声明性判定取
  `@objectstack/formula` 的 `firstUndeclaredReference`(即 `validateExpression` 给 record 作用域
  裸引用定罪的同一个严格环境),AST 取规范入口 `parseCelToAst`。AST 先收集所有处于**接收者位置**
  的标识符(`a.b` / `a?.b` / `a['b']` / `a.exists(…)`)并在检查前声明它们,于是只剩「当作裸值引用」
  的标识符会被判 —— 未知**根**(`my_record.x`)交还给 ADR-0089 D3b,不在本规则射程内。

  **与 #4953(全量 vs 稀疏绑定)的边界**:#4953 实测同一求值器在两种绑定下语义相反
  (`has(record.a)` 全量 true / 稀疏 false;`record.a != null` 全量 false / 稀疏 FAULT)。本规则
  **按构造与该分叉无关** —— 它从不追问某个 KEY 在已绑定的根上是否存在,只追问标识符有没有根,
  而无根标识符在两种绑定下都解析不到。`has(record.x)` / `record.x != null` 等守卫写法在本闸门下
  一律绿,无论 #4953 最终怎么裁;已加测试钉住这条边界。

  **遍历按实测修正,否则规则生来即死**:`os build` 跑 `examples/app-showcase` 得到的唯一一条
  view 表单谓词落在 `views[0].formViews.edit.sections[0].fields[6].visibleWhen` —— 运行时 app 形状下
  `views[]` 条目是**视图容器**(`ViewSchema` 声明的自有键就是 `list` / `form` / `listViews` /
  `formViews`),`sections` 在下一层。原遍历只读 `views[].sections`,在这份 stack 上报告「干净」。
  现在覆盖容器的 `form` 与每个 `formViews.<key>`,以及仍然直接携带 `sections` 的 `defineForm` 形状;
  pages 改走共享的 `walkPageComponents`(regions、slotted 页的 `slots`、以及 `properties` 里的
  `page:tabs` / `page:accordion` / `page:card` 子树都随之覆盖,source-authored 页按其既有语义跳过)。
  `objects[].views` 明确不读 —— 该键已被 schema 立碑拒绝,读它只会造出一条永不触发的幽灵检查。
  两条既有 ADR-0089 D3b advisory 随遍历一并变得真正可达。

  注册表 tier `advisory` → `gating`(#5762 的先例):tier 声明并非自述,
  `authoring-rule-wiring.test.ts` 会读规则源码核对。

  已知盲点(已钉测试、方向安全):字段名与 CEL **类型名**相同时(`type` / `int` / `string` / `list`
  / `map` / `timestamp` …)不判 —— CEL 自身声明这些标识符,`type == 'grid'` 到检查器那里是类型
  overload 错误而非未知变量;改读 overload 消息会误杀合法的 `type(record.x) == string`。语法不通过
  的谓词同样不判,交还给拥有该判定的闸门。两者都是漏判,永远不会变成误红。

  仓内 `app-todo` / `app-crm` / `app-showcase` 三个示例 `os validate` 全部通过、零 visibility finding,
  无需修改任何示例内容。

  `@objectstack/formula` 侧:公开导出 `firstUndeclaredReference`(理由与既有的
  `collectCelRootIdentifiers` 一致 —— 绑定根集合不同的消费方需要的是同一个答案,替代方案是在消费方
  自建严格 `Environment`,而那正是 #4812 从本包消费方手里拿掉的私有前端)。

### Patch Changes

- b230e5e: fix(formula): `classifyError` grades a CEL fault by error class + code, never by the message (#6223)

  `EvalResult.error.kind` is author-facing — `@objectstack/objectql`'s `cel-fault`
  puts it in front of the author as `` `${kind}: ${first line}` `` and
  `packages/rest` re-emits it as the HTTP body's `reason`. cel-js embeds the
  author's own **source line** in `message` (`formatErrorWithHighlight`), so a
  classifier that regex-matches that text is matching text the author writes.
  PR #6202 closed the `ParseError` arm this way and left `type` / `runtime` on the
  keyword table pending a per-code audit. This is that audit, and its verdict is
  that the table goes entirely.

  Measured on cel-js 8.0.0 — one `no such overload` **evaluation** fault, four
  field names, three wrong answers:

  ```text
  record.status        > 1  ->  runtime   (right)
  record.parse_status  > 1  ->  parse     (wrong)
  record.syntax_mode   > 1  ->  parse     (wrong)
  record.type_code     > 1  ->  type      (wrong)
  ```

  `parse` is the inverse of the #6133 misdirection: the expression is
  syntactically perfect and failed on the data, and the author was told to go fix
  an expression that has nothing wrong with it.

  `classifyError` now reads only structured contract:

  - `ParseError` -> `bounds` when `code === 'limit_exceeded'`, else `parse`
    (unchanged, from #6202);
  - `EvaluationError` -> `type` for the one declaration-class code
    (`unknown_variable`, the root identifier is not bound in this scope at all),
    else `runtime`;
  - anything that is not a cel-js error -> `runtime`.

  Two findings from the audit worth recording. First, the residual keyword arm was
  **not** dormant: `matches()` is an ObjectStack stdlib binding over `new
RegExp(...)`, so an uncompilable pattern escapes as a native `SyntaxError` whose
  message echoes the pattern — and the pattern can come off the row, not just out
  of the source. `matches(record.name, record.re)` with `re = "type("` was
  graded `type`; with `"Exceeded maxAstNodes("` it was graded `bounds`. A data
  value was picking the error kind. Second, there is deliberately no `TypeError`
  arm: cel-js raises that class only from its non-evaluating `TypeChecker`, which
  runs only inside `Environment#check`, and that method catches it and _returns_
  `{ valid: false, error }`. The check-time `TypeError -> type` mapping already
  lives in `celEngine.compile`, which reads that object.

  Six evaluate-time codes change verdict from `type` to `runtime`
  (`int_conversion_error`, `uint_conversion_error`, `double_conversion_error`,
  `invalid_index_type`, `heterogeneous_list_element`,
  `invalid_comprehension_range`). Each is a fault decided against the row; every
  one of them was graded `type` only because cel-js happens to use the word "type"
  in its prose (`int() type error: cannot convert to int`). Every evaluate-time
  code the engine can reach now has a fixture pinning its `kind`.

- 5d24f4b: fix(formula): the ADR-0032 §1c retry rewrites only the operands that faulted (#7098)

  **A CEL expression could return a silently wrong boolean.** No fault, no log
  line, no failing test — `{ ok: true }` with the wrong answer. If you have
  compound CEL that mixes a numeric comparison with a string equality over
  string-serialized fields, read the "which expressions change answer" list below:
  those expressions answer differently after this fix, and the new answer is the
  right one.

  ## What was wrong

  When a comparison faults on a string-serialized numeric or date field
  (`record.rating >= 4` where `rating` reads back as `"5.0"` — #1530 / #1534),
  ADR-0032 §1c hydrates and retries. The retry hydrated the **entire scope** and
  re-ran the **entire expression**, justified by a docblock claim that it

  > can never change a comparison that already evaluated cleanly — it only rescues
  > one that already faulted.

  That claim was false, and it was load-bearing: it was the stated reason the
  hydration was allowed to be unconditional and scope-wide. The retry knows only
  that the _whole expression_ faulted, not that each sub-comparison did. So:

  ```text
  record.n >= 4 && record.s == "5.0"    with { n: "7", s: "5.0" }
    before -> { ok: true, value: false }        after -> { ok: true, value: true }
  ```

  `record.n >= 4` faults and is correctly rescued. But `record.s` was hydrated to
  the number `5` as well, so the author's deliberate string equality — **true**
  when it was evaluated the first time — became `5 == "5.0"`, which CEL answers
  `false` across types. The expression returned `false`, and nothing reported that
  a clean answer had been overruled.

  ## Which expressions change answer

  Only expressions that **already reached the §1c retry** — i.e. some operand
  faulted `no such overload`. Everything that evaluates without faulting is
  untouched. Within that set, an expression changes answer when it also contains:

  - **a string equality / inequality on a numeric-looking or ISO-date field** —
    `record.n >= 4 && record.s == "5.0"`, and the `!=` and ternary forms. Now
    answers on the string the author wrote.
  - **a string membership test** — `record.s in ["5.0", "x"]`.
  - **the same field compared as a number in one place and as a string in
    another** — `record.n >= 4 && record.n == "7"`. Both answers are now correct
    at once; previously the second was collateral damage from the first.
  - **a numeric-looking string the expression RETURNS rather than compares** —
    `record.n >= 4 ? record.s : "none"` returned the number `5`; it now returns
    the string `"5.0"`. A `Field.formula` of type text was storing a different
    value than the record held.

  One class becomes a **loud fault where it used to be silently rescued**: an
  operand whose value the rewrite cannot read before deciding — bound by a
  comprehension (`record.items.exists(i, i.price > 100)`), or behind a computed
  index. That is the deliberate trade of this fix. Rescuing an operand we cannot
  prove faulted is exactly the defect being closed, so those report the original
  `no such overload` instead of guessing. The reported error is unchanged in shape
  and message.

  ## What replaces it

  The coercion is now **per operand position** — the same discipline
  `rewriteTemporalEquality` already documents ("no field-wide trade-off"), one
  step stricter. The scope is never rewritten; the faulting operand is wrapped in
  `double(…)` or `date(…)` in place. An operand is rewritten only when all three
  hold, which makes the docblock's guarantee true by construction rather than by
  assertion:

  1. the operator **raises** on a string-versus-number/Timestamp pair instead of
     answering one, so the comparison cannot have produced an answer;
  2. the counterpart is a number or a Timestamp **in this scope**, read off the
     values in hand rather than off a static type (every field is `dyn` under
     `unlistedVariablesAreDyn`);
  3. the operand's own value is a §1c serialization artifact — an entirely-numeric
     string or an ISO-8601 date. A zip like `"02134"`, or free text, still faults
     loudly.

  Measured per operator on cel-js 8.0.0 and pinned in the new tests: `<` `<=` `>`
  `>=` `+` `-` `*` `/` `%` **fault** on a mixed pair and are eligible. `==`, `!=`
  and `in` **answer** across types — CEL equality is total — so they already had
  an answer and are never rewritten. That measurement is the root of the defect:
  the string equality above never faulted at all.

  `Field.date` strings not matching a Timestamp under `==` remains owned by
  `rewriteTemporalEquality`, which wraps them statically on the clean path, where
  both sides are known from the source instead of inferred from an unrelated
  conjunct's fault.

  ## Reach

  `celEngine.evaluate` — the only home of this retry — does **not** reach RLS.
  Row-level security compiles its `using` / `check` predicates through
  `compileCelToFilter` (SQL pushdown) and `matchesFilterCondition` (write-side
  post-image), and declared sharing rules do the same; neither calls this
  evaluator. No access-control decision could be inverted by this.

  It does reach write-gating decisions, which is why the behaviour was not
  acceptable as documented: validation-rule predicates and `when` conditionals,
  `readonlyWhen`, hook `condition`s, automation/flow conditions, and formula
  fields and default values. A validation rule is **fail-closed** on a fault
  (#4649) — but a silently flipped boolean is not a fault, so a rule that should
  have rejected a write instead read as "not violated" and let it through.

- 29b94ed: fix(formula): the CEL hydration retry arms off cel-js's structured code, not the phrase "no such overload" (#6679)

  `celEngine.evaluate` catches a fault and asks `isNumericOverloadError` whether to
  hydrate string-serialized numeric / date fields and re-evaluate once — the
  ADR-0032 §1c accommodation for `Field.rating` → `"5.0"` and `Field.date` →
  `"2026-06-20"` (#1530, #1534). That question was answered by
  `/no such overload/i.test(err.message)`: the last message-text read in
  `cel-engine.ts` that armed behaviour after #6223 / PR #6677 closed the same hole
  in `classifyError`. It now reads
  `err instanceof EvaluationError && err.code === 'no_such_overload'`, the same
  class-and-code rule `classifyCelFault` already follows one function below.

  The phrase was reachable from a **native** throw, not only from cel-js. Our
  `matches()` stdlib binding is `new RegExp(String(re)).test(...)`, so an
  uncompilable pattern escapes cel-js unwrapped as a `SyntaxError` echoing the
  pattern verbatim — `Invalid regular expression: /no such overload(/` — which
  matched. The pattern can be written in the source or read off a row via
  `matches(record.name, record.re)`.

  The filing recorded this as observation-class, expecting the consequence to be
  nil because the retry re-throws the original error. Measuring it for the fix
  found one case where it is not nil, so this ships as a fix rather than a
  tolerance removal: when hydration lets the expression short-circuit around the
  throwing call, the spurious retry **succeeds** and returns a value where the
  fault was the right answer.

  ```text
  record.s == "5.0" ? matches(record.name, "no such overload(") : false
    { s: "5.0", name: "x" }   ->  was: ok, false        now: the regex fault
  record.s == "5.0" ? matches(record.name, "(") : false
    { s: "5.0", name: "x" }   ->  the regex fault       (unchanged)
  ```

  Evaluation 1 takes the `matches(...)` branch and throws natively; the phrase
  armed the retry; hydration made `record.s` the number `5`, so `5 == "5.0"` went
  false, the ternary took the other branch, and `matches` was never called. Two
  expressions that differ only in whether a regex literal happens to contain the
  phrase no longer disagree about whether they fault.

  The behaviour change is one-directional and narrow. A genuine cel-js
  `no_such_overload` still arms the retry and every §1c hydration behaves exactly
  as before; only a native throw whose message merely contains the phrase stops
  arming it. Faults are otherwise unchanged — a native throw carries no cel-js
  contract, so it is still reported as `runtime` (#6223).

- 07c68b0: fix(formula): 括号/引号/转义等 parse 期错误不再被误报为 `runtime`

  `celEngine` 的错误分类此前完全靠**错误文案关键词**判定,而 cel-js 8.0.0 的 parse 期错误有约 19 种措辞,只有 3 种含 `parse` / `unexpected` / `syntax`。其余整类 —— 最典型的括号/方括号/花括号不配对(`Expected RPAREN, got EOF`)、未闭合字符串、非法转义、保留字 —— 全部落到默认值 `runtime`。

  `kind` 不是内部字段:它被原样拼进作者可见的写入拒绝文案(`@objectstack/objectql` 的 `rule-validator` / `cel-fault`)与 REST 错误响应体的 `reason`。少写一个右括号的校验规则,作者读到的是 `(runtime: …)` —— 指向数据与求值期,而真正该改的是表达式本身,与 ADR-0032 D1d 的"消息面向自纠"相悖。

  改为按 cel-js 抛出的**错误类**判定:`ParseError` → `parse`(其中 `code: 'limit_exceeded'` 仍 → `bounds`,cel-js 的越界一律由 parser 抛出)。这一层不再读文案,因此也修掉了关键词方案无法修的一格:cel-js 会把**作者自己的源码行**嵌进 `message`(`formatErrorWithHighlight`),于是字段名能决定错误分类 —— 实测 `((record.type_id)` 这条普通的括号不配对,此前被判为 `type`,只因回显的源码里含子串 "type"。

  `type` / `runtime` 两支暂仍走原关键词表:cel-js 的 `TypeChecker` 按**阶段**而非按故障选择错误类(`isEvaluating ? evaluationError : typeError`),同一个 `unknown_variable` 在 check 期是 `TypeError`、在 eval 期是 `EvaluationError`,整体结构化会改变这些既有判定。审计见 #6133。

  kind 词表本身(`parse` / `type` / `runtime` / `bounds` / `dialect`)未变,消费方未改。

- e9b5265: fix(formula,lint): `current_user` becomes a declared root, and its field-level rejection becomes a real rule (#6290)

  `@objectstack/formula` told two stories about one root. `introspectScope` handed
  `current_user` to authors as a legal namespace and `checkRoleCatalog`'s four
  position-membership regexes all lead with it — both correct, because ADR-0068 D1
  makes `current_user` THE canonical spelling and `buildScope` really does mount
  the same `EvalUser` under it. Only `cel-engine.ts`'s `SCOPE_ROOTS` disagreed, so
  the strict environment read the blessed spelling as a BARE FIELD REFERENCE while
  its two aliases (`user`, `ctx`) passed unremarked.

  Three things change.

  **1. `SCOPE_ROOTS` declares `current_user`.** That list is a "never faults"
  baseline, not a per-surface contract, and it now advertises exactly what the
  package advertises elsewhere. A new pin asserts the property directly: every
  root `introspectScope` reports must resolve in the strict env.

  **2. The wrong prescription is gone.** Because the rejection used to fall out of
  the baseline's omission, the author got the GENERIC bare-field diagnostic —
  "Write `record.current_user`". That shape binds on no layer of the platform, so
  an author who followed the message ended up with something strictly worse than
  what they started with, still silent. The field-level verdict now comes from a
  rule of its own in `@objectstack/lint`, which names the real failure (unbound ⇒
  fault ⇒ visibility falls back to `true` ⇒ the field a `current_user` test was
  meant to hide stays visible for everyone, #6146) and prescribes surfaces that
  exist: move the predicate to the option's own `visibleWhen`, declare field-level
  security on a permission set (`fields: { '<object>.<field>': { readable: false } }`),
  or rewrite it against `record`. It covers `visibleWhen`, `readonlyWhen` and
  `requiredWhen`, which share the one evaluator.

  **3. Per-option `visibleWhen` is validated at all.** `validate-expressions.ts`
  walked field-level conditional rules and stopped there, so `SelectOption.visibleWhen`
  — an authorable CEL slot the client filters on AND the server enforces — reached
  compile, validate and run time checked by nobody. A bare field reference, a
  reference to a field that does not exist, a syntax error or a template-dialect
  predicate in an option all shipped in silence, and the option simply never
  offered itself. Options are now walked, located by option value, on the same
  `record` scope as their host field.

  The two surfaces deliberately give opposite verdicts on `current_user`, because
  their evaluators differ: field-level rules go through `evalFieldPredicate`
  (`record` + `previous` + `parent`, never a user), options through
  `resolveCascadingOptions` against the host's predicate scope, which does bind it
  (ADR-0068 / objectui#2284). The showcase's role-gated option
  (`'admin' in current_user.positions`) had never met this rule before and is now
  pinned as the legal usage it is.

  Sweep: `objectstack validate` is clean on all three example apps
  (`app-showcase`, `app-crm`, `app-todo`) with the option walk active — zero new
  findings, including the showcase object that carries both a record-scoped
  cascade and the role-gated option.

- d5e9f6e: 字段级 `*When` 的未绑定根检查:黑名单翻成白名单,并把因果句按槽位分档

  同一段诊断上的两条**正交**分档轴,一次设计通过 —— 分开做会把这段文案写两遍,
  且第二遍推翻第一遍。

  ## 轴一:根集合从 3 项黑名单翻成 3 项白名单(#6713)

  字段级 `visibleWhen` / `readonlyWhen` / `requiredWhen` 实测只绑 `record`、
  `previous`、`parent` 三个根,三处独立证据一致:服务端
  `rule-validator.ts` 的两处绑定(`readonlyWhen` 绑
  `{ record, previous, extra: { parent } }`,`requiredWhen` 绑
  `{ record, previous, ...parentScope }`);客户端 `evalFieldPredicate` 绑
  `record` + `previous` + 调用方 `scope`,而 objectui 全部五个字段级调用点
  (`form.tsx` ×3、`WizardForm.tsx`、`GridField.tsx`)传的 `scope` 只可能是
  `undefined` 或 `{ parent }`;作者端 objectui 的
  `FIELD_RULE_ROOTS = ['record', 'previous', 'parent']`,注释明写 "nothing else"。

  而检查此前是一张**黑名单** —— #6584 一项、#6711 三项
  (`current_user` / `user` / `ctx`)。黑名单在这个面上结构性地追不上
  `SCOPE_ROOTS`:每新增一个根都要有人记得抄过来(`current_user` 自己就是 #6290
  加进去、#6584 才被发现的)。实测有 **21 个根**落在这条缝里,它们同样未绑定、
  同样 fault、而且同样**静默** —— 都在 `SCOPE_ROOTS` 里,所以裸引用检查也从不
  报它们。其中两个是高可信度的作者笔误而非理论成员:

  - `os.user.id` —— ADR-0068 D1 的**第四种**用户拼写(`buildScope` 把同一个
    `EvalUser` 挂在 `current_user` / `user` / `ctx.user` / `os.user` 下),#6711
    收了三种,`os` 这一支没收;
  - `data.status == 'x'` —— `data` 是**元数据表单**里同一个 `visibleWhen` 键的
    **合法**根(`view.zod.ts`:"Root: `record` … in runtime forms, or `data` in
    metadata forms"),两种表单同一个键名、不同的根。

  判定改为 `SCOPE_ROOTS` 成员减去白名单,列表直接从 `@objectstack/formula` 取,
  不在消费端重述 —— 因此 `SCOPE_ROOTS` 将来新增的成员自动被覆盖。

  处方随之**按根分档**:用户根(`current_user` / `user` / `ctx` / `os`)保留原有
  的选项级 `visibleWhen` 与权限集 FLS 两条用户向处方;`data` 给出元数据表单 vs
  运行期表单的解释;其余根给出通用的「改写成 `record` 谓词」。此前只有用户向处方,
  对写了 `data.type == 'select'` 的作者是答非所问。

  ## 轴二:因果句按槽位分档(#6716)

  三个槽位此前共用一句「falls back to VISIBLE … showing for everyone」,而这句话
  只对其中一个精确。三格全部**实测**,每格量了两端:

  - **`visibleWhen` —— 仅客户端、fail-OPEN,原文案正确。** 服务端根本不评估字段级
    `visibleWhen`(`ConditionalFieldDef` 无此成员,`fieldsNeedPrior` 只看
    `requiredWhen || readonlyWhen ||` 选项可见性),唯一裁决来自渲染端,
    `resolveFieldRuleState` 对可见性传 `fallback: true`。
  - **`readonlyWhen` —— 两端方向相反,服务端说了算,原文案是反的。** 服务端
    `isReadonlyWhenLocked` 命中 `unknownVariableOf` 后返回 `true`(#4889 的
    carve-out,其触发条件正是未绑定根这一类),`stripReadonlyWhenFields` 随即把该
    字段从 payload 中删除;客户端 `resolveFieldRuleState` 传 `fallback: false`,
    表单仍渲染为可编辑。按 ADR-0057 D10(server enforces, client is courtesy)以
    服务端为准:作者改了字段、保存报成功、值静默不落库。原文案告诉作者「对所有人
    可见」—— 失败方向与排障方向都相反。
  - **`requiredWhen` —— 两端都 fail-OPEN,且与可见性无关。** 服务端记日志后
    `continue`(#4977 明确没有采用 #4889 的 carve-out),客户端 `fallback: false`,
    两端都不强制,记录带着空字段保存成功。原文案在这里不只是不精确,而是说错了
    字段的哪个属性。

  `conditionalRequired` 在 `FieldSchema` 里是 `retiredKey`(按名字拒绝),解析后的
  编译路径上该分支是惰性的,因此给它一条与槽位无关的通用句,而不是编造第四格测量。

  ## `@objectstack/formula`

  `SCOPE_ROOTS` 改为公开导出。一个绑定**封闭**根集合的面,必须能说出它**不**绑定
  的那些根,而那个补集就是 `SCOPE_ROOTS` 减去该面自己的白名单;消费端手抄的列表
  追不上这张表。注意它不能用 `firstUndeclaredReference` 替代:严格环境同时声明了
  CEL 的**类型名**,`type(record.x) == string` 里的 `string` 会被判成「能解析的根」
  —— 实测按可解析性判定会误杀这条合法谓词(1 例),按 `SCOPE_ROOTS` 成员判定不会。

- cafec0a: `validateExpression`: give an over-budget expression a SIZE prescription instead of the dialect trailer (#7073)

  ADR-0032's shared validator appended one trailer to every `celEngine.compile`
  refusal, byte for byte — `— predicates are bare CEL (e.g. \`record.rating >= 4\`).`
  That sentence is right for a dialect mistake and actively wrong for a **bounds**
  refusal: an 80-clause conjunction is already bare CEL, perfectly good syntax, and
  merely over the platform's parse budget. The author was told to change the one
  thing that was never wrong; an AI author, which obeys the last sentence it was
  handed, rewrites the dialect and regresses. Reported by #6833's measurement.

  The refusal is unchanged — same inputs refused, same `Exceeded maxAstNodes (256)`
  front half from cel-js. Only the prescription is now class-aware: a `bounds`
  verdict (read off the engine's own `error.kind`, with the exceeded bound named by
  `parseCelToAstWithReason`) produces

  > invalid CEL predicate: Exceeded maxAstNodes (256) … — this is valid CEL that
  > exceeds the `maxAstNodes` budget (limit 256) — a SIZE fault, not a dialect
  > mistake, so re-spelling the expression will not fix it. Shrink it (fewer
  > clauses, shallower nesting, fewer list elements), or precompute the heavy part
  > into a stored field and reference that field instead. …

  while a genuine dialect/syntax fault keeps the old trailer verbatim. Fixed once at
  the producer, so all ~10 expression slots benefit — build, metadata registration,
  lint's `validateStackExpressions`, and the `validate_expression` tool. The
  remedies are deliberately slot-generic: the slots' combination semantics differ,
  so PR #6831's RLS-specific "splitting the top-level `&&` widens the grant" is not
  portable and splitting is offered only with a caveat.

  Also documents, text-only, the completeness gap in `cel-pushdown-limits.ts`'s
  "nothing else needs to move at GA": a third lint gate (`validateStackExpressions`)
  covers the same `sharingRules[].condition` and is mode-agnostic, so during the
  rc grace window lint is stricter than the runtime — benign, tightening-direction,
  and self-healing at GA. No behaviour change there.

- Updated dependencies [3d5c090]
- Updated dependencies [e5bd768]
- Updated dependencies [e027b3e]
- Updated dependencies [c2429b0]
- Updated dependencies [445a0c2]
- Updated dependencies [f6609e6]
- Updated dependencies [a70358a]
- Updated dependencies [97e7e3c]
- Updated dependencies [8828b9e]
- Updated dependencies [53068c1]
- Updated dependencies [ee58392]
- Updated dependencies [f16e54e]
- Updated dependencies [06be54e]
- Updated dependencies [259459d]
- Updated dependencies [3f7f14e]
- Updated dependencies [6968885]
- Updated dependencies [eaed61f]
- Updated dependencies [debe2f6]
- Updated dependencies [97b0798]
- Updated dependencies [43a7a8d]
- Updated dependencies [73f69dc]
- Updated dependencies [04c56aa]
- Updated dependencies [b3efeb7]
- Updated dependencies [ddd075a]
- Updated dependencies [88154be]
- Updated dependencies [e8dc61e]
- Updated dependencies [2f3e793]
- Updated dependencies [d8e8d9c]
- Updated dependencies [94e749b]
- Updated dependencies [ea1d916]
- Updated dependencies [ae31a19]
- Updated dependencies [e0f300b]
- Updated dependencies [62b6a2f]
- Updated dependencies [5b4780b]
- Updated dependencies [a933452]
- Updated dependencies [8140915]
- Updated dependencies [7b48cf9]
- Updated dependencies [b5404f4]
- Updated dependencies [f764691]
- Updated dependencies [e120a5a]
- Updated dependencies [e650d67]
- Updated dependencies [04476e7]
- Updated dependencies [79228cd]
- Updated dependencies [b3363e9]
- Updated dependencies [2ef1807]
- Updated dependencies [d03fe25]
- Updated dependencies [2672f85]
- Updated dependencies [11066f6]
- Updated dependencies [916af17]
- Updated dependencies [84c86fb]
- Updated dependencies [2a2a9fb]
- Updated dependencies [a2e157c]
- Updated dependencies [95c4227]
- Updated dependencies [2a61116]
- Updated dependencies [d4df105]
- Updated dependencies [e2798fa]
- Updated dependencies [0fd8556]
- Updated dependencies [74155c7]
- Updated dependencies [6908830]
- Updated dependencies [8b06bba]
- Updated dependencies [4c54037]
- Updated dependencies [0f7157b]
- Updated dependencies [d9bef45]
- Updated dependencies [f549a0d]
- Updated dependencies [82da264]
- Updated dependencies [9b9b70f]
- Updated dependencies [f5a9bc2]
- Updated dependencies [881a3cc]
- Updated dependencies [ad6317b]
- Updated dependencies [8a88885]
- Updated dependencies [5f7669e]
- Updated dependencies [becbe53]
- Updated dependencies [b127c8b]
- Updated dependencies [a80302a]
- Updated dependencies [474f131]
- Updated dependencies [050cd82]
- Updated dependencies [4d552af]
- Updated dependencies [44d677c]
- Updated dependencies [c32944d]
- Updated dependencies [1dd780f]
- Updated dependencies [c8d6f6e]
- Updated dependencies [92a67f2]
- Updated dependencies [9136327]
- Updated dependencies [bf0ae99]
- Updated dependencies [cb3b6cd]
- Updated dependencies [73b7234]
- Updated dependencies [d2b97c3]
- Updated dependencies [59b794f]
- Updated dependencies [fc3a36a]
- Updated dependencies [69787f0]
- Updated dependencies [5d022a1]
- Updated dependencies [042b9ee]
- Updated dependencies [f549a0d]
- Updated dependencies [a36db28]
- Updated dependencies [3f8817a]
- Updated dependencies [a2443e3]
- Updated dependencies [e1554b1]
- Updated dependencies [4856789]
- Updated dependencies [c3f4916]
- Updated dependencies [33e0385]
- Updated dependencies [2205363]
- Updated dependencies [09fe58d]
- Updated dependencies [d0a5ceb]
- Updated dependencies [e18a162]
- Updated dependencies [d127ff0]
- Updated dependencies [9b86cf6]
- Updated dependencies [8825a06]
- Updated dependencies [5087ac6]
- Updated dependencies [2d1ddf0]
- Updated dependencies [354b00f]
- Updated dependencies [3de535b]
- Updated dependencies [fe2e15a]
- Updated dependencies [c6b6bb4]
- Updated dependencies [2f59da0]
- Updated dependencies [8ad609c]
- Updated dependencies [bbee302]
- Updated dependencies [08863dd]
- Updated dependencies [56664f5]
- Updated dependencies [31cbe90]
- Updated dependencies [90bbf25]
- Updated dependencies [eb91eba]
- Updated dependencies [42da73d]
- Updated dependencies [643b7c7]
- Updated dependencies [1a15893]
- Updated dependencies [b70e534]
- Updated dependencies [2233a85]
- Updated dependencies [62dd69a]
- Updated dependencies [e15e679]
- Updated dependencies [2ab1257]
- Updated dependencies [4cc4fb7]
- Updated dependencies [2c26040]
- Updated dependencies [f758cec]
- Updated dependencies [78f0be8]
- Updated dependencies [35f7fb4]
- Updated dependencies [a5302c7]
- Updated dependencies [7084313]
- Updated dependencies [0e043d8]
- Updated dependencies [dadd1ad]
- Updated dependencies [2f2e63c]
- Updated dependencies [486d526]
- Updated dependencies [89d7b35]
- Updated dependencies [85ec26d]
- Updated dependencies [f6476fc]
- Updated dependencies [4ac12ef]
- Updated dependencies [b88f5e8]
- Updated dependencies [42cc219]
- Updated dependencies [d7e0b42]
- Updated dependencies [3510e4a]
- Updated dependencies [aa4b90d]
- Updated dependencies [54299ca]
- Updated dependencies [dc61def]
- Updated dependencies [251e888]
- Updated dependencies [183b4c4]
- Updated dependencies [2fdb36e]
- Updated dependencies [20526f5]
- Updated dependencies [c5eef1d]
- Updated dependencies [e0f300b]
- Updated dependencies [761a0ba]
- Updated dependencies [be87153]
- Updated dependencies [60f0dd8]
- Updated dependencies [a87c5cd]
- Updated dependencies [a47f338]
- Updated dependencies [2598216]
- Updated dependencies [2c7e62d]
- Updated dependencies [eb7613c]
- Updated dependencies [ecc9110]
- Updated dependencies [f7bd4e2]
- Updated dependencies [361bd5b]
- Updated dependencies [1818998]
- Updated dependencies [09ee21c]
- Updated dependencies [f549a0d]
- Updated dependencies [3fc2e48]
- Updated dependencies [e8f435c]
- Updated dependencies [41610f6]
  - @objectstack/spec@17.0.0-rc.6

## 17.0.0-rc.5

### Patch Changes

- Updated dependencies [e8f8f6c]
- Updated dependencies [7f713b6]
- Updated dependencies [c960170]
- Updated dependencies [def5919]
- Updated dependencies [ce0cfe9]
- Updated dependencies [1363084]
  - @objectstack/spec@17.0.0-rc.5

## 17.0.0-rc.4

### Minor Changes

- 0f17114: fix(driver-sql,driver-memory,formula)!: `{ field: {} }` 一律拒收 —— 零个操作符的字段约束不再在四个后端有三个答案 (#5240)

  `{ a: {} }`(一个字段,后面跟零个操作符)是 `FilterConditionSchema` 今天**声明合法**的形状,
  而同一个 filter 在同仓四条路径上有三个答案:

  | 路径                                | 改前                                                                                            | 改后                          |
  | ----------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------- |
  | `driver-sql`,顶层 plain map         | 抛 `INVALID_FILTER`(#5041 的比较数闸门)                                                         | 抛 `INVALID_FILTER`(专用消息) |
  | `driver-sql`,`$and`/`$or`/`$not` 内 | 遍历零个操作符 → 不产出任何 SQL → **TRUE(匹配全表)**                                            | 抛 `INVALID_FILTER`           |
  | `driver-memory`                     | 实时路径经 mingo 变成「字段深等于空文档」;参考匹配器落到 `JSON.stringify` 结构相等 → 顺带 FALSE | 抛 `INVALID_FILTER`           |
  | `@objectstack/formula`              | `keys.length === 0` 显式 fail-closed → FALSE                                                    | 抛 `INVALID_FILTER`           |

  于是 `{ $or: [ { a: {} }, { b: 2 } ] }` 在 SQL 上编译成 `(b = 2)` —— 既不是「零约束即 TRUE」
  该给的全表,也不是两个 JS 后端给的 FALSE,而是**子句被 knex 连同空分组一起丢掉**的结果;
  而 `driver-sql` 自己内部就不自洽:同一个 `{ a: {} }` 写在顶层被响亮拒收,包进一层 `$or`
  就变成静默的 TRUE。

  维护者拍板取**拒收**(不取 TRUE、不取 FALSE):这个形状几乎必然是编写期事故 ——
  筛选器记下了字段却没记下操作符,或生成的元数据把操作符弄丢了 —— 让它在编写期就炸,
  好过在某个后端上安静地多返回或少返回几行。与 #5041 已在 driver-sql 顶层建立的先例一致,
  本次只是把同一道闸门补进组合子内部。四个后端(第四个是继承 `SqlDriver` 的
  `driver-sqlite-wasm`)现在给出同一个 `INVALID_FILTER` / 400,消息里指名出事的位置
  (如 `filter.$or[0].stage`)。

  **⚠️ 可观察的行为变更 —— RLS `check` 求值路径。** `@objectstack/formula` 的
  `matchesFilterCondition` 是 `plugin-security` 对 insert/update **后像**执行行级 `check`
  的那条路径(没有查询可下推,这个求值器就是执行本身)。它改为抛出后,落在 #4775
  「求不出值 = 该次操作失败」的既定姿态上。这不只是「拒绝得更响」——有一类结果直接翻转:

  | `check` 策略                                    | 改前                                  | 改后                     |
  | ----------------------------------------------- | ------------------------------------- | ------------------------ |
  | `{ a: {} }`                                     | FALSE → 写入被拒(403)                 | 抛出 → 该次写入失败(400) |
  | `{ $or: [ { a: {} }, { owner: '{userId}' } ] }` | FALSE 被另一析取项吸收 → 写入**放行** | 抛出 → 该次写入失败      |
  | `{ $not: { a: {} } }`                           | `!false` → 写入**放行**               | 抛出 → 该次写入失败      |

  后两行是**原本能成功、现在会失败**的写入。这是拍板的目的而非副作用:一条含
  `{ field: {} }` 的权限规则,是一条作者弄丢了操作符的规则,它的含义不该取决于四个后端里
  哪一个在求值。升级后请检查 `check`/`using` 策略里是否存在零操作符的字段约束——
  错误消息会指名位置。

  同一条改动也让 `@objectstack/driver-memory` 的两个过滤面(经 mingo 的实时查询路径,
  与跨后端一致性套件所用的 `memory-matcher` 参考匹配器)第一次对这个形状给出同一个答案。

  非空形状**逐字符不变**:普通比较、`$in`、`$or`/`$and` 组合、`$not` 的 #5146 NULL-safe 改写,
  编译出的 SQL 文本与匹配结果都与改前相同;`{}`(零个键的**节点**,#5134 的布尔单位元)
  与 `{ field: {} }` 是两个不同形状,前者的语义不受本次影响。

  注:本次收紧的是**实现**。`packages/spec` 的 `FilterConditionSchema` 仍然声明这个形状合法
  (非递归半边是 `z.record(z.string(), z.unknown())`),即实现现在比已声明的契约更严;
  契约收窄与 `FILTER_LOGIC_CASES` 补条归 spec 车道另行处理。

- 58f3220: 新增规范 parse-to-AST 入口 `parseCelToAst(source)`,并 re-export AST 节点类型 `CelAstNode`(#4812)。

  `parseCelToAst` 与 `compile` / `evaluate` / `collectCelRootIdentifiers` 共用同一条前端链路
  ——#3306 的 `rewriteNullableTernary` 重写、`DEFAULT_LIMITS` 边界、以及注册了 stdlib 的
  `unlistedVariablesAreDyn: true` 环境 —— 因此全仓对「什么能解析」只有一个答案。此前消费方
  若自建 `new Environment(...)`,拿到的是一份**不带 limits** 的答案:它会解析、并进而推理
  `compile()` 直接拒绝的表达式。

  `parseCelToAst` 只做 parse,不做 check(后者是 `compile()` 的职责):解析成功但类型检查失败的
  表达式(大量 `dyn` 操作数的谓词即是)仍然会拿到 AST。解析失败返回 `null` 而不抛错。

  `CelAstNode` 的 re-export 补上了一个既有缺口:`lowerCelAst` 一直接收 cel-js 的 `ASTNode`,
  而该类型从未导出,消费方只能越过本包直接依赖 `@marcbachmann/cel-js` —— 这正是第二个解析入口
  的成因。

- bf1edef: feat(formula,lint): wire ADR-0056 D4's RLS authoring gate, from the runtime's own predicate (#4983)

  `isSupportedRlsExpression` has carried the same docblock since ADR-0056 D4:
  "exposed so an authoring-time gate (`objectstack compile`) can REJECT a
  predicate the runtime would silently drop … A `false` here means 'this
  predicate will never enforce'." It had **no non-test consumer anywhere** — the
  function written to fix declared-but-never-read was itself declared and never
  read. This lands the consumer, in two steps that had to happen in this order.

  **1. `sqlPredicateToCel` and `isSupportedRlsExpression` move FROM
  `@objectstack/plugin-security` (`src/rls-compiler.ts`) TO `@objectstack/formula`
  (`src/rls-predicate.ts`), and are exported from its root.** Executable code
  unchanged — a change of address, not of behaviour; `plugin-security` now imports
  them from `@objectstack/formula` and keeps no copy, so there is still exactly
  one definition. No import path outside the two packages changes: neither symbol
  was ever exported from `@objectstack/plugin-security`'s entry point. The move is
  what makes step 2 possible at all — `@objectstack/lint` may depend on
  `@objectstack/spec` and never on a runtime, so with the predicate living in a
  runtime the gate's only other door was copying the SQL→CEL bridge, whose
  boundary conditions (quoted literals are never rewritten; canonical CEL passes
  through unchanged) _are_ the gate's red/green line. A fork drifting by one
  character rejects policies the runtime executes correctly — the false-positive
  direction, which is worse than the gap. ADR-0058 D1 asks for a single canonical
  shape gate; the bridge is part of that gate.

  **2. New `@objectstack/lint` rule `validateRlsPredicateEnforceability`,
  `error`, on all three authoring commands**, over
  `permissions[].rowLevelSecurity[].using` and `.check`:

  - **`rls-predicate-unenforceable`** — parses as CEL, outside the pushdown
    subset: a function call (`size(...)`, `has(...)`), arithmetic, a ternary, a
    cross-object path (`record.account.region`).
  - **`rls-predicate-unparseable`** — does not parse as CEL even after the legacy
    SQL bridge (`=` → `==`, `IN` → `in`): SQL `AND` / `OR` / `LIKE`, a subquery.
    Its own id because the fix is different — write CEL (`&&`, `||`), not a
    different shape.

  What the gate prevents, measured through `plugin-security` rather than inferred:
  `RLSCompiler` drops the policy and logs one request-time WARN. On the read path,
  when it is the only applicable policy, `compileFilter` returns the
  `RLS_DENY_FILTER` sentinel instead, which is AND-ed onto the where clause — so
  every select / update / delete on the object matches **zero rows**. On the
  ADR-0058 D4 write path the post-image `check` becomes that same sentinel, which
  no record satisfies, so every insert / update fails with `PermissionDeniedError`.
  The runtime fails closed, which is why this was survivable: the result is not a
  hole but a policy that reads as an authorization and behaves as a blanket
  refusal, with nothing at authoring time pointing at the line that caused it.

  Fix a flagged predicate by rewriting it inside the lowerable subset — `==` `!=`
  `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and
  `startsWith` / `endsWith` / `contains` over single-column field paths (ADR-0058
  D2), against a literal or a `current_user.*` value. Two specific migrations:
  `has(x)` / `size(x) > 0` → `x != null` (a function call is correct in an object
  _validation_ rule, which is interpreted, and wrong here, where the predicate is
  compiled to a filter); and a related record's field → denormalise it onto this
  object (formula/rollup) and test that column, since RLS cannot join (ADR-0055).

  Same construction as the sharing-rule gate (#4698): the rule does not model the
  consumer or grep for it — it calls `isSupportedRlsExpression`, the exact
  function `RLSCompiler.compileFilter` consults to decide whether a dropped policy
  earns its warning, so the two verdicts are one boolean by construction, pinned
  in both directions over a shared corpus. Measured before shipping: every RLS
  predicate declared anywhere in this repo — the `plugin-security` platform seeds,
  the examples, the dogfood fixtures, the authoring skill — is supported, so the
  gate turns nothing red that works today. Unlike the sharing-rule gate, CEL
  _syntax_ is reported here rather than deferred to `expression-invalid`:
  `validateStackExpressions` does not walk `rowLevelSecurity` at all, and could not
  judge this field correctly if it did, because `owner_id = current_user.id` is a
  CEL syntax error and a working RLS predicate at the same time.

### Patch Changes

- 07f1822: fix(formula): `matchesFilterCondition` 的 `$exists` 改读「有值」,与 `$null` 成严格互补

  **行为变更,影响 RLS 写侧 `check` 的判定。** `{ x: { $exists: true } }` 对
  `{ x: null }` 以前答 `true`(键存在),现在答 `false`(没有值)。

  `matchesFilterCondition` 是 RLS `check` 子句(insert/update 的 post-image)的求值器 ——
  写路径上没有查询可以下推,只能逐记录判定。它此前把 `$exists` 读成「键是否存在」
  (`actual !== undefined`),而 `driver-sql` 一直把同一个算子编译成 `IS NOT NULL`。
  于是同一条规则里的 `$exists`,写侧放行的记录读侧看不见。

  2026-08-06 裁定取「有值」,理由是另一种读法在最要紧的地方**无法兑现**:SQL 里列
  **就是** schema,一行不可能「缺一个键」,所以 `driver-sql` 除了 `IS NOT NULL` 别无
  可编译的东西。字段的存在性是 **schema** 的属性,不是**记录**的属性;spec 若声明
  「键是否存在」,就是在承诺两个后端永远交付不了的语义。因此 `driver-sql` 的发射器
  一字未动,移动的是本求值器。

  对齐之后 `$exists` 与 `$null` 在每个后端上都是严格互补:
  `$exists: true` ≡ `$null: false`,`$exists: false` ≡ `$null: true`。
  「键缺失」与「值为 null」在这里是同一个事实 —— 这也正是 `getPath` 对两者本来就
  返回同一个 `undefined` 的原因。

  `$ne` / `$nin` / `$notContains` / `$null` 四个算子本来就是本次裁定的目标语义,
  一字未改。

- Updated dependencies [9fe9c1d]
- Updated dependencies [d4e0809]
- Updated dependencies [f724f69]
- Updated dependencies [28ad90e]
- Updated dependencies [f8644c7]
- Updated dependencies [306ca50]
- Updated dependencies [978fed2]
- Updated dependencies [cfc293f]
- Updated dependencies [de70b42]
- Updated dependencies [fb3d99b]
- Updated dependencies [cdfbee2]
- Updated dependencies [29c6c9d]
- Updated dependencies [d21c001]
- Updated dependencies [f1cc3a3]
- Updated dependencies [ddc2527]
- Updated dependencies [553a47f]
- Updated dependencies [a3a884d]
- Updated dependencies [cfed092]
- Updated dependencies [2e284b2]
- Updated dependencies [1b49eaf]
- Updated dependencies [0161c7f]
- Updated dependencies [e900015]
- Updated dependencies [b5bdf48]
- Updated dependencies [a019e52]
- Updated dependencies [64fc6d5]
- Updated dependencies [947d4f9]
- Updated dependencies [eaaf03c]
- Updated dependencies [d17df80]
- Updated dependencies [7d0e7b5]
- Updated dependencies [6513c17]
- Updated dependencies [c142ced]
- Updated dependencies [eda599e]
- Updated dependencies [c001422]
- Updated dependencies [77022a9]
- Updated dependencies [52760bf]
- Updated dependencies [5543020]
- Updated dependencies [880d343]
- Updated dependencies [6e82972]
- Updated dependencies [4615a18]
- Updated dependencies [7f62706]
- Updated dependencies [667fa44]
- Updated dependencies [37e38d1]
- Updated dependencies [1eb13a0]
- Updated dependencies [c52e608]
- Updated dependencies [4dfd002]
- Updated dependencies [77be690]
- Updated dependencies [811c30c]
- Updated dependencies [b49ccfd]
- Updated dependencies [85d95e7]
- Updated dependencies [168f60f]
- Updated dependencies [244ca86]
- Updated dependencies [546ab3c]
- Updated dependencies [0b51bb6]
- Updated dependencies [d9971d3]
- Updated dependencies [abeb375]
- Updated dependencies [ef4efa8]
- Updated dependencies [cbb6a5c]
- Updated dependencies [795b6e1]
- Updated dependencies [175d789]
- Updated dependencies [55dbbba]
- Updated dependencies [72c3c86]
- Updated dependencies [7f1a635]
- Updated dependencies [502564d]
- Updated dependencies [471839d]
- Updated dependencies [b508244]
- Updated dependencies [594508e]
- Updated dependencies [1c625ca]
- Updated dependencies [71f205d]
- Updated dependencies [414395b]
- Updated dependencies [26e1029]
- Updated dependencies [108ba8d]
- Updated dependencies [b4ad984]
- Updated dependencies [a9f32df]
- Updated dependencies [aeb9b27]
- Updated dependencies [7d27da0]
- Updated dependencies [089767f]
- Updated dependencies [e4c8b6c]
- Updated dependencies [acb10f6]
- Updated dependencies [1c3da1f]
- Updated dependencies [a34fd2e]
- Updated dependencies [889ae47]
- Updated dependencies [4f4c3fb]
- Updated dependencies [7adc841]
- Updated dependencies [4845f85]
- Updated dependencies [7b005b4]
- Updated dependencies [94f7b6a]
- Updated dependencies [5c94f83]
- Updated dependencies [73e576f]
- Updated dependencies [c5a5996]
- Updated dependencies [ae490ef]
- Updated dependencies [f61c8cf]
- Updated dependencies [e3ef52b]
- Updated dependencies [07f1822]
- Updated dependencies [04fab5e]
- Updated dependencies [efedd28]
- Updated dependencies [5278e11]
- Updated dependencies [23dba62]
- Updated dependencies [ba98e26]
- Updated dependencies [fc5f536]
- Updated dependencies [f8cfbb4]
- Updated dependencies [c89d18c]
- Updated dependencies [aac90a5]
- Updated dependencies [1e6ab15]
- Updated dependencies [c87ef70]
- Updated dependencies [3cb0618]
- Updated dependencies [32a0874]
- Updated dependencies [7055c22]
- Updated dependencies [785a748]
- Updated dependencies [3af0354]
- Updated dependencies [866ff16]
- Updated dependencies [5a85e67]
- Updated dependencies [c183a12]
- Updated dependencies [8064b07]
- Updated dependencies [4a56dbd]
- Updated dependencies [06df4fa]
  - @objectstack/spec@17.0.0-rc.4

## 17.0.0-rc.2

### Patch Changes

- Updated dependencies [430dcc2]
- Updated dependencies [e6ac4bd]
- Updated dependencies [80334c7]
- Updated dependencies [ce5242c]
- Updated dependencies [a7163ea]
- Updated dependencies [e6e9379]
- Updated dependencies [98877c9]
- Updated dependencies [98877c9]
- Updated dependencies [e6b1b69]
- Updated dependencies [ad047d2]
- Updated dependencies [2826d1e]
- Updated dependencies [5a84d41]
- Updated dependencies [20b1a9e]
- Updated dependencies [203a449]
- Updated dependencies [ac37fc6]
- Updated dependencies [4820f55]
- Updated dependencies [462d9c4]
- Updated dependencies [7d21581]
- Updated dependencies [f2445c9]
- Updated dependencies [23338c3]
- Updated dependencies [5b843fb]
- Updated dependencies [b4487aa]
- Updated dependencies [65ca83a]
- Updated dependencies [67bf2e2]
- Updated dependencies [c6d1cb4]
- Updated dependencies [36030ff]
- Updated dependencies [6117f7b]
- Updated dependencies [e533b0b]
- Updated dependencies [cdf4d9a]
- Updated dependencies [aee1806]
- Updated dependencies [c13350b]
- Updated dependencies [c13350b]
- Updated dependencies [9ca2d85]
- Updated dependencies [c13350b]
- Updated dependencies [891d345]
- Updated dependencies [a52e2ef]
- Updated dependencies [5293114]
- Updated dependencies [20bc357]
- Updated dependencies [5966c2a]
- Updated dependencies [2382580]
- Updated dependencies [d9fa683]
- Updated dependencies [3c7bcc0]
- Updated dependencies [4b6cac7]
- Updated dependencies [7631964]
- Updated dependencies [ac471a0]
- Updated dependencies [60ae58e]
- Updated dependencies [ce92674]
- Updated dependencies [9f601e8]
- Updated dependencies [51c5227]
- Updated dependencies [a4a85c8]
- Updated dependencies [07a4e26]
- Updated dependencies [ec975f1]
- Updated dependencies [eb4204b]
- Updated dependencies [4f13be2]
- Updated dependencies [61cc079]
- Updated dependencies [0e96e46]
- Updated dependencies [d52d4fe]
- Updated dependencies [742cebb]
- Updated dependencies [ce92674]
- Updated dependencies [cf2c9b7]
- Updated dependencies [0f9faa2]
- Updated dependencies [7cf42fe]
- Updated dependencies [5966c2a]
- Updated dependencies [f78dd83]
- Updated dependencies [a2cd18a]
- Updated dependencies [4638aaa]
- Updated dependencies [0222d3c]
- Updated dependencies [0a936ea]
- Updated dependencies [023c00b]
- Updated dependencies [155507e]
- Updated dependencies [7bba90b]
- Updated dependencies [7e05d8e]
- Updated dependencies [061406d]
- Updated dependencies [c1f344b]
- Updated dependencies [9c93465]
- Updated dependencies [ebb209c]
- Updated dependencies [63b33e6]
- Updated dependencies [2a44c1d]
- Updated dependencies [695cfbd]
- Updated dependencies [7445149]
- Updated dependencies [071d0dc]
- Updated dependencies [0848bea]
- Updated dependencies [d51bed2]
- Updated dependencies [b8b3c64]
- Updated dependencies [0c0fbd9]
- Updated dependencies [f3141d8]
- Updated dependencies [5a84d41]
- Updated dependencies [fd3013a]
- Updated dependencies [21676eb]
- Updated dependencies [e336549]
- Updated dependencies [d40f43a]
- Updated dependencies [e5e7ee0]
- Updated dependencies [a2ebea2]
- Updated dependencies [800bdb0]
- Updated dependencies [04f1182]
- Updated dependencies [5647006]
- Updated dependencies [38f7e4f]
- Updated dependencies [c57f3cf]
- Updated dependencies [97faca3]
- Updated dependencies [ad5fe25]
- Updated dependencies [ea90179]
- Updated dependencies [ce92674]
- Updated dependencies [5ef0b5b]
- Updated dependencies [48fbacb]
- Updated dependencies [355e951]
- Updated dependencies [dadb43f]
  - @objectstack/spec@17.0.0-rc.2

## 17.0.0-rc.1

### Minor Changes

- 4965bfa: Warn on flow-node `config` keys the node type does not declare (#4045).

  `FlowNodeSchema.config` is `z.record(z.unknown())`, so a misspelled or invented
  config key was accepted in total silence: `visibleIf` instead of `visibleWhen`
  registered cleanly, was never read, and the only symptom was a feature that quietly
  did not happen. That diagnostic vacuum is what made #3528 take three passes and two
  wrong diagnoses to resolve.

  `registerFlow` now compares each node's `config` against its descriptor's
  `configSchema` and warns on anything undeclared, located and with the declared set
  listed:

  ```
  [flow 'lead_conversion'] node 'screen_1' (screen): unknown config key `visibleIf`
    at config.fields[0].visibleIf — It is not declared by this node type's
    configSchema, so nothing reads it. Declared here: name, label, type, required,
    visibleWhen.
  ```

  The walk descends where the schema declares structure and **stops at free-form
  keyValue maps**, whose keys are author data (`filter: { status: 'stale' }`).
  Descending matters: the #3528 typo class lives _inside_ the `screen` field
  repeater, so a top-level-only comparison would miss the exact mistake this exists
  to catch.

  **Warn, never reject.** An undeclared key is an author typo, a key the executor
  genuinely reads that its hand-written `configSchema` never declared (`notify.source`
  was exactly this), or dead config. Only 4 of the 13 schema-carrying builtins have
  been audited for the second population, so hard-failing would gamble on the other
  nine. Tightening to an error is a later, per-key decision once this warning has
  measured the real distribution. Nothing about the published `configSchema` changes,
  so no consumer sees a different shape.

  `@objectstack/formula` now exports `nearestName`, the edit-distance helper already
  used for unknown-field and unknown-role suggestions, so "did you mean?"
  diagnostics share one threshold. It is deliberately a bonus rather than the
  mechanism — `visibleIf` → `visibleWhen` is distance 4 against a threshold of 3, so
  the declared set is always listed instead of only as a fallback.

  Also fixes the first real finding from the new check: `showcase_inquiry_purge`'s
  `get_record` node carried `mode: 'records'`, which no executor reads, with a comment
  crediting it for behaviour that `limit > 1` actually produces.

### Patch Changes

- 2af1988: fix(formula,spec,core): the RLS write-side `check` evaluator honours calendar-day upper bounds (ADR-0053 D-D)

  `@objectstack/formula`'s `matchesFilterCondition` — the evaluator behind RLS
  write-side `check` policies (ADR-0058 D4) — compared a bare `YYYY-MM-DD` `$lte`
  bound literally. On a `datetime` post-image that meant a policy of the shape
  `{ signed_on: { $lte: '{today}' } }` **denied every write made after 00:00**:
  the write-side twin of the read-side data loss #3777 fixed, and the last of the
  platform's filter backends that disagreed about what a bare day means as a
  bound.

  `$lte` and a `$between` max now evaluate half-open against the next calendar
  day, matching the SQL compiler, the memory and mongo drivers, and the analytics
  preview evaluator. Unchanged, per the same semantics table: full-ISO bounds keep
  exact-instant semantics, `$gte`/`$gt`/`$lt` keep their midnight anchoring, and a
  plain `YYYY-MM-DD` value compares identically (string ordering makes the two
  forms equivalent). The evaluator stays fail-closed on a null bound.

  **Where the rule now lives.** `nextUtcCalendarDay` moved from
  `@objectstack/core` to `@objectstack/spec/data` — beside `date-macros.zod.ts`,
  whose vocabulary it interprets. `formula` cannot depend on `core`, and a second
  copy of the rule is exactly the divergence #3777 catalogued; `spec` is the one
  package all six consumers already depend on, so this adds no dependency edge.

  No import changes are required: `@objectstack/core` re-exports the symbol, so
  existing `import { nextUtcCalendarDay } from '@objectstack/core'` keeps working.
  New code should prefer `@objectstack/spec/data`.

- 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)

  The AGENTS.md post-task checklist requires breaking changesets to carry their
  FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
  inside the npm package and is what an upgrading agent greps after the tombstone
  error." That delivery path was severed for 68 of the 69 publishable packages:
  npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
  older npm versions — not `CHANGELOG.md`, and the canonical
  `"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
  10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
  70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
  explicitly.

  The tombstone-error scenario is precisely the one where the repo is out of
  reach — the upgrading agent has `node_modules` and nothing else — so the
  migration text has to ride in the tarball. Every publishable package now
  declares `CHANGELOG.md` in `files`, and the canonical whitelist is
  `["dist", "README.md", "CHANGELOG.md"]`.

  The other half is the gate: `check:published-files` gains a fifth invariant,
  COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
  always-required lint job, so the next package cannot silently sever the path
  again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
  into the canonical set.

  Consumer-visible change: one more file per install (the package's changelog,
  e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
  promised.

- cc2de0e: chore(packaging): 20 packages stop publishing their sources, tests and build tooling (#4248)

  These 20 packages declared no `files` field, so npm fell back to packing the
  whole package directory. `npm pack --dry-run` on `@objectstack/plugin-webhooks`
  listed **21 files** — 15 under `src/`, three of them unit tests
  (`auto-enqueuer.test.ts`, `bootstrap-declared-webhooks.test.ts`, …), plus the
  build-time `scripts/i18n-extract.config.ts`. `dist/` lands on top of that at
  publish time rather than instead of it, so consumers were installing the
  TypeScript sources and the test suite alongside the artifact they asked for.

  Each now declares `"files": ["dist", "README.md"]`, matching the 29 packages
  that already did. Nothing a consumer imports moves: every `main` / `types` /
  `exports` target in all 20 already resolved inside `dist/`, which the new
  `check:published-files` guard verifies rather than assumes. The visible change
  is a smaller install and a smaller dependency-scanning surface — `npm pack` on
  `@objectstack/plugin-webhooks` now yields 2 files plus `dist/`.

  The other half of the fix is the gate. Half the packages declaring `files` and
  half not was the #3786 shape — a hand-copied convention with nothing enforcing
  it, where whoever forgets the line gets no signal at all. `check:published-files`
  (new, wired into the always-required `lint` job) holds every non-private
  workspace package to four invariants: `files` is **declared**; it is
  **sufficient** (covers every entry point, so tightening a whitelist cannot ship
  a package that fails to resolve); it is **minimal** (admits no test, test-harness
  config or build script); and anything beyond `dist` + `README.md` is
  **registered** with a reason, reconciled in both directions so a stale exemption
  is an error rather than dead text. `@objectstack/spec` is the one package with
  registered extras — its `.zod.ts` sources, JSON Schemas, liveness ledgers and
  `CHANGELOG.md` are product, not build input.

  This also closes an assumption #4206 was resting on. Excluding `<pkg>/scripts/**`
  from the docs-drift implementation test is sound only while no package publishes
  `scripts/` as runtime code; that held, but it held because someone read all three
  offenders by hand. It is now checked on every PR.

- e4c2dc8: Order temporal operands correctly when one side is a JS `Date` on the two
  type-blind filter backends (ADR-0053 D-A3 / #4191).

  `utcInstantMs` joins `nextUtcCalendarDay` in `@objectstack/spec/data`
  (re-exported from `@objectstack/core`): it reads the UTC instant a temporal
  operand denotes, accepting only unambiguous spellings — a `Date`, epoch ms, a
  bare `YYYY-MM-DD`, and an ISO timestamp with or without an explicit zone (a
  zone-naive one being UTC, per D-B2) — and returning `null` for everything
  else, notably a bare wall clock, which denotes no instant.

  Both type-blind evaluators now use it to compare a `Date` against wire text,
  which JS relational operators cannot do: `<` and friends coerce with hint
  `number`, so the `Date` becomes its epoch and the string becomes `NaN`.

  - `formula`'s `matchesFilterCondition` (the RLS write-side `check`) dropped
    every `Date`-valued row in 10 of the 16 shared conformance cases. The
    post-image is the caller's raw write payload, so an SDK write of
    `new Date()` hit this directly, and fail-closed turned it into a **denied
    write**.
  - `service-analytics`' preview evaluator diverged on the same 10 cases in
    BOTH directions, because `String(new Date())` sorts after every `'2026-…'`
    comparand — a drafted chart both lost rows and gained ones, then changed
    its numbers at publish. Rows from a mongo-backed dataset arrive as BSON
    `Date`s, so this was reachable in normal use.

  Comparisons that did not involve a `Date` are unchanged.

- Updated dependencies [6a67d7a]
- Updated dependencies [0ecc656]
- Updated dependencies [06772eb]
- Updated dependencies [270650f]
- Updated dependencies [3aef718]
- Updated dependencies [1ea6bce]
- Updated dependencies [c1dcacd]
- Updated dependencies [ad303ed]
- Updated dependencies [32ccb23]
- Updated dependencies [f5a4ef0]
- Updated dependencies [2d3e255]
- Updated dependencies [7d7521f]
- Updated dependencies [5dc4d02]
- Updated dependencies [05154a1]
- Updated dependencies [9b6fe7c]
- Updated dependencies [8c711fb]
- Updated dependencies [09e4547]
- Updated dependencies [91f4c78]
- Updated dependencies [820eff9]
- Updated dependencies [8d895ff]
- Updated dependencies [f6472d7]
- Updated dependencies [78caf51]
- Updated dependencies [62a789b]
- Updated dependencies [789ad63]
- Updated dependencies [2af1988]
- Updated dependencies [12a19a8]
- Updated dependencies [41dcda3]
- Updated dependencies [c8124e5]
- Updated dependencies [a1a4140]
- Updated dependencies [217e2e6]
- Updated dependencies [86a71d1]
- Updated dependencies [d5c75e2]
- Updated dependencies [03d26f7]
- Updated dependencies [4384921]
- Updated dependencies [3c628ce]
- Updated dependencies [7cb922e]
- Updated dependencies [1d22114]
- Updated dependencies [b5f9397]
- Updated dependencies [ed77493]
- Updated dependencies [58a03d2]
- Updated dependencies [dc530b4]
- Updated dependencies [e59786e]
- Updated dependencies [bcf1112]
- Updated dependencies [9774b78]
- Updated dependencies [b07d829]
- Updated dependencies [a648e96]
- Updated dependencies [a47ac06]
- Updated dependencies [e4c61a7]
- Updated dependencies [cc60165]
- Updated dependencies [081aa6f]
- Updated dependencies [91f4c78]
- Updated dependencies [e8d0c21]
- Updated dependencies [c1d44f7]
- Updated dependencies [ab9fb5c]
- Updated dependencies [f985b3f]
- Updated dependencies [9a4932a]
- Updated dependencies [f9fc874]
- Updated dependencies [011b386]
- Updated dependencies [7777e8f]
- Updated dependencies [507b92a]
- Updated dependencies [7309c81]
- Updated dependencies [20bc1ec]
- Updated dependencies [90c2b15]
- Updated dependencies [42eeb7d]
- Updated dependencies [01e124d]
- Updated dependencies [7ce02eb]
- Updated dependencies [a13827e]
- Updated dependencies [7733604]
- Updated dependencies [40e420f]
- Updated dependencies [d13004a]
- Updated dependencies [5b47ab5]
- Updated dependencies [b09d8d9]
- Updated dependencies [b09d8d9]
- Updated dependencies [8675db6]
- Updated dependencies [b09d8d9]
- Updated dependencies [3eb1b2b]
- Updated dependencies [59b85c0]
- Updated dependencies [6e357ed]
- Updated dependencies [d6938bf]
- Updated dependencies [31e0be9]
- Updated dependencies [4bfd455]
- Updated dependencies [ffd2ce2]
- Updated dependencies [62f8017]
- Updated dependencies [a831df1]
- Updated dependencies [f752ee3]
- Updated dependencies [a1b61e0]
- Updated dependencies [cd6b9f2]
- Updated dependencies [2cb6d3c]
- Updated dependencies [af2a095]
- Updated dependencies [ec796d5]
- Updated dependencies [e87fea1]
- Updated dependencies [c65e529]
- Updated dependencies [3ca34c1]
- Updated dependencies [239c3a3]
- Updated dependencies [94a0bbc]
- Updated dependencies [d6bfb3d]
- Updated dependencies [a2266a6]
- Updated dependencies [d25a0ec]
- Updated dependencies [667b83e]
- Updated dependencies [627b188]
- Updated dependencies [8d4eae7]
- Updated dependencies [65a3a84]
- Updated dependencies [ccd9397]
- Updated dependencies [bca935b]
- Updated dependencies [c54c822]
- Updated dependencies [8dcc0f5]
- Updated dependencies [75b9e51]
- Updated dependencies [0a2f233]
- Updated dependencies [8621cdd]
- Updated dependencies [6f23667]
- Updated dependencies [5d21a48]
- Updated dependencies [19365b7]
- Updated dependencies [b7ed26d]
- Updated dependencies [b3a3d83]
- Updated dependencies [7a55913]
- Updated dependencies [35accbf]
- Updated dependencies [6038de7]
- Updated dependencies [eb95d97]
- Updated dependencies [e4c2dc8]
- Updated dependencies [1bd2795]
- Updated dependencies [8186a70]
- Updated dependencies [a329cca]
- Updated dependencies [6eec18c]
- Updated dependencies [4d7bebf]
- Updated dependencies [821ac7a]
- Updated dependencies [8f81731]
- Updated dependencies [8b50cb3]
- Updated dependencies [8c2db68]
- Updated dependencies [22b5e54]
- Updated dependencies [0166bd5]
- Updated dependencies [9b702dc]
- Updated dependencies [ab16331]
  - @objectstack/spec@17.0.0-rc.1

## 17.0.0-rc.0

### Minor Changes

- 2fa4ca1: Dynamic approver routing for approval nodes (#3447 P2) — three new declarative capabilities:

  **`expression` approvers.** A new approver type whose CEL expression resolves WHO approves at node entry, over exactly three roots: `current.*` (the record's live state), `trigger.*` (the submit-time snapshot) and `vars.*` (flow variables, incl. upstream node outputs). `record` and bare field names are rejected before evaluation — on this platform `record` always means "the record at event time", which is ambiguous at an approval node — with error messages that prescribe the correct spelling. The optional `resolveAs: 'user' | 'department' | 'position' | 'team'` re-expands each resolved id through the same graph lookups the static types use; with `behavior: 'per_group'` each intermediate value (e.g. each returned department) forms its own sign-off group. A missing key fails the node loudly; only a present-but-empty result counts as an empty slate.

  **`onEmptyApprovers` policy.** What an empty resolved slate does, node-level, for all approver types: `admin_rescue` (default — request opens for privileged takeover, the #3424 behaviour), `fail` (node fails), or `auto_approve` (skip the request, continue down the `approve` edge with `output.autoApproved = true`). To support auto-approve, the automation engine now honours `NodeExecutionResult.branchLabel` on the synchronous completion path — the field existed but was only ever consumed via resume signals.

  **Decision outputs.** `decide(..., { outputs })` hands structured data from the approver to the flow: the author declares allowed keys on the node (`decisionOutputs`), approvers fill values only, and accepted outputs resume the run as `<nodeId>.<key>` variables — a later approval node's expression can read `vars.<nodeId>.picked_departments`, closing "the previous approver picks the next step's approvers" without a record-field detour. Undeclared keys reject the decision; `decision`/`requestId` are reserved. Multi-approver tallies now always pin to the open-time approver snapshot (previously unanimous re-resolved at each decision against the payload snapshot).

  Also: `collectCelRootIdentifiers` is exported from `@objectstack/formula` (shared by the new `os lint` rules and the runtime pre-check, so they can never drift), resolution inputs are audited on the request snapshot as `__resolvedFrom`, and three new lint rules gate expressions, empty-slate policies and reserved output keys at author time.

### Patch Changes

- Updated dependencies [50616d9]
- Updated dependencies [08b5a3d]
- Updated dependencies [d99aeb3]
- Updated dependencies [4727eb8]
- Updated dependencies [f63cd09]
- Updated dependencies [fa3d0cf]
- Updated dependencies [af5a224]
- Updated dependencies [71f76e1]
- Updated dependencies [37b1346]
- Updated dependencies [99736a0]
- Updated dependencies [fe67e34]
- Updated dependencies [fdb4f50]
- Updated dependencies [1bd5652]
- Updated dependencies [14252d3]
- Updated dependencies [7fb436c]
- Updated dependencies [879ea13]
- Updated dependencies [201b31f]
- Updated dependencies [e2616e0]
- Updated dependencies [6fdc5c6]
- Updated dependencies [8b9d71e]
- Updated dependencies [33f5e23]
- Updated dependencies [259af21]
- Updated dependencies [587fc91]
- Updated dependencies [1986594]
- Updated dependencies [ad4af62]
- Updated dependencies [d44dbfa]
- Updated dependencies [474fe39]
- Updated dependencies [0bc685a]
- Updated dependencies [b949059]
- Updated dependencies [be1c52c]
- Updated dependencies [c5ff96d]
- Updated dependencies [84e7be9]
- Updated dependencies [a6c3f38]
- Updated dependencies [debc23a]
- Updated dependencies [0f8ad09]
- Updated dependencies [8f9689f]
- Updated dependencies [57a3bb3]
- Updated dependencies [5f9a987]
- Updated dependencies [db02d47]
- Updated dependencies [0bfdf46]
- Updated dependencies [376a061]
- Updated dependencies [7c7e246]
- Updated dependencies [f35cdc5]
- Updated dependencies [9ea2bc5]
- Updated dependencies [c2d9098]
- Updated dependencies [a227ed7]
- Updated dependencies [9613396]
- Updated dependencies [e47b342]
- Updated dependencies [4ed7ed4]
- Updated dependencies [2fa4ca1]
- Updated dependencies [f5a2320]
- Updated dependencies [deb538f]
- Updated dependencies [5b89711]
- Updated dependencies [0c8a22f]
- Updated dependencies [763931e]
- Updated dependencies [de9af8a]
- Updated dependencies [c4df271]
- Updated dependencies [a41ba5c]
- Updated dependencies [189854c]
- Updated dependencies [0e3a226]
- Updated dependencies [1d4756e]
- Updated dependencies [720c5ad]
- Updated dependencies [a8d1e24]
- Updated dependencies [41642b0]
- Updated dependencies [4cca74c]
- Updated dependencies [88ef03e]
- Updated dependencies [9e2caf3]
- Updated dependencies [81ce41a]
- Updated dependencies [85e1e4e]
- Updated dependencies [dac6a08]
- Updated dependencies [394b7a1]
- Updated dependencies [677b591]
- Updated dependencies [d77d1b7]
- Updated dependencies [5b79a34]
- Updated dependencies [c757854]
- Updated dependencies [0045682]
- Updated dependencies [2a5f04a]
- Updated dependencies [4f740b0]
- Updated dependencies [67452d1]
- Updated dependencies [0fc6219]
- Updated dependencies [605e190]
- Updated dependencies [c6c59f1]
- Updated dependencies [b0e78a8]
- Updated dependencies [f31cc8d]
- Updated dependencies [f343dc4]
- Updated dependencies [8269e32]
- Updated dependencies [74f7339]
- Updated dependencies [a6c35a2]
- Updated dependencies [c2f1002]
- Updated dependencies [f163028]
- Updated dependencies [f07808c]
- Updated dependencies [7ffc3d3]
- Updated dependencies [88346ba]
- Updated dependencies [4631592]
- Updated dependencies [32ff033]
- Updated dependencies [5ac93d4]
- Updated dependencies [93f267f]
- Updated dependencies [0024abf]
- Updated dependencies [acbf364]
- Updated dependencies [7687f7b]
- Updated dependencies [1659072]
- Updated dependencies [abceb0d]
- Updated dependencies [0c302a7]
- Updated dependencies [6633337]
- Updated dependencies [f00d8d4]
- Updated dependencies [503be86]
- Updated dependencies [cde1975]
- Updated dependencies [0bc685a]
- Updated dependencies [11949fc]
- Updated dependencies [b098b0e]
- Updated dependencies [4d00b13]
- Updated dependencies [57bab76]
- Updated dependencies [b90086a]
- Updated dependencies [b95577a]
- Updated dependencies [83c161f]
- Updated dependencies [d8c4957]
- Updated dependencies [f24cb83]
- Updated dependencies [5dbbb92]
- Updated dependencies [69f1dfd]
  - @objectstack/spec@17.0.0-rc.0

## 16.1.0

### Patch Changes

- Updated dependencies [9e45b63]
  - @objectstack/spec@16.1.0

## 16.0.0

### Minor Changes

- 6b51346: feat(formula): `dateField == today()` now matches — AST temporal-comparison rewrite (#3183)

  **Behavior change (the fix):** a `Field.date` compared with `==`/`!=` against a
  temporal function now matches on the calendar day. Previously it **silently
  returned the wrong answer** — `record.due_date == today()` was always `false`
  (and `!= today()` always `true`) even for a same-day record, because a
  `Field.date` reads back as a `YYYY-MM-DD` **string** (ADR-0053 Phase 1) and
  cel-js's equality (`overloads.js` `isEqual`) treats a string and a timestamp as
  unequal without consulting any overload.

  `celEngine.evaluate` now rewrites the parsed AST: for each `==`/`!=` whose one
  operand is `today()`/`daysFromNow()`/`daysAgo()`/`now()`, the **field operand**
  is wrapped in `date(...)` (the stdlib coercion), then the expression is
  serialized and evaluated. So `record.due_date == today()` runs as
  `date(record.due_date) == today()`.

  - **Per-occurrence**, not per-field: `record.d == "2026-06-20" || record.d == today()`
    keeps the string-literal comparison intact while fixing the temporal one.
  - **Type-blind-safe**: `date()` degrades gracefully — an already-`Date`
    (`Field.datetime`) operand passes through; a non-date string or null →
    `Invalid Date` → the comparison stays `false`, exactly as before. No
    field-type information is needed, and no currently-correct result is worsened.
  - **Cheap**: the rewrite only reserializes when such a comparison is present
    (a plain-`includes` gate skips the rest), and is memoized per source string.

  Applies to every interpreter site — read-time `Field.formula`, default values,
  validation rules, hook conditions, and flow conditions — since all route through
  `celEngine.evaluate`. RLS/sharing conditions are unaffected: they compile via
  `cel-to-filter`, which already rejects function calls as a loud authoring error.

  **Supersedes the #3192 advisory lint.** That build-time warning
  (`checkTemporalDateEquality`) flagged `dateField == today()` as a silent-miss;
  with the runtime fixed it would be a false alarm, so it (and the
  `temporalEqualityFields` helper it used) is removed. Authors can now write the
  natural `record.due_date == today()` directly; the `date(...)` /
  `daysBetween(...) == 0` / range idioms all keep working.

- 80273c8: feat(formula): warn when a `date` field is compared to a temporal function with `==`/`!=` (#3183)

  A `Field.date` deserializes as a `YYYY-MM-DD` **string** (ADR-0053 Phase 1), and
  cel-js's equality hard-codes `string == <timestamp>` to `false` — it returns
  `false` for a string left operand without ever consulting a registered overload,
  and refuses cross-type object equality (`@marcbachmann/cel-js` `overloads.js`
  `isEqual`). So the most natural "is it due today" predicate —

  ```cel
  record.due_date == today()      // silently false, even when due_date IS today
  record.due_date != today()      // silently true for a same-day record
  ```

  — compiles clean, throws nothing, and silently never matches. Same silent-miss
  family as #1928; **timezone-independent** (fails identically at UTC) and
  cross-cutting (formulas, validation, RLS, flow/action/sharing/hook predicates).

  cel-js gives no operator-layer hook to fix the comparison, so this adds a
  **build-time advisory warning** (the established ADR-0032 guardrail strategy)
  rather than a runtime behavior change. `validateExpression` reuses the shared
  `ExprSchemaHint.fieldTypes` (the same per-field type map the #1928 tier-4
  soundness check already threads through `@objectstack/lint`) to flag a `==`/`!=`
  between a `date` field (`record.`/`previous.`/bare) and
  `today()`/`daysFromNow()`/`daysAgo()`/`now()`, with a self-correcting message
  pointing at the working idioms: `date(record.d) == today()`, a range
  (`>= … && <= …`), or `daysBetween(today(), record.d) == 0`.

  Warning severity — never fails the build (the write/validation path may carry a
  real `Date`). Restricted to `type: 'date'` (unambiguously a string); `datetime`
  is excluded to avoid false positives. Ordering operators (`>=`/`<=`/`<`/`>`)
  already work — cel-js _throws_ for them, tripping the engine's existing
  string-hydration retry — so they are not flagged.

  A runtime fix (normalizing the peer of a temporal operand in the data layer)
  remains tracked in #3183; a naive "hydrate date fields to `Date`" version would
  trade this silent-miss for another (breaking `dateField == "2026-06-20"`), so it
  needs its own design.

- 7125007: **Stored `Field.formula` fields that compute dates/durations no longer silently evaluate to `null` (#3306).** Three independent CEL gaps made shipped template formulas (e.g. `hr_employee.tenure_years`, `hr_time_off_request.days`) return `null` with no parse/build/runtime error:

  1. **The null-guard idiom `cond ? <value> : null` now compiles and evaluates.** cel-js's ternary type-unifier rejects a concrete `int`/`double`/`string` branch against `null` — so even `true ? 5 : null` faulted _"Ternary branches must have the same type"_ and the whole formula nulled. A `Field.formula` is inherently nullable and the catalog blesses both ternary and `== null`, so this is the canonical "compute value, else blank" shape. An AST pre-pass (mirroring the #3183 temporal-equality rewrite) wraps the non-null branch in `dyn(...)` — value-preserving, null-branch-only, idempotent — so it type-checks and runs. Applied in `compile()`, `evaluate()`, and the build soundness check alike.

  2. **`floor(x)` / `ceil(x)` are now registered** (parallel to `round`/`abs`) and advertised in the catalog. They round toward −∞ / +∞, so `floor(-1.2) == -2` — NOT interchangeable with integer division's round-toward-zero. Previously `floor(...)` faulted `found no matching overload` and the formula nulled.

  3. **Date arithmetic is now a build-time ERROR instead of a silent runtime `null`.** `record.end_date - record.start_date + 1`, `today() + 30`, `record.date + n` type-check clean (operands are `dyn`) but always fault at runtime and never recover (a date string is not numeric, so hydration can't rescue it). The build soundness check now types `date`/`datetime` fields as `google.protobuf.Timestamp` and flags date/duration **arithmetic against a number** with a corrective message pointing at `daysBetween(a, b)` / `daysFromNow(n)` / `addDays(d, n)` / `addMonths(d, n)`. Sound by construction — ordering (`date < today()`, `date < "2026-01-01"` string-lex), equality (#3183), and string concatenation (`"Due: " + date`) are all runtime-tolerated and never flagged; only arithmetic against a number is. A `!= null` guard on a date field no longer masks the inner fault (`== null` no-op overloads registered in the check-only env).

  > **Heads-up for downstream:** (3) adds a NEW build-time error. A stored formula or predicate doing arithmetic on a `date`/`datetime` field (`end - start + 1`, `today() + 30`) that previously built (and nulled at runtime) will now fail `objectstack build` / `validateStackExpressions` with a message telling you to use `daysBetween` / `daysFromNow` / `addDays`. This only fires for genuinely-broken expressions that already returned `null`.

  Fixes #3306.

- ea32ec7: feat(formula,lint): advisory type-soundness warnings for formula/predicate expressions (#1928 tier 4)

  Closes the last open guardrail from #1928. A `Field.formula` or record-scoped
  predicate that uses a **text or boolean field with an arithmetic (`+ - * / %`)
  or ordering (`< > <= >=`) operator against a number** faults the runtime
  overload and silently evaluates to `null` (e.g. `record.title * 2`,
  `record.is_active + 1`). The build now surfaces this as a **non-blocking
  warning** with the offending field and a corrective message.

  Honours the ADR-0032 design law — the checker only flags what the runtime
  would also fail:

  - Number / currency / percent / date / datetime fields are declared `dyn`, so
    the cases the runtime rescues never warn — `record.amount / 100` (the #1930
    `registerOperator` fix), `record.due == today()` and numeric-string / ISO-date
    values (the string-hydration retry), and numeric-coded `select` option values.
  - Equality (`==` / `!=`) is excluded: a heterogeneous equality is runtime-safe
    (evaluates to `false`), never a fault.

  New `firstTypeMismatch(source, fieldCelTypes, scope)` export in
  `@objectstack/formula` (and an optional `fieldTypes` hint on
  `validateExpression`); `@objectstack/lint`'s `validateStackExpressions` threads
  each object's field types into every checked site:

  - **record-scoped** sites (`record.<field>`) — formula fields, validation rules,
    action / hook / sharing predicates;
  - **flattened** flow / automation conditions (bare `field`) — where flow
    variables stay `dyn` and are never flagged, and equality stays runtime-safe.

  Warnings are advisory in `objectstack build` / `validate` (fatal only under
  `--strict`), matching the tier-3 channel.

### Patch Changes

- e0859b1: fix(formula): retire the `js` expression dialect and fix the `hasDialect` false-positive (#3278)

  The `js` **expression** dialect was declared in `ExpressionDialect` but never
  shipped — it existed only as a registry stub with no engine and no author helper
  (`cel`/`F`/`P` → CEL, `tmpl` → template, `cron` → cron; nothing ever emitted
  `js`). Per ADR-0049 (enforce-or-remove) it is removed from the enum; the set is
  now `{cel, cron, template}`.

  Procedural JavaScript is unaffected: it remains the **L2** authoring surface —
  the sandboxed, capability-gated `ScriptBody { language: 'js' }` in hook/action
  bodies — which is a separate enum (`hook-body.zod.ts`), not an expression
  dialect.

  Also fixes a latent bug in `hasDialect`: it detected stubs via
  `dialect.startsWith('stub:')`, but stubs were registered under their real name,
  so the check was dead code and `hasDialect('js')` returned a false-positive
  `true`. With the stub removed, `hasDialect` reports only registered real
  engines, and the registry test now asserts the negative case (`hasDialect('js')
=== false`) so the gate can actually go red.

  No runtime behavior changes for any valid persisted artifact — no producer ever
  emitted `dialect: 'js'`. See the ADR-0058 addendum.

- Updated dependencies [f972574]
- Updated dependencies [6289ec3]
- Updated dependencies [22013aa]
- Updated dependencies [3ad3dd5]
- Updated dependencies [8efa395]
- Updated dependencies [3a18b60]
- Updated dependencies [a8aa34c]
- Updated dependencies [a3823b2]
- Updated dependencies [43a3efb]
- Updated dependencies [524696a]
- Updated dependencies [bfa3c3f]
- Updated dependencies [5e3301d]
- Updated dependencies [46e876c]
- Updated dependencies [158aa14]
- Updated dependencies [62a2117]
- Updated dependencies [d2723e2]
- Updated dependencies [fefcd54]
- Updated dependencies [beaf2de]
- Updated dependencies [369eb6e]
- Updated dependencies [06ff734]
- Updated dependencies [b659111]
- Updated dependencies [5754a23]
- Updated dependencies [6c270a6]
- Updated dependencies [668dd17]
- Updated dependencies [8abf133]
- Updated dependencies [e0859b1]
- Updated dependencies [04ecd4e]
- Updated dependencies [4d5a892]
- Updated dependencies [16cebeb]
- Updated dependencies [86d30af]
- Updated dependencies [8923843]
- Updated dependencies [a2795f6]
- Updated dependencies [f16b492]
- Updated dependencies [4b6fde8]
- Updated dependencies [2018df9]
- Updated dependencies [fc5a3a2]
- Updated dependencies [8ff9210]
  - @objectstack/spec@16.0.0

## 16.0.0-rc.1

### Minor Changes

- 7125007: **Stored `Field.formula` fields that compute dates/durations no longer silently evaluate to `null` (#3306).** Three independent CEL gaps made shipped template formulas (e.g. `hr_employee.tenure_years`, `hr_time_off_request.days`) return `null` with no parse/build/runtime error:

  1. **The null-guard idiom `cond ? <value> : null` now compiles and evaluates.** cel-js's ternary type-unifier rejects a concrete `int`/`double`/`string` branch against `null` — so even `true ? 5 : null` faulted _"Ternary branches must have the same type"_ and the whole formula nulled. A `Field.formula` is inherently nullable and the catalog blesses both ternary and `== null`, so this is the canonical "compute value, else blank" shape. An AST pre-pass (mirroring the #3183 temporal-equality rewrite) wraps the non-null branch in `dyn(...)` — value-preserving, null-branch-only, idempotent — so it type-checks and runs. Applied in `compile()`, `evaluate()`, and the build soundness check alike.

  2. **`floor(x)` / `ceil(x)` are now registered** (parallel to `round`/`abs`) and advertised in the catalog. They round toward −∞ / +∞, so `floor(-1.2) == -2` — NOT interchangeable with integer division's round-toward-zero. Previously `floor(...)` faulted `found no matching overload` and the formula nulled.

  3. **Date arithmetic is now a build-time ERROR instead of a silent runtime `null`.** `record.end_date - record.start_date + 1`, `today() + 30`, `record.date + n` type-check clean (operands are `dyn`) but always fault at runtime and never recover (a date string is not numeric, so hydration can't rescue it). The build soundness check now types `date`/`datetime` fields as `google.protobuf.Timestamp` and flags date/duration **arithmetic against a number** with a corrective message pointing at `daysBetween(a, b)` / `daysFromNow(n)` / `addDays(d, n)` / `addMonths(d, n)`. Sound by construction — ordering (`date < today()`, `date < "2026-01-01"` string-lex), equality (#3183), and string concatenation (`"Due: " + date`) are all runtime-tolerated and never flagged; only arithmetic against a number is. A `!= null` guard on a date field no longer masks the inner fault (`== null` no-op overloads registered in the check-only env).

  > **Heads-up for downstream:** (3) adds a NEW build-time error. A stored formula or predicate doing arithmetic on a `date`/`datetime` field (`end - start + 1`, `today() + 30`) that previously built (and nulled at runtime) will now fail `objectstack build` / `validateStackExpressions` with a message telling you to use `daysBetween` / `daysFromNow` / `addDays`. This only fires for genuinely-broken expressions that already returned `null`.

  Fixes #3306.

### Patch Changes

- Updated dependencies [6289ec3]
- Updated dependencies [8efa395]
- Updated dependencies [bfa3c3f]
- Updated dependencies [62a2117]
- Updated dependencies [06ff734]
  - @objectstack/spec@16.0.0-rc.1

## 16.0.0-rc.0

### Minor Changes

- 6b51346: feat(formula): `dateField == today()` now matches — AST temporal-comparison rewrite (#3183)

  **Behavior change (the fix):** a `Field.date` compared with `==`/`!=` against a
  temporal function now matches on the calendar day. Previously it **silently
  returned the wrong answer** — `record.due_date == today()` was always `false`
  (and `!= today()` always `true`) even for a same-day record, because a
  `Field.date` reads back as a `YYYY-MM-DD` **string** (ADR-0053 Phase 1) and
  cel-js's equality (`overloads.js` `isEqual`) treats a string and a timestamp as
  unequal without consulting any overload.

  `celEngine.evaluate` now rewrites the parsed AST: for each `==`/`!=` whose one
  operand is `today()`/`daysFromNow()`/`daysAgo()`/`now()`, the **field operand**
  is wrapped in `date(...)` (the stdlib coercion), then the expression is
  serialized and evaluated. So `record.due_date == today()` runs as
  `date(record.due_date) == today()`.

  - **Per-occurrence**, not per-field: `record.d == "2026-06-20" || record.d == today()`
    keeps the string-literal comparison intact while fixing the temporal one.
  - **Type-blind-safe**: `date()` degrades gracefully — an already-`Date`
    (`Field.datetime`) operand passes through; a non-date string or null →
    `Invalid Date` → the comparison stays `false`, exactly as before. No
    field-type information is needed, and no currently-correct result is worsened.
  - **Cheap**: the rewrite only reserializes when such a comparison is present
    (a plain-`includes` gate skips the rest), and is memoized per source string.

  Applies to every interpreter site — read-time `Field.formula`, default values,
  validation rules, hook conditions, and flow conditions — since all route through
  `celEngine.evaluate`. RLS/sharing conditions are unaffected: they compile via
  `cel-to-filter`, which already rejects function calls as a loud authoring error.

  **Supersedes the #3192 advisory lint.** That build-time warning
  (`checkTemporalDateEquality`) flagged `dateField == today()` as a silent-miss;
  with the runtime fixed it would be a false alarm, so it (and the
  `temporalEqualityFields` helper it used) is removed. Authors can now write the
  natural `record.due_date == today()` directly; the `date(...)` /
  `daysBetween(...) == 0` / range idioms all keep working.

- 80273c8: feat(formula): warn when a `date` field is compared to a temporal function with `==`/`!=` (#3183)

  A `Field.date` deserializes as a `YYYY-MM-DD` **string** (ADR-0053 Phase 1), and
  cel-js's equality hard-codes `string == <timestamp>` to `false` — it returns
  `false` for a string left operand without ever consulting a registered overload,
  and refuses cross-type object equality (`@marcbachmann/cel-js` `overloads.js`
  `isEqual`). So the most natural "is it due today" predicate —

  ```cel
  record.due_date == today()      // silently false, even when due_date IS today
  record.due_date != today()      // silently true for a same-day record
  ```

  — compiles clean, throws nothing, and silently never matches. Same silent-miss
  family as #1928; **timezone-independent** (fails identically at UTC) and
  cross-cutting (formulas, validation, RLS, flow/action/sharing/hook predicates).

  cel-js gives no operator-layer hook to fix the comparison, so this adds a
  **build-time advisory warning** (the established ADR-0032 guardrail strategy)
  rather than a runtime behavior change. `validateExpression` reuses the shared
  `ExprSchemaHint.fieldTypes` (the same per-field type map the #1928 tier-4
  soundness check already threads through `@objectstack/lint`) to flag a `==`/`!=`
  between a `date` field (`record.`/`previous.`/bare) and
  `today()`/`daysFromNow()`/`daysAgo()`/`now()`, with a self-correcting message
  pointing at the working idioms: `date(record.d) == today()`, a range
  (`>= … && <= …`), or `daysBetween(today(), record.d) == 0`.

  Warning severity — never fails the build (the write/validation path may carry a
  real `Date`). Restricted to `type: 'date'` (unambiguously a string); `datetime`
  is excluded to avoid false positives. Ordering operators (`>=`/`<=`/`<`/`>`)
  already work — cel-js _throws_ for them, tripping the engine's existing
  string-hydration retry — so they are not flagged.

  A runtime fix (normalizing the peer of a temporal operand in the data layer)
  remains tracked in #3183; a naive "hydrate date fields to `Date`" version would
  trade this silent-miss for another (breaking `dateField == "2026-06-20"`), so it
  needs its own design.

- ea32ec7: feat(formula,lint): advisory type-soundness warnings for formula/predicate expressions (#1928 tier 4)

  Closes the last open guardrail from #1928. A `Field.formula` or record-scoped
  predicate that uses a **text or boolean field with an arithmetic (`+ - * / %`)
  or ordering (`< > <= >=`) operator against a number** faults the runtime
  overload and silently evaluates to `null` (e.g. `record.title * 2`,
  `record.is_active + 1`). The build now surfaces this as a **non-blocking
  warning** with the offending field and a corrective message.

  Honours the ADR-0032 design law — the checker only flags what the runtime
  would also fail:

  - Number / currency / percent / date / datetime fields are declared `dyn`, so
    the cases the runtime rescues never warn — `record.amount / 100` (the #1930
    `registerOperator` fix), `record.due == today()` and numeric-string / ISO-date
    values (the string-hydration retry), and numeric-coded `select` option values.
  - Equality (`==` / `!=`) is excluded: a heterogeneous equality is runtime-safe
    (evaluates to `false`), never a fault.

  New `firstTypeMismatch(source, fieldCelTypes, scope)` export in
  `@objectstack/formula` (and an optional `fieldTypes` hint on
  `validateExpression`); `@objectstack/lint`'s `validateStackExpressions` threads
  each object's field types into every checked site:

  - **record-scoped** sites (`record.<field>`) — formula fields, validation rules,
    action / hook / sharing predicates;
  - **flattened** flow / automation conditions (bare `field`) — where flow
    variables stay `dyn` and are never flagged, and equality stays runtime-safe.

  Warnings are advisory in `objectstack build` / `validate` (fatal only under
  `--strict`), matching the tier-3 channel.

### Patch Changes

- e0859b1: fix(formula): retire the `js` expression dialect and fix the `hasDialect` false-positive (#3278)

  The `js` **expression** dialect was declared in `ExpressionDialect` but never
  shipped — it existed only as a registry stub with no engine and no author helper
  (`cel`/`F`/`P` → CEL, `tmpl` → template, `cron` → cron; nothing ever emitted
  `js`). Per ADR-0049 (enforce-or-remove) it is removed from the enum; the set is
  now `{cel, cron, template}`.

  Procedural JavaScript is unaffected: it remains the **L2** authoring surface —
  the sandboxed, capability-gated `ScriptBody { language: 'js' }` in hook/action
  bodies — which is a separate enum (`hook-body.zod.ts`), not an expression
  dialect.

  Also fixes a latent bug in `hasDialect`: it detected stubs via
  `dialect.startsWith('stub:')`, but stubs were registered under their real name,
  so the check was dead code and `hasDialect('js')` returned a false-positive
  `true`. With the stub removed, `hasDialect` reports only registered real
  engines, and the registry test now asserts the negative case (`hasDialect('js')
=== false`) so the gate can actually go red.

  No runtime behavior changes for any valid persisted artifact — no producer ever
  emitted `dialect: 'js'`. See the ADR-0058 addendum.

- Updated dependencies [f972574]
- Updated dependencies [22013aa]
- Updated dependencies [3ad3dd5]
- Updated dependencies [3a18b60]
- Updated dependencies [a8aa34c]
- Updated dependencies [a3823b2]
- Updated dependencies [43a3efb]
- Updated dependencies [524696a]
- Updated dependencies [5e3301d]
- Updated dependencies [46e876c]
- Updated dependencies [158aa14]
- Updated dependencies [d2723e2]
- Updated dependencies [fefcd54]
- Updated dependencies [beaf2de]
- Updated dependencies [369eb6e]
- Updated dependencies [b659111]
- Updated dependencies [5754a23]
- Updated dependencies [6c270a6]
- Updated dependencies [668dd17]
- Updated dependencies [8abf133]
- Updated dependencies [e0859b1]
- Updated dependencies [04ecd4e]
- Updated dependencies [4d5a892]
- Updated dependencies [16cebeb]
- Updated dependencies [86d30af]
- Updated dependencies [8923843]
- Updated dependencies [a2795f6]
- Updated dependencies [f16b492]
- Updated dependencies [4b6fde8]
- Updated dependencies [2018df9]
- Updated dependencies [fc5a3a2]
  - @objectstack/spec@16.0.0-rc.0

## 15.1.1

### Patch Changes

- @objectstack/spec@15.1.1

## 15.1.0

### Patch Changes

- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [3fe9df1]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [4109153]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [627f225]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
- Updated dependencies [f531a26]
  - @objectstack/spec@15.1.0

## 15.0.0

### Patch Changes

- Updated dependencies [28b7c28]
- Updated dependencies [13749ec]
- Updated dependencies [e62c233]
- Updated dependencies [ed61c9b]
- Updated dependencies [31d04d4]
  - @objectstack/spec@15.0.0

## 14.8.0

### Patch Changes

- Updated dependencies [16b4bf6]
- Updated dependencies [16b4bf6]
- Updated dependencies [10e8983]
- Updated dependencies [607aaf4]
- Updated dependencies [bb71321]
  - @objectstack/spec@14.8.0

## 14.7.0

### Patch Changes

- Updated dependencies [d6a72eb]
  - @objectstack/spec@14.7.0

## 14.6.0

### Patch Changes

- Updated dependencies [609cb13]
- Updated dependencies [ce6d151]
  - @objectstack/spec@14.6.0

## 14.5.0

### Patch Changes

- Updated dependencies [526805e]
- Updated dependencies [d79ca07]
- Updated dependencies [33ebd34]
- Updated dependencies [c044f08]
- Updated dependencies [01274eb]
  - @objectstack/spec@14.5.0

## 14.4.0

### Patch Changes

- Updated dependencies [7953832]
- Updated dependencies [82e745e]
- Updated dependencies [f3035bd]
- Updated dependencies [82c0d94]
- Updated dependencies [7449476]
  - @objectstack/spec@14.4.0

## 14.3.0

### Patch Changes

- Updated dependencies [2a71f48]
- Updated dependencies [02f6af4]
- Updated dependencies [c1064f1]
  - @objectstack/spec@14.3.0

## 14.2.0

### Patch Changes

- Updated dependencies [ac8f029]
- Updated dependencies [4ab9958]
  - @objectstack/spec@14.2.0

## 14.1.0

### Patch Changes

- Updated dependencies [5a8465f]
- Updated dependencies [7f8620b]
- Updated dependencies [82ba3a6]
  - @objectstack/spec@14.1.0

## 14.0.0

### Patch Changes

- Updated dependencies [0a8e685]
- Updated dependencies [afa8115]
- Updated dependencies [80f12ca]
- Updated dependencies [e2fa074]
- Updated dependencies [23c8668]
- Updated dependencies [29f017d]
- Updated dependencies [216fa9a]
- Updated dependencies [6c22b12]
  - @objectstack/spec@14.0.0

## 13.0.0

### Major Changes

- 6d83431: ADR-0090 P1 breaking wave — permission model v2 concept convergence.

  Pre-launch one-step renames and secure defaults (no compatibility aliases, per
  ADR-0090 D3/D4 superseding ADR-0057 D5/D7's alias discipline):

  - `sys_role` → `sys_position`, `sys_user_role` → `sys_user_position` (field
    `role` → `position`), `sys_role_permission_set` → `sys_position_permission_set`
    (field `role_id` → `position_id`); `RoleSchema`/`defineRole` →
    `PositionSchema`/`definePosition` with **no `parent`** (positions are flat;
    hierarchy lives on the business-unit tree).
  - `ExecutionContext.roles[]` → `positions[]`; the EvalUser/CEL contract
    `current_user.roles` → `current_user.positions` (formula validators updated);
    stack property `roles:` → `positions:`; metadata kinds `role`/`profile` →
    `position` (profile kind removed).
  - `isProfile` removed from `PermissionSetSchema` (ADR-0090 D2); `isDefault`
    narrows to an install-time suggestion; `appDefaultProfileName` →
    `appDefaultPermissionSetName` (isDefault-only).
  - OWD enum drops legacy aliases `read`/`read_write`/`full`; new optional
    `externalSharingModel` (external dial, `private` default) lands as P1 spec
    shape (ADR-0090 D11).
  - **Secure default (D1)**: a custom object with an owner field and NO
    `sharingModel` now resolves `private` (was: fully public). System objects
    keep their explicit posture. Unrecognised stored values fail closed.
  - ExecutionContext gains the P1 principal-taxonomy shape (D10):
    `principalKind` / `audience` / `onBehalfOf` (optional, semantics phase in
    later).
  - Sharing recipients: `role` → `position` (expanded via `sys_user_position`
    ∪ the better-auth membership transition source); `role_and_subordinates`
    removed — `unit_and_subordinates` now expands the business-unit subtree
    (finishes ADR-0057 D5's re-homing).

### Patch Changes

- Updated dependencies [6d83431]
- Updated dependencies [01917c2]
- Updated dependencies [b271691]
- Updated dependencies [a5a1e41]
- Updated dependencies [466adf6]
- Updated dependencies [5be00c3]
- Updated dependencies [466adf6]
- Updated dependencies [2bee609]
- Updated dependencies [fc7e7f7]
  - @objectstack/spec@13.0.0

## 12.6.0

### Patch Changes

- Updated dependencies [6cebf22]
  - @objectstack/spec@12.6.0

## 12.5.0

### Patch Changes

- Updated dependencies [8b3d363]
  - @objectstack/spec@12.5.0

## 12.4.0

### Patch Changes

- Updated dependencies [60dc3ba]
  - @objectstack/spec@12.4.0

## 12.3.0

### Patch Changes

- Updated dependencies [e7eceec]
  - @objectstack/spec@12.3.0

## 12.2.0

### Patch Changes

- Updated dependencies [fce8ff4]
- Updated dependencies [3962023]
- Updated dependencies [2bb193d]
- Updated dependencies [0426d27]
- Updated dependencies [da807f7]
  - @objectstack/spec@12.2.0

## 12.1.0

### Patch Changes

- Updated dependencies [93e6d02]
  - @objectstack/spec@12.1.0

## 12.0.0

### Patch Changes

- Updated dependencies [a8df396]
- Updated dependencies [e695fe0]
- Updated dependencies [7c09621]
- Updated dependencies [7709db4]
- Updated dependencies [2082109]
- Updated dependencies [7c09621]
- Updated dependencies [9860de4]
- Updated dependencies [069c205]
  - @objectstack/spec@12.0.0

## 11.10.0

### Patch Changes

- Updated dependencies [6a9397e]
- Updated dependencies [c0efe5d]
  - @objectstack/spec@11.10.0

## 11.9.0

### Patch Changes

- Updated dependencies [d3595d9]
  - @objectstack/spec@11.9.0

## 11.8.0

### Patch Changes

- @objectstack/spec@11.8.0

## 11.7.0

### Patch Changes

- Updated dependencies [5178906]
  - @objectstack/spec@11.7.0

## 11.6.0

### Patch Changes

- @objectstack/spec@11.6.0

## 11.5.0

### Patch Changes

- Updated dependencies [6ee4f04]
- Updated dependencies [c1e3a65]
  - @objectstack/spec@11.5.0

## 11.4.0

### Patch Changes

- Updated dependencies [5821c51]
- Updated dependencies [a0fce3f]
  - @objectstack/spec@11.4.0

## 11.3.0

### Patch Changes

- Updated dependencies [58e8e31]
- Updated dependencies [b4a5df0]
  - @objectstack/spec@11.3.0

## 11.2.0

### Patch Changes

- Updated dependencies [d0f4b13]
- Updated dependencies [302bdab]
  - @objectstack/spec@11.2.0

## 11.1.0

### Patch Changes

- Updated dependencies [ecf193f]
- Updated dependencies [51bec81]
- Updated dependencies [3e593a7]
- Updated dependencies [63d5403]
  - @objectstack/spec@11.1.0

## 11.0.0

### Minor Changes

- ef3ed67: Formula field typing: `inferExpressionType()` + a declared `returnType`.

  - `@objectstack/formula`: new `inferExpressionType()` (and lower-level `inferCelType()`) surfaces the cel-js type-checker's result for a CEL value/formula expression, mapped to `number | text | boolean | date | unknown`. Conservative — two `dyn` operands stay `unknown`; typed literals/stdlib returns pin a concrete type.
  - `@objectstack/spec`: `FieldSchema` gains an optional `returnType` (`number|text|boolean|date`) so a formula field can carry its declared value type (the way Salesforce/Airtable do), letting consumers (dataset measures, formatting, validation) read a declared type instead of re-parsing the expression.

### Patch Changes

- Updated dependencies [ab5718a]
- Updated dependencies [4845c12]
- Updated dependencies [c1a754a]
- Updated dependencies [6fbe91f]
- Updated dependencies [715d667]
- Updated dependencies [5eef4cf]
- Updated dependencies [72759e1]
- Updated dependencies [6c4fbd9]
- Updated dependencies [ef3ed67]
- Updated dependencies [cd51229]
- Updated dependencies [7697a0e]
- Updated dependencies [e7e04f1]
- Updated dependencies [cfd5ac4]
- Updated dependencies [2be5c1f]
- Updated dependencies [ad143ce]
- Updated dependencies [5c4a8c8]
- Updated dependencies [3afaeed]
- Updated dependencies [8801c02]
- Updated dependencies [3d04e06]
- Updated dependencies [4a84c98]
- Updated dependencies [d980f0d]
- Updated dependencies [a658523]
- Updated dependencies [82ff91c]
- Updated dependencies [638f472]
  - @objectstack/spec@11.0.0

## 10.3.0

### Patch Changes

- @objectstack/spec@10.3.0

## 10.2.0

### Patch Changes

- Updated dependencies [b496498]
  - @objectstack/spec@10.2.0

## 10.1.0

### Patch Changes

- Updated dependencies [49da36e]
- Updated dependencies [ac79f16]
  - @objectstack/spec@10.1.0

## 10.0.0

### Minor Changes

- cfd86ce: ADR-0058 — expression & predicate surface unification. Adds the canonical
  CEL→FilterCondition pushdown compiler in `@objectstack/formula`
  (`compileCelToFilter`, `isPushdownableCel`, `lowerCelAst`) plus an in-memory
  `matchesFilterCondition` backend (one AST, three backends). `plugin-security`
  (RLS `using`, via a SQL bridge) and `plugin-sharing` (`celToFilter`) cut over to
  it, retiring the bespoke regex/field-equality front-ends. Compound sharing
  conditions now compile and enforce end-to-end (closes #1887). The RLS `check`
  clause is now enforced on the write post-image (insert/by-id update), fail-closed.
  Non-pushdownable predicates (arithmetic, functions, subqueries, cross-object) are
  an authoring compile error, never silently dropped (ADR-0049/0055).

### Patch Changes

- 48a307a: build: validate UI action `visible` / `disabled` predicates at compile time

  Extends the ADR-0032 build-time expression check to cover action `visible` and
  `disabled` predicates (stack-level and object-attached), evaluated record-scoped
  like validation rules. A record-header / row action's `visible` is evaluated by
  `ActionEngine` against `{ record, recordId, objectName, user, … }` with
  fail-closed semantics, so a **bare** field reference (`!done` instead of
  `!record.done`) throws at runtime and the action is **silently hidden on every
  record** — the trap behind the #2183 "Mark Done never hides" debugging hunt.
  `os build` now reports it as an error with the corrective `record.<field>`
  message instead of letting it ship.

  `@objectstack/formula`: `ctx` and `features` are added to the record-scope
  namespace roots (alongside the existing `user`, `data`, `context`, …) so the
  ambient globals real action predicates use (`record.id == ctx.user.id`,
  `features.multiOrgEnabled`) are not false-positives. Verified against the full
  monorepo build (every example + platform bundle still compiles clean).

- 25fc0e4: build: extend ADR-0032 predicate validation to all flat record-scoped sites

  Builds on the action-predicate guard. `os build` now also validates these
  record-scoped predicates for bare field references (`status` instead of
  `record.status`), which otherwise evaluate to nothing at runtime and silently
  mis-behave:

  - **field conditional rules** — `requiredWhen`, `readonlyWhen`,
    `conditionalRequired`, `visibleWhen` (server-enforced; a broken one is
    fail-open — the required/readonly rule just never fires);
  - **sharing-rule `condition`** (security-critical — decides which rows a
    principal sees);
  - **lifecycle hook `condition`** (skips the handler when false);
  - **nested `when`** on `conditional` validation rules (previously only the
    top-level rule predicate was checked).

  `@objectstack/formula`: adds `parent` to the record-scope namespace roots —
  master-detail inline grids inject the header record as `parent` for a child
  field's `readonlyWhen`/`requiredWhen` (ADR-0036, #1581), so `parent.status` is
  legitimate, not a bare ref. Verified against the full monorepo build (76 tasks
  clean).

  Not yet covered (separate follow-up — needs a recursive view/page tree walker
  and per-node scope classification): deeply-nested UI visibility predicates
  (`view` element/section `visibleOn`/`condition`, `page` component `visibility`),
  object field-group `visibleOn`, and app-nav `visible` (user/feature-scoped, not
  record-scoped).

- Updated dependencies [d7ff626]
- Updated dependencies [2a1b16b]
- Updated dependencies [e16f2a8]
- Updated dependencies [e411a82]
- Updated dependencies [a581385]
- Updated dependencies [220ce5b]
- Updated dependencies [3efe334]
- Updated dependencies [feead7e]
- Updated dependencies [6ca20b3]
- Updated dependencies [5f875fe]
- Updated dependencies [b469950]
  - @objectstack/spec@10.0.0

## 9.11.0

### Patch Changes

- Updated dependencies [e7f6539]
- Updated dependencies [2365d07]
- Updated dependencies [6595b53]
- Updated dependencies [fa8964d]
- Updated dependencies [36138c7]
- Updated dependencies [a8e4f3b]
- Updated dependencies [4c213c2]
- Updated dependencies [2afb612]
  - @objectstack/spec@9.11.0

## 9.10.0

### Minor Changes

- 1f88fd9: Add `addDays(date, n)` and `addMonths(date, n)` to the CEL standard library — shift an arbitrary date by a (possibly negative) number of days or months. Unlike `daysFromNow`, these operate on a _given_ date (the "next service date = last service + cycle" shape). `addMonths` clamps to the target month's last day (`addMonths(date('2026-01-31'), 1)` → Feb 28, never overflowing into March). Both coerce their inputs (Date | ISO string | epoch) and type `n` as `dyn` so a record number field arriving as a `double` doesn't fault `no such overload` (#1928).

### Patch Changes

- Updated dependencies [db02bd5]
- Updated dependencies [641675d]
- Updated dependencies [94e9040]
- Updated dependencies [1f88fd9]
- Updated dependencies [1f88fd9]
  - @objectstack/spec@9.10.0

## 9.9.1

### Patch Changes

- @objectstack/spec@9.9.1

## 9.9.0

### Minor Changes

- d99a75a: feat(formula): timezone-aware `today()` / `daysFromNow()` / `daysAgo()` (ADR-0053 Phase 2)

  These are now **calendar-day** functions resolved in a reference timezone, threaded from `ExecutionContext.timezone` (#1978) through `EvalContext.timezone` into the CEL stdlib. Each returns the reference-tz calendar day expressed as a **UTC-midnight `Date`** (ADR-0053 decision D1) — the one representation consistent with how `Field.date` strings hydrate, how the SQL driver normalizes date filters, and how Phase 1 stores dates. So `record.close_date == daysFromNow(30)` now matches in-memory too, not just at the storage boundary. The timezone calculation uses `Intl.DateTimeFormat` (DST-safe; no hand-rolled offset math).

  **⚠️ Behavior change:** `daysFromNow(n)` / `daysAgo(n)` previously kept the wall-clock time of `now` (e.g. `daysFromNow(30)` at `10:00Z` → `…T10:00:00Z`). They now drop the time and return the calendar day at **midnight** (`…T00:00:00Z`) — the ADR-0053 "defect #3" fix. `today()` is unchanged at UTC (it already truncated to start-of-day). For a genuine sub-day offset use the documented escape hatch `now() + duration("Nh")`.

  With no reference timezone configured the zone resolves to `UTC`, so `today()` is byte-for-byte unchanged; only the `daysFromNow`/`daysAgo` midnight-truncation differs from before. `objectql` threads `execCtx.timezone` into read-time formula evaluation (`applyFormulaPlan`) and default-value expressions (`applyFieldDefaults`).

  Part of #1980. (Consuming a non-UTC reference timezone end-to-end also needs the `localization` settings manifest noted in #1978.)

- 575448d: feat(formula,email): render `datetime` in a reference timezone (ADR-0053 Phase 2)

  `datetime` template holes now render in a reference timezone's wall-clock when one is supplied, at the presentation boundary — storage stays UTC.

  - **Formula template engine** — the `datetime` formatter takes the reference timezone from `EvalContext.timezone` (threaded in #1980) and passes it to `Intl.DateTimeFormat`. `{{ ts | datetime }}` renders in that zone; `{{ ts | datetime:iso }}` stays UTC (machine-readable). Calendar-day `date` rendering is intentionally **unchanged** (tz-naive — a `Field.date` has no zone). New exported `formatValue(name, value, arg, { locale, timeZone })` makes the whitelisted formatters reusable outside the full CEL template engine.
  - **Email pipeline** — `plugin-email`'s renderer previously bypassed the formatter pipeline (`String()` only), so a datetime went out as raw ISO. Email holes now accept the shared formula formatters — `{{ order.total | currency }}`, `{{ ts | datetime }}` — reusing `formatValue` (single source of truth), while keeping the engine's HTML-escaping and `{{{ }}}` raw-output semantics. `SendTemplateInput.timezone` (mirroring the existing `locale`) flows into rendering so an email's datetime shows the recipient's wall-clock.

### Patch Changes

- Updated dependencies [84249a4]
- Updated dependencies [11af299]
- Updated dependencies [d5774b5]
- Updated dependencies [134043a]
- Updated dependencies [90108e0]
- Updated dependencies [9afeb2d]
- Updated dependencies [6bec07e]
- Updated dependencies [601cc11]
- Updated dependencies [575448d]
  - @objectstack/spec@9.9.0

## 9.8.0

### Minor Changes

- c17d2c8: feat(formula): register the CEL functions the authoring catalog advertises (daysBetween, abs, round, min, max, upper, lower, contains, startsWith, endsWith, matches, len, isEmpty, date, datetime)

  `introspectScope` / `CEL_STDLIB_FUNCTIONS` advertised 25 functions to authors
  (incl. AI), but only 8 were registered — 14 faulted at runtime (`daysBetween`,
  `abs`, `round`, `min`, `max`, `upper`, `lower`, `len`, `isEmpty`, `contains`,
  `startsWith`, `endsWith`, `matches`, plus `date`/`datetime`). Authors were told
  to call functions that don't exist (e.g. `daysBetween` for "days remaining").

  Register the genuinely-useful set in `registerStdLib` with dyn-lenient signatures
  (so a `Field.date` arriving as a string still works) and internal coercion, and
  reconcile the catalog so every advertised entry resolves — guarded by a test that
  evaluates every `CEL_STDLIB_FUNCTIONS` entry. Pure additions; no behavior change
  to existing expressions.

### Patch Changes

- Updated dependencies [97c55b3]
- Updated dependencies [1b1f490]
  - @objectstack/spec@9.8.0

## 9.7.0

### Minor Changes

- ff0a87a: feat(validate): flag bare field references in record-scoped CEL sites at build time

  > **Heads-up for downstream:** this adds a NEW build-time error. A `Field.formula`
  > or validation predicate that references a field bare (`amount` instead of
  > `record.amount`) now fails `objectstack compile`. These expressions were already
  > silently broken at runtime (they evaluated to `null` / never fired), so this is a
  > fix that surfaces a latent bug — but a stack carrying one will go from
  > "builds, silently wrong" to "fails the build" on upgrade. The error message
  > states the exact correction (`write record.<field>`).

  A `Field.formula` and an object validation predicate evaluate against the
  `record` namespace only — there is no field flattening — so a bare top-level
  identifier (`amount`, `status`) resolves to nothing and the expression silently
  evaluates to `null` / never fires. This is the silent-at-runtime class behind
  the broken example-crm formulas (#1927) and is exactly what AI authors get wrong.

  `validateExpression` now takes an evaluation `scope` and, for `scope: 'record'`,
  reports a bare reference with the corrective form (`write record.<field>`). The
  check is schema-free and acts only on cel-js's `Unknown variable` fault, so it
  cannot false-positive on arithmetic/comparison/null-guard type overloads. Flow
  and automation conditions keep the default `scope: 'flattened'` — the record's
  fields ARE spread to top-level there (alongside flow variables), so bare refs
  are correct and are NOT flagged. `objectstack compile` wires `record` scope for
  field formulas and validation predicates; flow conditions stay flattened.

### Patch Changes

- 82c7438: fix(formula): register mixed `double <op> int` arithmetic overloads so number-field formulas compute

  cel-js types a record field number as `double` and a bare integer literal as
  `int`, and ships overloads only for matching numeric pairs. So an everyday
  formula like `record.amount / 100` or `record.price * 2` faulted at runtime
  (`no such overload: dyn<double> / int`); the engine caught the fault and the
  formula silently evaluated to `null` — passing build, empty at runtime (#1928).

  The CEL engine now registers the missing `double <op> int` / `int <op> double`
  overloads for `+ - * / %`, computing the result as a `double` (CEL's mixed-numeric
  promotion). Pure `int op int` is untouched, so integer division (`7 / 2 == 3`)
  keeps its semantics — the overloads fire only when the operands are genuinely a
  `double` and an `int`. Authors no longer need the `/ 100.0` float-literal workaround.

- 417b6ac: feat(validate): advisory did-you-mean warnings for likely field typos in flow conditions

  Adds a non-blocking warning channel to build-time expression validation (#1928
  tier 3). Flow / automation conditions flatten the record's fields to top-level,
  so a bare `status` is correct — but a bare NON-field identifier is either a flow
  variable or a typo. When it is a near-miss of a known field (edit distance), the
  build now emits a `did you mean \`status\`?`warning instead of staying silent,
WITHOUT failing the build (a genuine flow variable won't be close to a field
name, so it stays quiet).`ExprValidationResult`gains a`warnings`array and`ExprIssue`a`severity`; `objectstack compile` prints warnings and only fails on
  errors. This closes the silent-skip gap for misspelled trigger-condition fields
  (the #1877 family) without the false-positive risk of a hard gate.

  - @objectstack/spec@9.7.0

## 9.6.0

### Patch Changes

- bb00a50: fix(formula): catch unknown functions in CEL conditions at build (#1877)

  `compile()` discarded cel-js's type-check verdict because `check()` returns a `TypeCheckResult` object (`{ valid, error }`), not an array — so the `Array.isArray(checkErrors)` guard never matched. A condition calling an unknown function (`PRIOR(status)`, a typo'd `isBlnk(...)`) type-checks as `found no matching overload`, but that result never surfaced, so `objectstack compile`, `registerFlow`, and the `validate_expression` tool all accepted the predicate, which then silently no-op'd the flow at runtime. Now reads the documented `{ valid, error }` shape, closing the gap for flow conditions, validation rules, and field formulas at once.

- Updated dependencies [d1e930a]
- Updated dependencies [71578f2]
- Updated dependencies [5e3a301]
- Updated dependencies [5db2742]
  - @objectstack/spec@9.6.0

## 9.5.1

### Patch Changes

- Updated dependencies [ee72aae]
  - @objectstack/spec@9.5.1

## 9.5.0

### Patch Changes

- Updated dependencies [d08551c]
- Updated dependencies [707aeed]
- Updated dependencies [7a103d4]
- Updated dependencies [4b01250]
  - @objectstack/spec@9.5.0

## 9.4.0

### Patch Changes

- Updated dependencies [060467a]
- Updated dependencies [0856476]
- Updated dependencies [b678d8c]
- Updated dependencies [b678d8c]
- Updated dependencies [b678d8c]
  - @objectstack/spec@9.4.0

## 9.3.0

### Patch Changes

- Updated dependencies [1ada658]
- Updated dependencies [3219191]
- Updated dependencies [290f631]
- Updated dependencies [50b7b47]
- Updated dependencies [f15d6f6]
- Updated dependencies [f8684ea]
- Updated dependencies [b4765be]
  - @objectstack/spec@9.3.0

## 9.2.0

### Patch Changes

- Updated dependencies [2f57b75]
- Updated dependencies [2f57b75]
  - @objectstack/spec@9.2.0

## 9.1.0

### Patch Changes

- Updated dependencies [b9062c9]
  - @objectstack/spec@9.1.0

## 9.0.1

### Patch Changes

- Updated dependencies [1817845]
  - @objectstack/spec@9.0.1

## 9.0.0

### Patch Changes

- Updated dependencies [4c3f693]
- Updated dependencies [0bf39f1]
- Updated dependencies [f533f42]
- Updated dependencies [1c83ee8]
  - @objectstack/spec@9.0.0

## 8.0.1

### Patch Changes

- @objectstack/spec@8.0.1

## 8.0.0

### Patch Changes

- Updated dependencies [a46c017]
- Updated dependencies [b990b89]
- Updated dependencies [99111ec]
- Updated dependencies [d5a8161]
- Updated dependencies [5cf1f1b]
- Updated dependencies [9ef89d4]
- Updated dependencies [3306d2f]
- Updated dependencies [bc44195]
- Updated dependencies [9e2e229]
  - @objectstack/spec@8.0.0

## 7.9.0

### Patch Changes

- @objectstack/spec@7.9.0

## 7.8.0

### Patch Changes

- f01f9fa: fix(formula): hydrate ISO date/datetime strings on CEL `no such overload` fault (#1530)

  Date-typed formula fields and date predicates always evaluated to `null`:
  `Field.date`/`Field.datetime` serialize to ISO strings, and cel-js compared the
  raw string against the `google.protobuf.Timestamp` from `today()`/`now()`/
  `daysFromNow()`, raising `no such overload` (swallowed to null). The existing
  numeric-string fault-retry (#1534) is now extended to also coerce strict ISO-8601
  date/date-time strings to `Date` before retrying once, fixing every caller
  (formula fields, flow conditions, validation/workflow predicates). Hydration runs
  only after a fault, so clean expressions are never re-interpreted and genuine
  non-temporal strings still fault loudly.

- Updated dependencies [06f2bbb]
- Updated dependencies [36719db]
- Updated dependencies [424ab26]
  - @objectstack/spec@7.8.0

## 7.7.0

### Patch Changes

- 825ab06: fix(formula): hydrate string-serialized numeric fields in CEL comparisons (#1534)

  Numeric fields that serialize as strings — `Field.rating(allowHalf)` → `"5.0"`, `Field.currency(scale)` → `"250000.00"`, `Field.percent` — made comparisons like `record.rating >= 4` fault under strict CEL with `no such overload: dyn >= int`. In flow decision/edge conditions this silently dead-ended the run (no edge matched), and in objectql `applyFormulaPlan` it swallowed to `null`.

  The CEL engine now retries an evaluation **once** with purely-numeric strings hydrated to numbers, but only after a `no such overload` fault — so a comparison that already type-checks is never re-interpreted (a zip like `"02134"` stays a string in `record.zip == "02134"`). Because both the automation condition path (`service-automation` `evaluateCondition`) and the objectql formula path route through `ExpressionEngine.evaluate`, both are fixed consistently. A genuinely non-numeric operand (e.g. `record.rating >= 4` where `rating` is `"high"`) still faults loudly rather than being silently rescued.

- Updated dependencies [b391955]
- Updated dependencies [f06b64e]
- Updated dependencies [023bf93]
  - @objectstack/spec@7.7.0

## 7.6.0

### Minor Changes

- c4a4cbd: ADR-0032 (phase 1): validate-by-default expression layer — no silent failure.

  Kills the #1491 class where a malformed predicate (e.g. the `{record.x}`
  template-brace-in-CEL mistake) silently evaluated to `false` and made a flow
  "fire" with no effect:

  - **service-automation**: flow `evaluateCondition` no longer swallows CEL
    failures to `false` — it throws an attributed, corrective error; and
    `registerFlow` now parse-validates every predicate (start/decision/edge
    condition) at registration, failing loudly with the offending location +
    source + the fix.
  - **formula**: new shared validator — `validateExpression(role, src, schema?)`,
    `introspectScope`, `CEL_STDLIB_FUNCTIONS` — with schema-aware field-existence
    - did-you-mean. The `{{ }}` template engine gains a formatter whitelist
      (`currency`/`number`/`percent`/`date`/`datetime`/`truncate`/`upper`/`lower`/
      `default`/…) with defined value→string semantics; arbitrary logic in holes is
      rejected. Plain `{{ path }}` stays back-compatible.
  - **cli**: `objectstack compile` validates every flow / validation-rule /
    field-formula predicate against the resolved object schema and fails the
    build with located, corrective messages.
  - **service-ai**: new agent-callable `validate_expression` tool so authoring
    agents self-correct before committing.
  - **spec**: fix the `FlowSchema` JSDoc example that taught the bad
    `condition: "{amount} < 500"` single-brace form.

### Patch Changes

- Updated dependencies [955d4c8]
- Updated dependencies [c4a4cbd]
- Updated dependencies [b046ec2]
- Updated dependencies [2170ad9]
- Updated dependencies [02d6359]
- Updated dependencies [7648242]
- Updated dependencies [8fa1e7f]
- Updated dependencies [55866f5]
- Updated dependencies [60f9c45]
  - @objectstack/spec@7.6.0

## 7.5.0

### Patch Changes

- @objectstack/spec@7.5.0

## 7.4.1

### Patch Changes

- @objectstack/spec@7.4.1

## 7.4.0

### Patch Changes

- Updated dependencies [23c7107]
- Updated dependencies [c72daad]
- Updated dependencies [f115182]
- Updated dependencies [2faf9f2]
- Updated dependencies [2faf9f2]
- Updated dependencies [2faf9f2]
- Updated dependencies [58b450b]
- Updated dependencies [82eb6cf]
- Updated dependencies [13d8653]
- Updated dependencies [ff3d006]
- Updated dependencies [5e831de]
  - @objectstack/spec@7.4.0

## 7.3.0

### Patch Changes

- Updated dependencies [5e7c554]
  - @objectstack/spec@7.3.0

## 7.2.1

### Patch Changes

- @objectstack/spec@7.2.1

## 7.2.0

### Patch Changes

- @objectstack/spec@7.2.0

## 7.1.0

### Patch Changes

- Updated dependencies [47a92f4]
  - @objectstack/spec@7.1.0

## 7.0.0

### Patch Changes

- Updated dependencies [74470ad]
- Updated dependencies [d29617e]
- Updated dependencies [dc72172]
  - @objectstack/spec@7.0.0

## 6.9.0

### Patch Changes

- @objectstack/spec@6.9.0

## 6.8.1

### Patch Changes

- @objectstack/spec@6.8.1

## 6.8.0

### Patch Changes

- Updated dependencies [6e88f77]
- Updated dependencies [c8b9f57]
  - @objectstack/spec@6.8.0

## 6.7.1

### Patch Changes

- @objectstack/spec@6.7.1

## 6.7.0

### Patch Changes

- Updated dependencies [430067b]
- Updated dependencies [4f9e9d4]
  - @objectstack/spec@6.7.0

## 6.6.0

### Patch Changes

- Updated dependencies [a49cfc2]
  - @objectstack/spec@6.6.0

## 6.5.1

### Patch Changes

- @objectstack/spec@6.5.1

## 6.5.0

### Patch Changes

- @objectstack/spec@6.5.0

## 6.4.0

### Patch Changes

- Updated dependencies [f8651cc]
- Updated dependencies [f8651cc]
- Updated dependencies [0bf6f9a]
  - @objectstack/spec@6.4.0

## 6.3.0

### Patch Changes

- @objectstack/spec@6.3.0

## 6.2.0

### Patch Changes

- Updated dependencies [b4c74a9]
  - @objectstack/spec@6.2.0

## 6.1.1

### Patch Changes

- @objectstack/spec@6.1.1

## 6.1.0

### Patch Changes

- Updated dependencies [93c0589]
  - @objectstack/spec@6.1.0

## 6.0.0

### Patch Changes

- Updated dependencies [629a716]
- Updated dependencies [dbc4f7d]
- Updated dependencies [944f187]
  - @objectstack/spec@6.0.0

## 5.2.0

### Patch Changes

- Updated dependencies [bab2b20]
- Updated dependencies [fa011d8]
- Updated dependencies [b806f58]
  - @objectstack/spec@5.2.0

## 5.1.0

### Patch Changes

- Updated dependencies [75f4ee6]
- Updated dependencies [823d559]
  - @objectstack/spec@5.1.0

## 5.0.0

### Patch Changes

- Updated dependencies [2f9073a]
  - @objectstack/spec@5.0.0

## 4.2.0

### Patch Changes

- Updated dependencies [2869891]
  - @objectstack/spec@4.2.0

## 4.1.1

### Patch Changes

- @objectstack/spec@4.1.1

## 4.1.0

### Patch Changes

- Updated dependencies [2108c30]
- Updated dependencies [23db640]
  - @objectstack/spec@4.1.0

## 4.0.5

### Patch Changes

- 15e0df6: chore: unify all package versions to a single patch release
- Updated dependencies [15e0df6]
  - @objectstack/spec@4.0.5
