Repository navigation
Commit db74b16
Part of #20309
Clause-②: no (narrowing)
A number, currency, percent, rating, slider or progress field now
refuses an array, a boolean or an object with `400 VALIDATION_FAILED` /
`invalid_number`, on every engine and REST write door. At base `[500]`
answered 201 and SQLite stored the TEXT `'[500]'`. A number is judged
and stored exactly as before. **A string is unchanged**: it is still
judged by `Number()` and written as sent. That is the half this PR
leaves open, which is why the first line says `Part of`: see "The string
half" below.
## What changes (read from the code at the head below)
- **`packages/objectql/src/validation/record-validator.ts`, the number
arm** (`NUMERIC_VALUE_TYPES` minus `COMPUTED_VALUE_TYPES`, as #20308
left it). One guard before the old finite check: a value whose `typeof`
is neither `number` nor `string` is `invalid_number`. The rest of the
arm is byte-identical: the finite check, `progress`'s early return,
`min`, `max`, `scale` and every message.
- No change in `engine.ts`, no driver change, no REST change. Every
REST, batch and import door reaches this validator through `insert()` /
`update()`.
- `summary` is still not judged (ruling 5860986842 on #20308). A blank
is still `null` before the arm (#20308's `normalizeBlankTypedValues`).
- `.changeset/20309-number-arm-non-string-refused.md`:
`@objectstack/objectql` `minor`, a BREAKING banner, `Clause-②: no
(narrowing)`, ADR-0087 `not-required (no-migration-prescription)`.
## Measured, base to head (H1, H2)
Instrument: a scratch script, not committed, booting the real
`ObjectQL`, `ObjectStackProtocolImplementation` and `RestServer` from
the built packages, once on `InMemoryDriver` and once on `SqlDriver`
over better-sqlite3 in memory. Types: number, currency, percent, rating,
slider, progress. Doors: engine `insert`, engine `update`, REST `POST
/data/:object`, REST `PATCH /data/:object/:id`, REST batch create, REST
batch update. Each cell records the door's answer, the physical cell
(the memory driver's own store; on SQLite the column and its
`typeof()`), `engine.findOne` and REST `GET`. Base is `c74de10a94`. Head
is this branch at `bf83ded05c` (the code at the head below is the same).
1016 cells: 14 inputs x 6 types x 6 doors x 2 drivers, plus the 4 H6
cells.
| input | base, memory | base, SQLite | head, both drivers |
|---|---|---|---|
| `[500]` | accepted, stores the array, reads `[500]` | 201, stores TEXT
`'[500]'`, reads `"[500]"` | `invalid_number` (REST 400; batch row
`VALIDATION_FAILED`), nothing written |
| `[]` | accepted, stores `[]` | 201, stores TEXT `'[]'` |
`invalid_number`, nothing written |
| `true` / `false` | accepted, stores the boolean | 201, stores `1` /
`0` (`real`; `integer` on rating) | `invalid_number`, nothing written |
| `[5, 7]`, `{}`, `'Infinity'` | `invalid_number` | `invalid_number` |
unchanged |
| `500`, `12.5` | stored as the number | stored `real` (`integer` for
500 on rating) | unchanged |
| `'0x10'` | stored the string | stored TEXT `'0x10'`, read back as `16`
| unchanged (the string half) |
| `' 12 '`, `'12'`, `'1e3'` | stored the string | stored as a number
(column affinity) | unchanged (the string half) |
| `'12.5'` | stored the string | stored `real` 12.5 | unchanged (the
string half) |
Of 1016 cells, exactly 288 moved: `[500]`, `[]`, `true` and `false`, 72
cells each (6 types x 6 doors x 2 drivers). The other 728 are
byte-identical base to head in all five columns.
**H2.** At base the arm judged `Number(value)` and the driver received
`value`: the table's `[500]` row is that gap, read from the raw column.
At head a non-string never reaches the driver. A number arrives as the
same number: `engine-number-value-door.test.ts` asserts `Object.is` on
the driver-facing payload for insert and update. A string still has the
gap, and that is the open half.
## The string half, and why this PR does not close the card
The seat's update after dispatch set two branches. (a) If no shipped
producer sends a numeric string to a number-typed field, refuse every
string. (b) If one does, refuse only the non-strings, leave strings
exactly as base, open the PR as `Part of`, and sequence the string half
after #20336's grammar.
The in-repo census (below) finds no producer that sends a numeric
string. **objectui's form widgets, the main row, are NOT MEASURED**:
this session has no read access to `objectstack-ai/objectui` (REST `GET`
answered 403, "GitHub access to this repository is not enabled for this
session"), and the request to attach it was refused by the session's
permission classifier. So branch (a) cannot be established. This PR
takes branch (b): it is the one that refuses no form a shipped producer
might send. The string half is an open question in the report, not a
guess here. ⛔ No numeric-string grammar is authored in
`packages/objectql`.
## Producer census (H3)
Every row is a shipped producer of values for a number-typed field, and
what it sends.
| producer | file | what it sends | measured how |
|---|---|---|---|
| Example seed records | `examples/app-crm/src/data/index.ts`,
`examples/app-showcase/src/data/**` | JS numbers only: 35 values (crm)
and 124 (showcase) on numeric fields; app-todo and app-multi-package
seed none | each example's `objectstack.config.ts` imported with tsx and
every seed record walked against its object's field types |
| Numeric `defaultValue`s | the example objects | JS numbers only: 4
(crm), 11 (showcase), 2 (todo) | same walk |
| Example flow `create_record` / `update_record` nodes |
`examples/app-todo/src/flows/task.flow.ts`, `create_next_task` | one
value on a numeric field: `recurrence_interval:
'{completedTask.recurrence_interval}'`, a single-token template.
`interpolateString`
(`packages/services/service-automation/src/builtin/template.ts`) returns
the resolved raw value for a single token, so this sends the stored
number | same walk; the template rule read at source |
| Flow templates in general | `template.ts` | a single token keeps its
type; an EMBEDDED template (text around a token) is stringified. No
shipped flow puts one on a numeric field | read at source |
| CSV / JSON import | `packages/rest/src/import-coerce.ts`
(`parseNumberCell`), called by `import-runner.ts` before the engine | a
JS number, or the row's own `invalid_number` refusal | read at source |
| REST batch, `createMany`, `updateMany`, import doors | `packages/rest`
| pass the caller's JSON through to the engine; a door, not a producer |
measured above |
| `@objectstack/client` | `packages/client/src/index.ts` | serialises
the caller's record as JSON; no value stringification | read at source |
| Read-modify-write through driver-sql |
`packages/drivers/driver-sql/src/sql-driver.ts` (`numericValueFields`) |
numeric columns are presented as JS numbers on every dialect, so a
record read back and written again carries numbers | read at source |
| objectui form widgets | `objectstack-ai/objectui` | **NOT MEASURED**
(no read access in this session). Indirect only: PR #20340 measured that
a cleared number box sends `null`, not `''` | none |
Runtime sweep at head: `@objectstack/service-automation` (147 files,
1767 tests), `@objectstack/rest` and `@objectstack/objectql` all pass
with the refusal in place.
## No collateral (H4)
- A blank (`''`, `' '`) is still `null` before the arm: #20308's pins
pass unchanged (`record-validator.blank-typed-value.test.ts`,
`engine-blank-typed-value-door.test.ts`,
`rest-data-blank-typed-value.test.ts`), and the new files re-assert it.
- `summary` is not judged: the new validator pin writes `[500]`, `true`
and a Date to `summary` and all are accepted, as at base.
- Existing refusals keep their words and code: `[5, 7]`, `{}`, `NaN`,
`Infinity`, `'Infinity'`, `'abc'`, `min_value` / `max_value` on all six
types and `max_scale`. That is 192 validator answers (code, message,
fields) compared between the base file and the head file, en and zh-CN,
insert and update: 192 identical.
- A valid JS number is stored byte-identical: the 1016-cell table, and
the `Object.is` pin.
## Declaration (H5)
`Clause-②: no (narrowing)`, the claim's line. `[500]`, `[]`, `true` and
`false` answered 201 at base on memory and SQLite and are refused at
head. The census names no shipped producer that sends an array, a
boolean or an object to a number-typed field, so no producer's form is
refused. The objectui row is NOT MEASURED, as above.
- `check-changeset-no-major --base origin/main --event` (this body as
the `pull_request` payload): exit 0. The gate printed `LEVEL AXIS: this
PR declares clause-② no (narrowing)` and, for the direction arm,
`narrowing — a BREAKING change; during the launch window it ships
minor`.
- `check-adr-0087-registration --base origin/main`: exit 0, "1
declared-breaking changeset(s)", signals
`BREAKING+bang+clause-②-narrowing`, disposition `not-required
(no-migration-prescription)`.
- `check-empty-changeset --base origin/main`: exit 0. #20308's pending
changeset (`.changeset/20308-blank-typed-value-null.md`) reads true at
this head, so there is no DELIBERATE CORRECTION.
## H6, for the seat (nothing changed for it)
Base `c74de10a94`, REST `POST /data/:object`, one field each, bounds
declared on the field. The four cells are the same on SQLite and memory,
and the same at head.
| field | value | answer | stored |
|---|---|---|---|
| `progress`, `max: 100` | 150 | 201 | 150 (SQLite `real`) |
| `progress`, `min: 0` | -5 | 201 | -5 (SQLite `real`) |
| `number`, `max: 100` (control) | 150 | 400 `VALIDATION_FAILED` /
`max_value` | no row |
| `number`, `min: 0` (control) | -5 | 400 `VALIDATION_FAILED` /
`min_value` | no row |
It reproduces: `progress` stores a value outside the `min` / `max` it
declares.
## Tests
New files:
-
`packages/objectql/src/validation/record-validator.number-value.test.ts`:
the refusal set on the six judged types, insert and update, as the
envelope (`VALIDATION_FAILED` plus `invalid_number`); parity with the
spec's `valueSchemaFor` over every non-string input; a characterization
that the string half is unchanged, which turns red when it lands; and
the blank and `summary` controls.
- `packages/objectql/src/engine-number-value-door.test.ts`: what the
driver receives on insert, `insert([...])`, `insertMany`, update by id
and update by predicate, plus the dry run; a refused value never reaches
the driver.
- `packages/rest/src/rest-data-number-value.test.ts`, on SQLite: `POST`,
batch create, `PATCH`, batch update and `updateMany`, with the physical
cell and its `typeof()`.
Suites run at `c135a38d31` (this branch after merging `origin/main`
`26daf0b036`):
- `@objectstack/objectql`, whole suite: 325 files, 6013 tests passed.
- `@objectstack/rest`, whole suite: 211 files, 3856 passed and 2
skipped.
- `@objectstack/driver-memory`: 57 files, 1374 passed.
- `@objectstack/driver-sql` (no live PostgreSQL or MySQL; those files
skip): 192 files passed and 11 skipped; 3157 tests passed and 176
skipped.
- `@objectstack/service-automation`: 147 files, 1767 passed.
- `pnpm --filter @objectstack/objectql --filter @objectstack/rest run
typecheck`: exit 0, both test layers included.
## Reverse verification
The guard was removed with `scripts/ablation-replace.mjs` (anchor hit
once, blob changed), `@objectstack/objectql` was rebuilt, and
`ablation-dist-preflight` found the marker in 4 built files. Predicted
before the run: 68 objectql reds (42 per-input validator cases, the
spec-parity case, 24 per-input door cases and the dry run) and 24 REST
reds. Measured: objectql 68 failed of 151, REST 24 failed of 43, and
every red was one of the predicted cases. Measured at `bf83ded05c`; the
merge of `origin/main` that followed changed no file in
`packages/objectql` or `packages/drivers`. The restore proved blob
equals HEAD and `git diff HEAD` is empty; after a rebuild the `--absent`
preflight passed with a clean tree, and the pins were green again
(151/151 and 43/43).
## Gates
Derived at `c135a38d31` with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack`: 64 commands, the same 64
as the dispatch list. Each was run at that head with its exit code
recorded before any pipe.
- `--ran` reconciliation: `64 derived famil(ies) accounted for — 62 run,
2 NOT-MEASURED`.
- 62 exit 0, including `check:driver-memory-census`,
`check:engine-double-contract`, `check:objectql-double-limit`,
`check:test-source-alias`, `check:cross-package-test-inputs`,
`check:nul-bytes`, `check:issue-citations` and
`check:type-check-coverage`.
- **NOT MEASURED: `check:dual-build-cjs-loads` and
`check:type-check-debt`** (exit 3, `PREREQUISITE NOT MET`). Both need
every package built, and CI runs both.
- `node scripts/check-issue-citations.mjs --base 26daf0b` (the merge
base): exit 0, 4 citations resolve.
## Acceptance notes
- **Memory is measured, not pinned at the REST door.** The triage asked
for REST pins on driver-sql and driver-memory. A new
`@objectstack/driver-memory` test consumer is refused by
`check:driver-memory-census` without a ruling, which is the same
constraint PR #20340 met. The engine pin reads the driver-facing
payload, which memory stores verbatim, and the memory REST cells are in
the table above.
- **The import route's header** (`packages/rest/src/import-coerce.ts`)
says the validator "coerces only to *check* a value and then discards
the coerced form". That is still true for strings, and for the boolean
and date arms. It is outside this claim's file surface, so it is not
edited.
- **Stored rows.** A value written before this change is never re-read
by the check. The changeset carries a read-only SQLite query that finds
TEXT cells in a numeric column; nothing is rewritten.
- #20336 and #20351 are not addressed here. The census found a shipped
producer of numeric strings: objectui's `plugin-grid` CSV import legacy
fallback writes the raw cell (seat answer 5863923799 on #20309). So the
string half accepts #20336's grammar and stores the parsed number, and
it waits for that grammar.
- **Not filed:** the boolean arm has the same judged-vs-written shape
(it accepts `0`, `1`, `'0'`, `'1'`, `'true'`, `'false'` and writes the
value as sent). It was read at source only, not measured at a door, so
it is not a card (filing gate ①).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 70ce802 commit db74b16
5 files changed
Lines changed: 538 additions & 0 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 | + | |
| 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 | + | |
| 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 | + | |
| 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 | + | |
0 commit comments