Commit 9801da1
Fixes #20386
Clause-②: no (narrowing)
A `progress` field's declared `min` / `max` are now **enforced at the
objectql write seam**, per triage `5865053231` (ENFORCE, no decision
card). A `progress` write outside a declared bound is refused with `400
VALIDATION_FAILED` and the `number` field's own field codes, `max_value`
/ `min_value`. `scale` and `precision` stay unread on `progress`.
Measured head: **`af9e5101a`**.
## What changes
- **`packages/objectql/src/validation/record-validator.ts`, the number
arm.** The `if (t === 'progress') return null;` early return moves from
above the `min` / `max` checks to directly below them, and above `scale`
/ `precision`. The #20308 docblock that deferred this now says why the
bounds bind and why the return stays above `scale` / `precision`: each
of those keys' own `.describe()` names a type set `progress` is not in.
- **The file header's `min` / `max` line** now lists `progress`. It
named the five types that were enforced, so leaving it would have made
it false. This is one line outside "the number arm and the #20308
docblock" (declared below as a deviation). It is line 31, far from the
date line PR #20469 edits (line 55 on `main`).
- **Tests.** `record-validator.blank-typed-value.test.ts` pinned the old
boundary (`progress` `max: 100` accepting `150.5`). It now pins what
stays true: `summary` reads no bound or `scale`, and `progress` reads no
`scale`. One test name in `record-validator.precision.test.ts` said
`progress`'s "bounds the numeric branch never reads", and it is
reworded. Its assertion is unchanged.
- **`.changeset/20386-progress-min-max-enforced.md`**:
`@objectstack/objectql` `minor`, **BREAKING** banner, the `Clause-②`
line, a before → after line and the ADR-0087 disposition (below).
## Measured premises (dispatch zone 2)
- **H1: `progress` bounds are skipped on `origin/main`. Held.**
Reproduced on `dc0ab6a2e` through the real `RestServer` `POST
/api/v1/data/:object` handler, over a real `ObjectQL` engine on the
SQLite `SqlDriver` and on the memory driver. This was a scratch harness,
not committed, because `check:driver-memory-census` refuses a new
driver-memory consumer.
| field | value | SQLite before | memory before | both after
(`5f5bc7580`) |
|:--|:--|:--|:--|:--|
| `progress`, `max: 100` | `150` | 201, stored `150` (real) | 201,
stored `150` | 400 `VALIDATION_FAILED` / `max_value` `{ max: 100 }`, no
row |
| `progress`, `min: 0` | `-5` | 201, stored `-5` (real) | 201, stored
`-5` | 400 `VALIDATION_FAILED` / `min_value` `{ min: 0 }`, no row |
| `number`, `max: 100` (control) | `150` | 400 / `max_value`, no row |
the same | unchanged |
| `number`, `min: 0` (control) | `-5` | 400 / `min_value`, no row | the
same | unchanged |
| `progress` in bounds / on each bound | `50`, `0`, `100` | 201 | 201 |
201, stored unchanged |
- **H2: `scale` / `precision` after the move. Held, with a measured
boundary.** With the return placed below the bounds, `scale` and
`precision` are still not enforced on `progress`: `33.5` under `scale:
0` gets 201, and `99.5` under `precision: 2` gets 201, on SQLite and
memory. Deleting the return outright would start both. Measured by
ablation (below): four pins turn red with `max_scale` / `max_precision`.
So the placement is load-bearing, and it is pinned from both sides.
- **H3: producers. Zero writes outside a declared bound.**
- objectui `SliderField`, the `progress` editor
(`FieldEditWidget.tsx:87` `progress: SliderField`), is byte-identical at
the pinned `.objectui-sha` `dd3f7e1be3` and at objectui HEAD `b8e0941`.
It passes `min = field.min ?? 0` and `max = field.max ?? 100` to
`@radix-ui/react-slider` (`^1.4.7`). That component's `updateValues`
does `clamp(snapToStep, [min, max])` before every `onValueChange` (read
in the 1.4.7 tarball). So it cannot emit a value outside a declared
bound.
- Example apps, on `dc0ab6a2e`: 2 `progress` fields declare bounds,
`showcase_task.progress` and the field zoo's `f_progress`, both `min: 0,
max: 100`. Their writers are 12 seed rows, the `showcase_mark_done`
action (`progress: 100`) and the dogfood field-zoo matrix (`60`). All of
them are in bounds.
## Pins
-
`packages/objectql/src/validation/record-validator.progress-bounds.test.ts`
(new, 14 tests):
- the triage pins (`150` gets `max_value` `{ max: 100 }`, `-5` gets
`min_value` `{ min: 0 }`, and in-bounds values plus both inclusive
bounds are accepted);
- envelope equality with the `number` refusal;
- one bound declared alone, and no invented 0..100 bound when none is
declared;
- update mode, string-carried values, and an omitted field that is never
re-read;
- ⛔ `scale` / `precision` unread on `progress`, while the same
declaration on `slider` refuses;
- every engine write door through a stub driver: insert of one row and
of an array, `insertMany` partial success, update by id and by
predicate, the `validate` dry run, and a control showing that in-bounds
values arrive as the same number.
- `packages/rest/src/rest-data-progress-bounds.test.ts` (new, 6 tests),
on the real `RestServer` routes over SQLite, reading the physical column
past every read coercion:
- POST, batch create, PATCH, batch update and updateMany refuse `150` /
`-5` with the `number` field's envelope and write or change nothing;
- controls: in-bounds values and both bounds are stored unchanged, and
`33.5` writes under `scale: 0, precision: 2`.
## Verification
Heavy runs went through `scripts/pm/os-verify-lock.sh`. All readings are
at `af9e5101a` unless stated otherwise.
- **Build.** `pnpm turbo run build --filter='@objectstack/rest...'
--concurrency=2`: 25/25, VERDICT command-exit 0. It was re-run after
each merge of `main` (last at `5c4148234`). The objectql source has not
changed since.
- **objectql.**
- Validator and door suites (`progress-bounds`, `blank-typed-value`,
`precision`, `number-value`, `record-validator`,
`engine-number-value-door`, `engine-blank-typed-value-door`): 7 files,
333/333.
- Full `--project local`: 328 files, 6081/6081 (at `6a029a923`, before
the second merge, which brought only spec and driver-sql commits).
- `typecheck` (tsc, scripts, and `check:test-typecheck`, whose
`tsconfig.test.json` includes `src/**/*`): exit 0.
- **rest.** `rest-data-progress-bounds`, `rest-data-number-value`,
`rest-data-blank-typed-value` and `import-integration`: 4 files,
100/100. `typecheck` including `check:test-typecheck`: exit 0.
- **Reverse verification.**
- **REST pin, with a build.** Against the `dist/` built from base
`dc0ab6a2e`, the REST pin read **5 failed / 1 passed**. Each failure was
`expected 201 to be 400` or a batch row reporting success. The one green
is the in-bounds control. The `dist/index.js` number arm was read
directly before and after: the return sat above the bounds, then below
`max`. After `pnpm --filter @objectstack/objectql build` at `5f5bc7580`
the pin reads 6/6.
- **Ablation 1, fix committed first (`3b7b55406`).**
`scripts/ablation-replace.mjs` re-planted `if (t === 'progress') return
null;` above the bounds: anchor 1 → 0, blob `9ede5b5b` → `d396c53a`.
Result: the new objectql file reads **10 failed / 4 passed**. The 4
greens are the controls: in-bounds values, `scale` unread, `precision`
unread, and the engine in-bounds control. Restored: blob `9ede5b5b`
equals HEAD, and `git diff HEAD` is empty. The subject resolves through
a relative import to `src/`, so no rebuild was involved. Direction: red.
- **Ablation 2, the H2 reading.** The same tool deleted the return: blob
`9ede5b5b` → `d683b83e`. **4 failed**: the two `progress-bounds` pins
for `scale` / `precision`, the `blank-typed-value` `scale` pin, and the
`precision` test that excludes `progress`. Restored the same way.
Direction: red.
- **Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `af9e5101a` derived 64 commands. **62
exit 0**. 2 are **NOT MEASURED** (exit 3, PREREQUISITE NOT MET):
`check:dual-build-cjs-loads` and `check:type-check-debt` both need a
whole-repo build. `--ran` reconciliation: 64 derived, 62 run, 2
NOT-MEASURED (derived from exit 3), 0 UNRUN, exit 0.
- First pass at `5c4148234`: `check:error-code-casing` exit 1 on four
bare `{ code: 'max_value' }` style assertions. They are now
field-addressed (`af9e5101a`), and the gate reads 0.
- **Lint, a declared narrowing.** `eslint --no-inline-config --format
json` on the 5 changed `.ts` files: 5 files, 0 errors, 0 warnings.
- Population: `--print-config` applies the config's rules to each file
(6 rules on the validator, 5 on the REST test).
- Invariance: `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`), so this diff cannot move any untouched file's
verdict.
- The repo-wide `pnpm lint` is declared to CI.
- **Not run locally, declared to CI:** the rest of the rest suite,
runtime and dogfood, and the 6 path-matched families that take a value
from the workflow.
## ADR-0087 disposition: which precedent, and why
`not-required (no-migration-prescription)`, following **PR #20423**, not
#7501:
- #20423 is the closer precedent: the same arm, the same kind of change
(a declared numeric bound starting to bind at the write seam, a
narrowing of the write accept set with no authored key moving), and a
gate-era marker.
- #7501's changeset (`number-scale-enforced-by-rejection.md`,
`951476719`) declared no BREAKING banner, so
`check:adr-0087-registration` never asked it for a marker. It carries
none, and there is nothing to copy.
One conflict with the dispatch order's wording is worth stating. The
seat's dispatch order asks for "a FROM → TO line" (claim 5873443045
itself names only `.changeset/20386-*.md`; seat edit after review
5875022310). Measured with the gate's own exported
`findMigrationPrescription`: a line opening with the literal `FROM → TO`
label is read as a **migration prescription** (branch `from-to-label`),
and that refuses `no-migration-prescription`. The only category left
would then be `registered`, which would need a new ledger entry in
`packages/spec`. That is out of scope for this card and wrong on the
facts, since nothing authorable moves. So the changeset carries the
mapping as **"What a caller sees, before → after"** (`201`, stored as
sent → `400 VALIDATION_FAILED` + `max_value` / `min_value`, nothing
stored), plus the one-line fix. The detector reads that as no
prescription, and `check:adr-0087-registration` passes.
## Acceptance notes
- **`scale` / `precision` declared on a `progress` field parse, and
nothing reads them at the write seam.**
- Their `.describe()` texts name the types they bind on, and
`precision`'s says "Not read on any other field type".
- The metadata designer offers neither on `progress`:
`ObjectFieldInspector` `isNumeric` covers only `number`, `currency` and
`percent`.
- No example declares them. Noted, not filed.
- **objectui `SliderField`'s undeclared-bound defaults** (`min ?? 0`,
`max ?? 100`) are narrower than the server, which enforces nothing
undeclared.
- There is one degenerate shape, not measured in a browser: a `progress`
field declaring `min` above 100 and no `max`. The slider then clamps
into `[min, 100]`, so it would emit `100`, which is now refused.
- No field declares that shape. Noted, not filed. Holder: none.
- **`docs/qa/platform-checklist/areas/records-forms.json`**, item
`records-forms.field-type-constraints`: its steps do not reach
`progress` bounds (triage said so). This belongs to the next
checklist-author sweep. Holder: none.
- **PR #20469** (#20264) edits the header's date line and the date /
datetime arm of the same file. The hunks are far apart and there is no
textual overlap. The later lander merges `main`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent e967cbd commit 9801da1
6 files changed
Lines changed: 455 additions & 16 deletions
File tree
- .changeset
- packages
- objectql/src/validation
- rest/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
Lines changed: 9 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
155 | | - | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
160 | | - | |
161 | | - | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
162 | 164 | | |
163 | 165 | | |
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
190 | | - | |
| 190 | + | |
191 | 191 | | |
192 | 192 | | |
193 | 193 | | |
| |||
Lines changed: 252 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
0 commit comments