Skip to content

Commit ec6a275

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20419-spec-dts-heap-ceiling
2 parents 29d48fd + dc0ab6a commit ec6a275

125 files changed

Lines changed: 7288 additions & 884 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/platform-objects": patch
4+
---
5+
6+
Clause-②: no
7+
8+
Two live structured object keys are authorable in the metadata form: `fieldGroups` and `indexes`. Each was **declared** by `ObjectSchema`, graded `live` by the liveness ledger, and offered by **no** form in `METADATA_FORM_REGISTRY`, so an author's only door was the Source tab. Each is now a `type: 'repeater'` row on the object form whose sub-rows are declared by hand rather than derived from the schema:
9+
10+
- `fieldGroups` (Basics, beside `highlightFields`) — six sub-rows, one per canonical group key: `key` and `label` (required text), `icon` (text), `description` (textarea), `collapse` (a `none` / `expanded` / `collapsed` select) and `visibleWhen` (`type: 'code'`, `language: 'expression'`, the `fields` grid's predicate rows). The three `[DEPRECATED → collapse]` aliases (`defaultExpanded`, `collapsible`, `collapsed`) are **not** offered; the metadata-form reconciliation ledger records a nested `omit` row for each. The parse still accepts them and derives `collapse` from one only when `collapse` is absent, so a stored entry keeps its meaning, and a `collapse` set in the form outranks any alias it carries.
11+
- `indexes` (Advanced, beside `datasource`) — three sub-rows over the keys the SQL driver reads: `name` (text), `fields` (`widget: 'string-tags'`, required) and `unique`, a select offering **only** `global` and `organization`. The deprecated bare `unique: true` is never offered: a schema-derived control would take the union's first arm and render a switch that writes it. An edit merges into the stored entry, so an index that already carries `true` or `false` keeps it until the author picks a scope, and the select can write only the two values the parse accepts. `type` and `partial` are tombstones and have no row.
12+
13+
The help text states what the runtime does with each value. `indexes[].fields` is free text, and no authoring door judges its names: not the schema parse, not the publish door, not `os validate`. A name that is not a stored column makes the SQL driver skip the whole index at sync with a warning in the server log, and the help text says exactly that. A field group has no field-name list: a field joins a group through its own `group` key.
14+
15+
The two row schemas also carry a JSON Schema `title` on every property, as every repeater row schema must: `IndexSchema` on `name`, `fields` and `unique`, and `ObjectFieldGroupSchema` on its nine keys, the three deprecated aliases included. A property panel that reads the served schema's titles therefore shows a named column instead of a raw key. Each title is a `.meta({ title })` call and nothing more.
16+
17+
⛔ **No schema accept set moves and no export changes.** `METADATA_FORM_REGISTRY` is declared as an opaque `Readonly<Record<string, FormView>>`, so row contents were never part of the declared surface. What changes is the **form payload** `getMetaTypes()` serves (its rows, and the titles above in its JSON Schema) and the translation keys `os i18n extract` walks, hence the regenerated `platform-objects` metadata-form bundles. Their 22 new leaves are authored in `zh-CN`, `ja-JP` and `es-ES` rather than left as extractor fills.
18+
19+
⛔ **The gate that would notice a missing row is NOT landed here.** The reconciliation gate's top-level `zodOnly` direction stays unwired; this change lands offers and three nested ledger rows only.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec): `ApiError.code` and a flattened list overlay's legacy `options` bag carry the shapes their doors accept (#19920)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING for TypeScript code that annotates with `ApiError`, with any response type built on `BaseResponseSchema` (`BaseResponse`, `BatchUpdateResponse`, `SessionResponse`, the metadata, package, storage, analytics and automation response types, and the rest), with `ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` or `AssembledViewArtifactParsed`, or with the input type of a schema returned by `makeApiErrorSchema`**: a narrowing of published TYPES, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). The runtime accept set does not move at all: no schema's parse, no value and no export changes, and no export is added.
10+
11+
Two places in the published types were wider than the doors that judge the same bodies, so values those doors refuse type-checked:
12+
13+
- `ApiError.code` (the INPUT type of `ApiErrorSchema`): FROM `unknown` TO `ErrorCode`, the vocabulary the schema parses against (`StandardErrorCode` and the registered ledger codes). `ErrorCode` was cast to `z.ZodType` with its output type only, and `z.ZodType`'s input type defaults to `unknown`, so `{ code: 42, message: 'x' }` compiled as an `ApiError` while the schema refuses it at `code`. The same `code` narrows in the `error` of every response envelope built on `BaseResponseSchema`, and in each `ApiError` row of a batch result. `makeApiErrorSchema(codes)` had the same cast for a caller-supplied vocabulary: its schema's input `code` is now the standard catalogue plus `codes`, where it was `unknown`. The parsed types (`ApiErrorParsed`, the `…Parsed` response types) do not move: their `code` was already typed.
14+
- A flattened list overlay's legacy `options` bag: FROM a string-keyed record of `unknown` TO one optional entry per list kind that has a block (`calendar`, `chart`, `gallery`, `gantt`, `kanban`, `map`, `timeline`, `tree`), each entry that kind's own block with every key optional. This holds on the list overlay member of `ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` and `AssembledViewArtifactParsed`. `options: { foo: 1, kanban: 42 }` type-checked as all four while that member refuses both keys.
15+
16+
**If your code stops compiling.** A value you annotated with one of these names is not the shape the door accepts: correct it, or type a value that is still unvalidated as `unknown` and let the schema's `safeParse` decide. An error `code` is a member of `ErrorCode` (or, for a `makeApiErrorSchema` schema, of the standard catalogue plus the codes you supplied); a producer whose own code is outside the vocabulary reports it on `declaredCode`, not `code`. An `options` bag carries only the per-kind blocks listed above, each judged key by key like the top-level block of the same kind; `grid` has no block, and its settings are top-level keys of the view.
17+
18+
The declared types of `ErrorCode` and of `makeApiErrorSchema`'s `code` narrow with them, so `z.input` of each is typed where it was `unknown`. The types are the schemas' declared shapes, not their verdicts: refinements are not types, so each schema remains the only judge.
19+
20+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author writes moves — no spec key, no export and no stored row changes, and every runtime accept set is unchanged, so `objectstack migrate meta` has nothing to reach — and only TypeScript annotations narrow, whose channel is the consumer's compiler. -->
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/objectql": minor
3+
"@objectstack/spec": minor
4+
---
5+
6+
feat(objectql,spec)!: a numeric field's declared `precision` ("Total digits") is enforced on writes — a value that needs more digits is refused with field code `max_precision` (#19992)
7+
8+
Clause-②: yes
9+
10+
**BREAKING** — a narrowing of the write accept set on `@objectstack/objectql`, shipped as `minor` under the repo's launch-window convention (`check-changeset-no-major` refuses `major` until GA); the breaking-ness is carried by this banner and the ADR-0087 disposition, never by the level. Nothing an author writes changes spelling: `precision` keeps its key, its type and its legality.
11+
12+
`FieldSchema.precision` was declared ("Total digits") and read by nothing. Every numeric column is the fixed exact decimal of `NUMERIC_COLUMN_REPRESENTATION`, the record validator had no branch for it, and the renderer reads the liveness ledger cited are gone, so `precision: 5` on a `number` stored `123456789` verbatim. The metadata designer writes the key (labelled Precision, beside Scale), so it was a setting an author could make and see nothing come of. It is now enforced at the one place a write is judged.
13+
14+
**`@objectstack/objectql`** — the record validator refuses, after `min` / `max` and `max_scale`, a `number`, `currency`, `percent`, `rating` or `slider` value whose digit count exceeds a declared `precision`. It refuses with `400 VALIDATION_FAILED` and the field code `max_precision`, and it never rounds. The count is the SQL `DECIMAL(p, s)` one, taken on the stored value:
15+
16+
- **With a `scale`**, digits are counted at the field's decimal places, so the integer part may carry `precision − scale` digits. `precision: 5, scale: 2` holds up to `999.99` and refuses `1234.5`, which is `1234.50`, six digits.
17+
- **With no `scale`**, the value's own digits count. Leading zeros never count, and trailing zeros of the integer part always do: under `precision: 4`, `0.001` fits and `10000` does not.
18+
- **On `currency`**, where `scale` is refused, an amount counts at its own decimals. The decimals themselves stay unconstrained, and only the total is bounded: `precision: 18` refuses a 19-digit amount.
19+
- **On a fraction-stored `percent`** the count is taken two places further right (`scale + 2`, or 2 with no `scale`). The count is then the percentage-point value's digits as displayed: `precision: 4, scale: 2` holds 99.99% and refuses 100%.
20+
21+
What an author with an oversize value sees: the write is refused, nothing is stored, and the field error names the declaration and the count. For example, `constraint: { precision: 5, scale: 2, actual: 6 }` renders as "Hourly rate must have at most 5 digits in total, counting 2 decimal places (got 6)" in four locales. The REST create, batch, update and import routes all answer it, and `validate` (the dry run) predicts it. Only NEW writes are judged: a stored value longer than a `precision` declared later is never re-read. Nothing changes in storage or DDL.
22+
23+
The fix is one of three. Write a value that fits. Raise `precision` to the digits the field really holds. Or delete the key if the number was meant as decimal places: those are `scale`, and a currency's decimal places are its ISO 4217 minor unit.
24+
25+
**`@objectstack/spec`** — `FieldErrorCode` (the ADR-0114 field-level catalog) gains `max_precision` beside `max_scale`. `BUILTIN_VALIDATION_MESSAGES` gains its two sentences, `max_precision` and `max_precision_scaled`, in `en` / `zh-CN` / `ja-JP` / `es-ES`. `FieldSchema.precision`'s describe now states the counting rule and where it is enforced. The `precision` row of the field liveness ledger is re-evidenced at the write seam.
26+
27+
**Who is affected, measured** on `origin/main` `df3ba164`: no example app, template, platform object, seed or JSON fixture in the tree declares a field-level `precision`. Two test fixtures do (`precision: 5, scale: 0` on a 1–12 hours field), and every value they write fits.
28+
29+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable moves: `precision` keeps its key, its type (`z.number().int().min(0)`) and its legality on every field type, and no stored metadata representation changes, so `objectstack migrate meta` has nothing to rewrite and the ledger has no row to gain. What narrows is the record validator's write accept set for values under an already-declared count, which is runtime behaviour, not an authored shape. The spec edits add a member to a closed enum, two message templates and a describe; none removes or renames anything an author can write. The other categories are closed on facts: both packages publish (not `unpublished`); no ADR-0087 id is minted here and none covers this (not `registered` / `already-registered`); and runtime behaviour changes, not only a TypeScript declaration (not `runtime-interface-only` / `type-surface-only`). -->
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/driver-turso': minor
3+
'@objectstack/spec': patch
4+
---
5+
6+
fix(driver-turso)!: `new TursoDriver` refuses `syncUrl` under a forced `mode: 'remote'`, and `sync` with no `syncUrl`, instead of building and ignoring them
7+
8+
Clause-②: no (narrowing) — nothing is widened. No key is added, removed or renamed, and no exported symbol moves. Two driver configurations that the turso driver used to build and then ignore a key of are now refused when it is built.
9+
10+
`new TursoDriver()` accepted `syncUrl` beside a forced `mode: 'remote'`. Remote mode sends every read and write straight to `url`, and the remote client is created without `syncUrl`, so no replica is built and no sync ever runs. Measured on the built driver before this change, a `libsql://` or `file:` url under `mode: 'remote'` with `syncUrl` and `sync` constructed and connected, and `isSyncEnabled()` answered `true`. No sync interval started, and the sync call rejected with `SYNC_NOT_SUPPORTED` (`SyncNotSupported("File")` on the `file:` url). It also accepted `sync` with no `syncUrl`, in any mode, where nothing reads it. `@objectstack/spec`'s `TursoConfigSchema` already refused both at authoring. A datasource row stored before that, or a config a host builds itself, reached the constructor unparsed and ran with a sync setting that did nothing.
11+
12+
**BREAKING** accept-set narrowing on a published constructor, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). Refused now with `VALIDATION_ERROR` / 400, before any client or database is opened:
13+
14+
- `syncUrl` under a forced `mode: 'remote'`. A remote url beside `syncUrl` with no `mode` was already refused, as a replica on a remote url;
15+
- `sync` with no `syncUrl` (or with an empty one), in local, replica and remote mode alike.
16+
17+
Each refusal's message is the spec contract's issue message for that key, byte for byte, so authoring and boot say the same thing. A test holds the copies equal. The spec's `syncUrl` message said the driver "runs no sync, so the setting changes nothing". It now reads "the turso driver refuses this configuration when it starts", like its sibling refusals, and the driver's own `TursoConfigSchema` mirror follows (`@objectstack/spec` patch: message text only). The ADR-0087 entry `turso-config-transport-mismatch-refused` now also records that the constructor refuses these two shapes at boot.
18+
19+
Left accepted on purpose: `mode: 'replica'` on a `file:` url with no `syncUrl` (and no `sync`). It still runs as a plain local database. Refusing it in the constructor alone would refuse a config both schemas accept, so it is tracked separately.
20+
21+
### Migration: FROM → TO
22+
23+
| You wrote | Write instead |
24+
| --- | --- |
25+
| `url: 'libsql://my-db.turso.io', mode: 'remote', syncUrl: …` (with or without `sync`) | a remote database: drop `syncUrl` and `sync`. An embedded replica: `url: 'file:./data/replica.db', syncUrl: 'libsql://my-db.turso.io'` and no `mode` |
26+
| `url: 'file:./data/app.db', mode: 'remote', syncUrl: …` | the same two ways out |
27+
| `sync: { … }` with no `syncUrl` | name the remote in `syncUrl` (with a `file:` url), or drop `sync` |
28+
29+
A datasource row stored with one of these shapes is not re-parsed when it loads, so it now fails when the driver is built. `factory.create` throws the refusal. The connection service records the datasource as `failed-degraded` with the message, and a test connection answers `ok: false` ("Failed to build driver: …"). Under ADR-0062 D5, the boot fails fast when objects bind to that datasource or are routed to it, or when it is boot-critical, unless `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set. Otherwise it is left unconnected with a warning. Before this change the same row booted, reported sync as enabled and never synced. The way out is the table above: drop `syncUrl` / `sync` from a remote config, or use a `file:` url with the remote in `syncUrl`.
30+
31+
Blast radius, measured on this tree: no example, template, published skill or hand-written doc authors either shape, and no in-repo caller reads `isSyncEnabled()` or calls the driver's sync outside `@objectstack/driver-turso`'s own tests. Whether any out-of-repo deployment declares such a config is NOT measured and is not claimed to be zero.
32+
33+
<!-- adr-0087: not-required (already-registered turso-config-transport-mismatch-refused) that entry is this family's semantic TODO (the turso transport refusals, registered with the authoring half), and this diff updates its stored-row sentence to cover the constructor half: syncUrl under a forced remote mode and sync with no syncUrl are refused at boot as well -->

‎.changeset/20331-validate-view-container-name.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ derived object key is empty, because the boot registrar skips that entry with a
2929
warning and never refuses it. The boot registrar now calls this function. What it
3030
refuses, its message and its `VALIDATION_ERROR` / `400` envelope are unchanged.
3131

32-
Not changed: `os build` does not run this check, so it still writes an artifact
33-
carrying such a container, and the server refuses that artifact when it loads it.
32+
`os build` runs the same check as well (#20393, its own entry), so it no longer
33+
writes an artifact carrying such a container.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
`@objectstack/spec/data` declares the platform's numeric grammar for a string, and the contract of the number-comparand declared-type door: which string comparands a field whose declared type is numeric may be compared against (#20336).
6+
7+
Clause-②: yes
8+
9+
**The grammar.** A string is numeric when its whole content is a JSON number literal (`-?(0|[1-9][0-9]*)(.[0-9]+)?([eE][+-]?[0-9]+)?`) that names a finite number; it then means what the same characters mean as a JSON number. `NUMERIC_STRING_PATTERN`, `parseNumericString(s)` (the number, or `undefined`) and `readNumericString(s)` (the number, or which of eight named forms the string is: `empty`, `padded`, `placeholder`, `radix-prefix`, `non-finite`, `digit-separator`, `non-json-spelling`, `not-a-number`). `NUMERIC_STRING_GRAMMAR_CASES` records every form with the reason it is admitted or refused: `"12"`, `"-3"`, `"12.5"`, `"1e3"` and every string `String(n)` produces for a finite number are admitted; `""`, `" 12 "`, `"0x10"`, `"Infinity"`, `"NaN"`, `"1,000"`, `"+5"`, `".5"`, `"007"` and any `{placeholder}` are not.
10+
11+
**The door's contract.** `numberComparandDoorVerdict(field, comparand)` answers, for a comparand at a value position (implicit equality, `$eq` / `$ne` / `$gt` / `$gte` / `$lt` / `$lte`, and each member of `$in` / `$nin` / `$between`) of a filter on a field whose declared type is in `NUMERIC_VALUE_TYPES` (or a `formula` whose `returnType` is `number`): `door-refusal` (`INVALID_FILTER` / 400) for a non-numeric string, `narrows` with the number for a numeric one, `passes` for any other comparand or field, `deferred` for a `formula` without a readable `returnType`. `numberComparandRefusalMessage(site, context?)` is the refusal the door prints: the field, its declared type, the comparand, its position and what is wrong with it. A fixture object and a derived case table (`NUMBER_COMPARAND_DOOR_FIXTURE`, `NUMBER_COMPARAND_DOOR_CASES`) are published for the engine suite that pins the door.
12+
13+
**What moves for consumers.** Nothing yet. This is the contract only, and no door reads it in this release: a non-numeric string compared with a number field still reaches the driver as written (a 500 on PostgreSQL, an empty or different result elsewhere). The engine door that refuses it with `INVALID_FILTER` / 400 and narrows a numeric string lands with #20351, and the record validator's number arm adopts the same grammar for writes with #20309.

0 commit comments

Comments
 (0)