You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 303aef0
Browse filesBrowse the repository at this point in the historyBrowse files
fix(spec)!: a flow get_record node's limit is declared a positive integer, so a fraction, 0 or a negative is refused at every door that parses a flow (#25357)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoBtFLBZ6RGauULKM9CboqUv)_Fixes#25302
Clause-②: no (narrowing)
Metadata-protocol: narrowed
## What changes
`GetRecordConfigSchema.limit`
(`packages/spec/src/automation/builtin-node-config.zod.ts`) goes from
`z.number()` to `z.number().int().positive()`. The `get_record` executor
(`packages/services/service-automation/src/builtin/crud-nodes.ts`)
parses its config against this contract, then calls `data.find` with
`limit` as written when `limit > 1`, and `findOne` otherwise. It does
not go through `findData`, so the Data API's page-window normalizer
never judges it. Before this change `limit: 2.5` reached the driver as
its LIMIT, and `0`, `-1` or `0.5` silently read one record.
The executor contract is the one judge every flow door shares
(`flowNodeConfigRefusals`, the value half). Narrowing it refuses the
value at `nodes.N.config.limit` with `node-config-refused-by-contract`
at `FlowSchema.parse`, `defineStack` (`STACK_SCHEMA_INVALID`, 422),
`objectstack validate` / `compile`, the registered `flow` type schema
the metadata save door validates against,
`AutomationEngine.registerFlow`, and an artifact's parse. No consumer
code changes.
- **Why not reuse the PR #25300 exports.** `QueryPageWindowSchema` is
`int().nonnegative()`, and `0` means "the total alone" at a list door. A
`get_record` node always reads at least one row, so `0` there is not a
window. The bound is its own, and the TSDoc says why.
- **ADR-0087 disposition:** `not-required (already-registered
flow-builtin-node-config-values-refused)`, amended in this diff. That D3
entry already covers "a value outside the declared set or range" for
builtin node configs. Its surface, replacement and reason now name
`get_record` `limit`. The reason also says the one difference: such a
node used to run rather than fail. No D2 conversion, because only the
author knows the count they meant.
- **Changeset:**
`.changeset/25302-get-record-limit-positive-integer.md`,
`@objectstack/spec` `minor`, with FROM → TO and the one-line fix.
- **Generated:**
`content/docs/references/automation/builtin-node-config.mdx`
(`check:generated --fix`, 1 of 14 artifacts stale: `check:docs`). The
JSON Schema (gitignored) now reads `type: integer`, `exclusiveMinimum:
0`.
## Producers measured
- **This repo** at `97967a9`: 70 `get_record` nodes in `*.ts`. Every
`limit` within 14 lines of one is a positive whole number (200 ×4, 5 ×5,
50 ×2, 10 ×2). The one other value is the string `'{page.size}'` in
`canonicalize-stored-flow.test.ts`, which is already refused as the
wrong type and still throws `/limit/`. Query: `git grep -n -A14 -E
"type: ?['\"]get_record['\"]"` piped to a grep for `limit:`.
- **HotCRM** at `4166f21` (current `main`): 42 `get_record` nodes. 12
`limit`s, all positive whole numbers (500 ×7, 1000 ×2, 1 ×1, and
`BILLING_HANDOFF_LINE_ITEM_LIMIT` = 200 ×2).
## Tests
At head `d972d18` (the spec diff is unchanged since `e92f351`; the merge
brought plugin-security/plugin-sharing only):
- `flow-builtin-node-config-values.test.ts`: probes `limit` 2.5 / 0 / -1
at the judge, and a new `get_record limit is a positive integer` block.
That block covers a fraction at `FlowSchema` in the contract's words;
`defineStack` asserting `code` + `status` (`STACK_SCHEMA_INVALID` / 422)
and the path, for 2.5, 0, -1, 0.5 and an unsafe integer; and the
save-door schema. Controls: `limit` 1, 2 and 200 parse, and no `limit`
parses.
- `@objectstack/spec` test: 709/709 files, 21566 passed. typecheck: exit
0.
- Consumers (filter direction: downstream readers of the schema):
`@objectstack/service-automation` 198/198 files, 2618 tests;
`@objectstack/lint` 153/153, 7076; `@objectstack/metadata-protocol` 259
passed + 3 skipped, 21416 passed. typecheck exit 0 for all three. No
fixture needed triage, because no fixture carries a non-positive or
fractional `limit`.
- **Ablation** (`scripts/ablation-replace.mjs`, mutation
`int().positive()` removed): the test file went 10 failed / 59 passed,
which is exactly the 10 refusal cases. The controls stayed green.
Restored: blob equals HEAD `a900d3e7` and `git diff HEAD` is empty.
Spec's own tests import `src`, so no build leg applies.
- Dogfood: see the report comment on the card (it runs on the final
head).
## Gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 113 families for this diff. Each was
run locally with its exit code captured before any pipe. 112 exited 0.
`pnpm check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET: it
reads the dist of about 40 packages this diff does not build), so it is
NOT MEASURED locally and left to CI's `Lint & Repo Gates`.
`check:skill-examples` first exited 3 (no client dist), then exited 0
after building `@objectstack/client` and `client-react`. Three gates
first exited 3 on a shallow clone or a missing lint dist, then exited 0
after `git fetch --unshallow` and building the lint closure.
## Acceptance notes
- The executor's own form descriptor in `crud-nodes.ts` declares `limit:
{ type: 'integer', title: 'Limit' }` with no `minimum`. So a designer
form built from it offers `0`, which the save door now refuses with the
contract's words. That is outside this card's file surface. Noted, not
filed: declared ≠ enforced does not apply, because the refusal is loud
at save.
- `if (limit && limit > 1)` in the executor is now exact: `limit` is
either absent or a whole number of at least 1.
---
_Generated by [Claude
Code](https://claude.ai/code/session_013XF6vcPvtFkNY5BmaMGzcd)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(spec)!: a flow `get_record` node's `limit` is declared a positive integer, so a fraction, `0` or a negative is refused at every door that parses a flow
6
+
7
+
Clause-②: no (narrowing)
8
+
Metadata-protocol: narrowed
9
+
10
+
<!-- adr-0087: not-required (already-registered flow-builtin-node-config-values-refused) registered before this change, amended in this diff: the entry's surface, replacement and reason now name a get_record limit that is not a positive integer, and the stored-row listing reports such a node under that id -->
11
+
12
+
**BREAKING** (a value the parse accepted is now refused), shipped as `minor` under the 18.x convention for breaking changes, with no retirement stage.
13
+
14
+
The `get_record` executor reads around the Data API's page-window normalizer: with `limit` above 1 it calls `data.find` with that LIMIT as written, and otherwise it calls `findOne`. The spec declared `limit` as any number, so a flow could declare a window the runtime did not honour as written.
15
+
16
+
-**FROM:**`GetRecordConfigSchema.limit` was `z.number()`. `limit: 2.5` parsed and reached the driver as `LIMIT 2.5`; `limit: 0`, `limit: -1` and `limit: 0.5` parsed and silently read one record through `findOne`.
17
+
-**TO:**`limit` is `z.number().int().positive()`. Every door that parses a flow refuses any other number at `nodes.N.config.limit` with the code `node-config-refused-by-contract`: `FlowSchema.parse`, `defineStack` (`STACK_SCHEMA_INVALID`, 422), `objectstack validate` and `objectstack compile`, the metadata save door, `AutomationEngine.registerFlow`, and an artifact's parse. The generated JSON Schema declares `type: integer` with an exclusive minimum of 0.
18
+
-**Fix:** write the count as a whole number of at least 1. `limit: 1` (or no `limit`) reads one record as `record`; a larger whole number reads a `records` list of at most that many.
19
+
-**Unchanged:** every positive whole `limit`, and a node with no `limit`. A string `limit` (`'10'`, `'{page.size}'`) was already refused as the wrong type.
20
+
-**Not the Data API's page window.**`QueryPageWindowSchema` admits `0`, which asks a list door for the total alone. A `get_record` node always reads at least one row, so its bound is its own.
21
+
22
+
Data at rest: a flow already stored with such a `limit` is refused whole when it registers. At boot it is skipped with a `warn` naming it and its trigger is not armed, while the flows beside it register. `objectstack migrate meta` reports it under `flow-builtin-node-config-values-refused`. No conversion rewrites it, because only the author knows the count they meant.
23
+
24
+
No producer writes such a value: every `get_record``limit` in this repository's examples and tests, and in HotCRM, is a positive whole number.
Copy file name to clipboardExpand all lines: content/docs/references/automation/builtin-node-config.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -233,7 +233,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
233
233
|**objectName**|`string`| ✅ | Object to query |
234
234
|**filter**|`Record<string, any>`| optional | Field/value pairs to match: each operand — a field's value, an operator's comparand (`{"$ne": null}`), a list comparand's element — is a CEL value envelope `{ dialect: 'cel', source }` evaluated before the query (one that evaluates to null refuses the node), a query placeholder such as `{current_user_id}` or `{today}` the query engine resolves, or a literal; a flow `{…}` template token is refused |
235
235
|**fields**|`string[]`| optional | Field projection — only these fields are read (default: all) |
236
-
|**limit**|`number`| optional | Max records to return; >1 switches to a multi-record query |
236
+
|**limit**|`integer`| optional | Max records to return — a positive integer; >1 switches to a multi-record query |
237
237
|**outputVariable**|`string`| optional | Flow variable the result is bound to — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}`|
+'contract refuses — a value of the wrong type (create_record outputVariable 42, a screen field min '
22
26
+'written as the string 1, get_record limit as a string, update_record multi as a string), a value '
23
27
+'outside the declared set or range (notify severity loud, screen mode view, loop maxIterations 0, '
24
-
+'try_catch retry.maxRetries above 10), an empty script function or subflow flowName, or a rule '
28
+
+'try_catch retry.maxRetries above 10, get_record limit 2.5 or 0 where a positive integer is declared), an empty script function or subflow flowName, or a rule '
25
29
+'finding on present keys (a notify template beside an inline title). A value carrying a token in '
26
30
+'braces is judged like any other (get_record limit written as a token, refused as the wrong type), '
27
31
+'and at a key its executor reads as written (a notify sourceObject, actorId or payload; an http '
0 commit comments