Skip to content

Commit 2581efb

Browse files
fix(service-automation): get_record form descriptor's limit declares minimum 1 (#25441)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuACdPWFJTLDcTrByFCHLzqb)_ Fixes #25361 Clause-②: no Metadata-protocol: none ## What The `get_record` node's `limit` is a positive integer in the contract (`GetRecordConfigSchema.limit`, `z.number().int().positive()`, narrowed by PR #25357), so every flow save door refuses `0`, a negative or a fraction. The executor's form descriptor in `packages/services/service-automation/src/builtin/crud-nodes.ts` still declared `limit: { type: 'integer', title: 'Limit' }`, so the designer form offered values the save always refuses. - The descriptor's `limit` now declares `minimum: 1`, the contract's floor. - Landing: the claimed file, `builtin/crud-nodes.ts`. No `packages/spec` edit, no `engine.ts` edit. - Derivation was considered: no node descriptor in `builtin/` is derived from its spec Zod today. Every descriptor is a hand-written literal, and the bounded siblings (`loop.maxIterations`, `try_catch.retry.*`) declare `minimum`/`maximum` by hand, so this follows that pattern. ## Pin `builtin-node-form-zod-ledger.test.ts` gains `get_record.limit: the form's minimum is the contract's refusal boundary`. It reads the descriptor's `limit.minimum` (asserts `1`), then parses `GetRecordConfigSchema` at that boundary: the minimum is accepted, one below it is refused, and `0` is refused. Neither side can move alone. ## Measurement: other `builtin/` descriptors against their contracts Every numeric property in `builtin/` descriptors was compared with its spec Zod at this tree: | Descriptor key | Descriptor | Contract | Agree | |:---|:---|:---|:---| | `get_record.limit` | integer, now `minimum: 1` | `int().positive()` | yes (this PR) | | `loop.maxIterations` | integer, `minimum: 1`, `maximum: LOOP_MAX_ITERATIONS_CEILING` | `int().min(1).max(LOOP_MAX_ITERATIONS_CEILING)` | yes | | `try_catch.retry.maxRetries` | integer 0..10 | `int().min(0).max(10)` | yes | | `try_catch.retry.backoffMs` / `maxRetryDelayMs` | integer, `minimum: 0` | `int().min(0)` | yes | | `try_catch.retry.backoffMultiplier` | number, `minimum: 1` | `min(1)` | yes | | `http.timeoutMs` | number | `z.number()` (unbounded) | yes | | `screen.fields[].min` / `max` | number | `z.number()` (unbounded) | yes | No other mismatch was found. ## Verification (head `21ecd5680e`) - `pnpm --filter @objectstack/service-automation typecheck && pnpm --filter @objectstack/service-automation test`: lock VERDICT command-exit 0; Test Files 198 passed (198), Tests 2619 passed (2619). - Ablation (committed fix first, through `scripts/ablation-replace.mjs`): the anchor `minimum: 1` was removed (anchor 1 to 0, blob `b7d8ec771a76` to `5c181206f838`). The new pin went red: `AssertionError: expected undefined to be 1`, 1 failed and 9 passed. The file was restored with blob == HEAD and `git diff HEAD` empty. The test imports `crud-nodes.js` from `src`, so no `dist/` hop is involved. - Derived gate families (`dispatch-gates.mjs --commands`, 67): 66 exit 0. `check:dual-build-cjs-loads` exited 3 with PREREQUISITE NOT MET (no full-repo `dist/`), so it is NOT MEASURED locally and is left to CI. ## Acceptance notes - None. --- _Generated by [Claude Code](https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8d9e8b9 commit 2581efb

3 files changed

Lines changed: 32 additions & 1 deletion

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
fix: the `get_record` designer form no longer offers a `limit` below 1
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The `get_record` node's `limit` is a positive integer, and every flow save door refuses `0`, a negative or a fraction. The executor's form descriptor still declared `limit` as a bare integer, so the designer form offered values the save always refused. The descriptor's `limit` now declares `minimum: 1`, the same floor as the contract. Nothing a flow can store changes; only the form stops offering what the save refuses.

‎packages/services/service-automation/src/builtin/builtin-node-form-zod-ledger.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,24 @@ describe('CRUD / screen / map form ↔ Zod reconciliation (#4045)', () => {
136136
).toEqual([]);
137137
});
138138

139+
it('get_record.limit: the form\'s minimum is the contract\'s refusal boundary', () => {
140+
// The contract refuses a `limit` below 1 at every save door; a form whose
141+
// floor sits lower offers values the save always refuses. Pin both sides
142+
// at the boundary the descriptor declares, so neither moves alone.
143+
const limit = formSchema('get_record').properties?.limit as
144+
| { type?: string; minimum?: number }
145+
| undefined;
146+
expect(limit?.type).toBe('integer');
147+
expect(limit?.minimum).toBe(1);
148+
149+
const parse = (value: number) =>
150+
(GetRecordConfigSchema as unknown as { safeParse(v: unknown): { success: boolean } })
151+
.safeParse({ objectName: 'account', limit: value }).success;
152+
expect(parse(limit!.minimum!), 'the contract accepts the form\'s minimum').toBe(true);
153+
expect(parse(limit!.minimum! - 1), 'the contract refuses one below the form\'s minimum').toBe(false);
154+
expect(parse(0), 'the contract refuses 0').toBe(false);
155+
});
156+
139157
describe('assignment: pinned as un-reconcilable, with the reason on record', () => {
140158
// `assignment` deliberately has NO entry in builtin-node-config.zod.ts: with
141159
// no `assignments` wrapper, the TOP-LEVEL config keys ARE the author's

‎packages/services/service-automation/src/builtin/crud-nodes.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,10 @@ export function registerCrudNodes(engine: AutomationEngine, ctx: PluginContext):
477477
type: 'array', items: { type: 'string' }, title: 'Fields',
478478
description: 'Field projection — only these fields are read (default: all).',
479479
},
480-
limit: { type: 'integer', title: 'Limit' },
480+
// `minimum: 1` mirrors the contract: `GetRecordConfigSchema.limit`
481+
// is a positive integer, refused below 1 at every flow save
482+
// door, so the form never offers a value the save refuses.
483+
limit: { type: 'integer', minimum: 1, title: 'Limit' },
481484
outputVariable: { type: 'string', title: 'Output variable' },
482485
},
483486
required: ['objectName'],

0 commit comments

Comments
 (0)