Skip to content

Commit f0a0378

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20141-masked-field-types-declared
2 parents 3d14f9e + c02fa12 commit f0a0378

87 files changed

Lines changed: 4353 additions & 499 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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING** — `CONCURRENT_LIMIT_EXCEEDED` is removed from the closed `StandardErrorCode` catalogue (#17707).
6+
7+
A `major`-class change, recorded as `minor` under the launch-window convention. ADR-0049 enforce-or-remove applied to the ADR-0112 error catalogue: ruling A on #17707, narrowed on 2026-09-24 to this code alone.
8+
9+
**Why.** A catalogue member is the list callers branch on exhaustively, and a member with no producer teaches a branch that cannot fire. `CONCURRENT_LIMIT_EXCEEDED` had no producer behind it when the ruling was recorded, so it leaves the catalogue. Its neighbour `QUOTA_EXCEEDED` stays, unchanged, as the narrowing ruled.
10+
11+
### FROM → TO
12+
13+
| removed | what to write instead |
14+
| --- | --- |
15+
| `CONCURRENT_LIMIT_EXCEEDED` (`StandardErrorCode`, 429) | nothing — delete the branch. For request pacing branch on `RATE_LIMIT_EXCEEDED` (HTTP 429; wait `retryAfterSeconds` before retrying). A service that enforces its own concurrency limit registers a code for it in its own error-code ledger. |
16+
17+
**The one-line fix: delete every branch on `CONCURRENT_LIMIT_EXCEEDED`.** A comparison against a value typed `StandardErrorCode` or `ErrorCode` no longer compiles (`TS2367`). At runtime the spelling now fails `StandardErrorCode`, `ErrorCode` / `ApiErrorSchema.code` and `makeApiErrorSchema(...)` parse, and the failure message is the removal prescription itself.
18+
19+
**What stays.** The other `StandardErrorCode` members, `QUOTA_EXCEEDED` included, are unchanged.
20+
21+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** Inside this repository the code occurred only in the enum declaration, the hand-written error catalogue page, the generated reference pages and the unpinned-status baseline, and the pinned objectui checkout does not name it; `@objectstack/spec` is published, so readers elsewhere were not measured.
22+
23+
The ADR-0087 D3 semantic entry `standard-error-code-concurrent-limit-exceeded-retired` carries the judgement: an error code is wire vocabulary, not a metadata key, so there is no authored source for a D2 conversion to rewrite.
24+
25+
Clause-②: no
26+
27+
<!-- adr-0087: registered standard-error-code-concurrent-limit-exceeded-retired -->
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/core': minor
4+
'@objectstack/runtime': minor
5+
---
6+
7+
feat(spec)!: the canon for "the version of a package or plugin" is SemVer 2.0.0 — nine carriers, one grammar
8+
9+
Clause-②: yes (narrowing)
10+
11+
<!-- adr-0087: registered manifest-version-semver-2-0-0, plugin-version-semver-2-0-0, package-version-row-semver-2-0-0, package-manifest-version-grammar-enforced -->
12+
13+
**BREAKING** — four published accept sets converge on one, and the fringe each
14+
of them carried outside SemVer 2.0.0 is refused. The widening half needs no
15+
action from anyone; the narrowing half is listed per carrier below, with its
16+
FROM → TO.
17+
18+
One concept was judged by four different grammars across ten carriers in two
19+
repositories, and the strictest refused `2.0.0-beta.1` — the exact string a
20+
sibling declaration documented as an example of itself. The disagreement was
21+
observable between doors on the same resource, not merely between schema files:
22+
`os plugin build` refused a prerelease the publish door accepted, the Studio
23+
form refused it twice over, the `PATCH` door answered `400`, and the install
24+
door parsed nothing at all. An earlier change collapsed the eight regex literals
25+
onto three exported constants, which removed the drift but not the disagreement.
26+
27+
`@objectstack/spec/kernel` now exports ONE grammar —
28+
`SEMVER_2_0_0_VERSION_PATTERN`, semver.org's own published expression — and
29+
every carrier references it.
30+
31+
## What every author gains, with no edit
32+
33+
Prerelease and build suffixes are accepted on the five carriers that demanded a
34+
bare three-segment core, so `2.0.0-beta.1`, `17.0.0-rc.5`, `1.0.0+20230101` and
35+
`1.0.0-rc.1+exp.sha.5114f85` now pass a key that refused all of them. Identifiers
36+
are case-preserving everywhere, as the standard requires. This repository cuts
37+
prereleases of its own packages while the key describing a package could not
38+
express one; that ends here.
39+
40+
```
41+
FROM ManifestSchema.parse({ id: 'com.acme.crm', version: '2.0.0-beta.1', … })
42+
-> throws // and `os plugin build` exits 1
43+
44+
TO ManifestSchema.parse({ id: 'com.acme.crm', version: '2.0.0-beta.1', … })
45+
-> parses
46+
```
47+
48+
## What stops being accepted, per carrier
49+
50+
Eight strings, all of them forms SemVer 2.0.0 forbids and none of them a valid
51+
prerelease. What they have in common is that no precedence order exists for any
52+
of them — `dependency-resolver.ts` can place none in an order — so a package
53+
versioned this way could be published and never compared against its own
54+
successor.
55+
56+
```
57+
FROM version: '01.1.1' TO version: '1.1.1' // §2 no leading zero in
58+
FROM version: '1.01.1' TO version: '1.1.1' // a numeric identifier
59+
FROM version: '1.1.01' TO version: '1.1.1'
60+
FROM version: '1.0.0-0123' TO version: '1.0.0-123' // §9 no leading zero in a
61+
// numeric prerelease id
62+
FROM version: '1.0.0-alpha..1' TO version: '1.0.0-alpha.1' // §9 no empty
63+
FROM version: '1.0.0-alpha..' TO version: '1.0.0-alpha' // identifier
64+
FROM version: '1.0.0-.' TO version: '1.0.0'
65+
FROM version: '1.0.0+.' TO version: '1.0.0' // §10 no empty build id
66+
```
67+
68+
⛔ Each repair above is one defensible reading and not the only one, which is
69+
why they ship as ADR-0087 D3 semantic TODOs rather than as mechanical D2
70+
conversions: a version is how a release is addressed, so rewriting one
71+
re-points whatever already resolved the old string. Run
72+
`objectstack migrate meta --from <N>` for the per-site list.
73+
74+
Per carrier:
75+
76+
- `ManifestSchema.version` and its three sibling declarations
77+
(`MetadataPluginManifestSchema`, `PluginRegistryEntrySchema`,
78+
`PluginMetadataSchema`), plus the `PATCH /api/v1/packages/:id` door: gain the
79+
whole prerelease and build space; lose a leading zero in the numeric core.
80+
- `PluginSchema.version` and the plugin boot path in `@objectstack/core`: lose
81+
those eight and **nothing else**. ⭐ Every valid prerelease and build form the
82+
loader accepts today it still accepts, which is what keeps the widen-never-
83+
narrow ruling on that path honoured rather than reversed; both halves of that
84+
bound are pinned in `plugin.test.ts` and `plugin-loader.test.ts`.
85+
- `PackageVersionSchema.version`: gains case-preserving identifiers
86+
(`1.0.0-Beta.1`, `1.0.0+Build.5`), which the boot path has always accepted and
87+
this key alone refused; loses the same eight.
88+
- `PackageManifestSchema.version`: was a bare `z.string()` constraining nothing,
89+
so it is the one carrier where the grammar is entirely new. `latest`,
90+
`v1.0.0`, `1.0`, the empty string and `2.0.0-beta.1extra!` were accepted and
91+
frozen into a published manifest snapshot; each is refused now. A dist-tag
92+
becomes the version it pointed at, a `v`-prefix drops, a two-segment string
93+
gains its patch.
94+
95+
## The prose moved with the grammar
96+
97+
Every `.describe()` names SemVer 2.0.0 and the nine generated reference-doc rows
98+
follow; the `PATCH` door's refusal says so; `manifest.test.ts`'s
99+
「should enforce semantic versioning」 case stops listing `1.0.0-beta` among the
100+
invalid versions. `PluginLoader.isSemverShapedVersion` becomes `isSemverVersion`
101+
— a predicate named for a standard it does not implement gets misused by the
102+
next caller whatever its docblock says, and the name is true now.
103+
104+
Three exported constants are retired, each replaced by the one canon:
105+
106+
```
107+
FROM import { MAJOR_MINOR_PATCH_VERSION_PATTERN } from '@objectstack/spec/kernel'
108+
FROM import { SEMVER_SHAPED_VERSION_PATTERN } from '@objectstack/spec/kernel'
109+
FROM import { SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN } from '@objectstack/spec/kernel'
110+
TO import { SEMVER_2_0_0_VERSION_PATTERN } from '@objectstack/spec/kernel'
111+
```
112+
113+
⛔ They are not interchangeable with what they replaced — each named an accept
114+
set that no longer exists, which is why they are retired rather than aliased. A
115+
consumer that referenced one to REPRODUCE a verdict gets the canon's verdict
116+
now; one that referenced it to match a foreign grammar owns that grammar itself.
117+
118+
The accept set is pinned witness by witness in `version-grammar.test.ts`: move a
119+
cell there and you have moved a published accept set on nine carriers at once,
120+
in one visible edit.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
'@objectstack/runtime': minor
3+
---
4+
5+
fix(runtime): `POST /api/v1/packages` parses the whole body through `PackageInstallBodySchema` instead of reading it key by key (#19328)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING for callers of the install door.** The door now enforces the
10+
WHOLE install-body declaration. A body that `PackageInstallBodySchema` refuses
11+
is refused with `400` / `VALIDATION_ERROR` and installs nothing, whatever the
12+
reason. Every shape below used to answer `201`, and two of the install options
13+
used to be honoured. The list names the shapes a caller is most likely to have
14+
sent, and ends with one bullet for everything else the declaration states. For
15+
each one, change what you send FROM the refused shape TO the declared one:
16+
17+
- **A manifest with no `type`, in either body form.** FROM
18+
`{ "manifest": { "id": …, "name": …, "version": … } }` (or the same manifest
19+
sent bare) TO the same manifest with a declared `type`: one of `app`,
20+
`plugin`, `ui`, `driver`, `server`, `theme`, `agent`, `objectql`, `module`,
21+
`gateway` or `adapter`, e.g. `"type": "app"`. It used to install anyway.
22+
- **A manifest with no `name`, in either body form.** `name` is the manifest's
23+
other required key besides `id`, `version` and `type`. FROM
24+
`{ "manifest": { "id": …, "version": …, "type": … } }` (or the same manifest
25+
sent bare) TO the same manifest with `"name": "…"`, the human-readable
26+
package name. It used to install anyway, because the door parsed only the
27+
`id` and `version` legs.
28+
- **An unknown key inside the manifest, or on a bare body.** Example: a
29+
transposed `namesapce`. FROM a manifest carrying the undeclared key TO the
30+
manifest with that key removed, or spelled as the declared key it was meant
31+
to be (`namespace`). The refusal names the key. The key used to be STORED
32+
with the package.
33+
- **A string-typed `enableOnInstall` or `overwrite`.** FROM
34+
`"enableOnInstall": "false"` / `"overwrite": "true"` TO JSON booleans,
35+
`"enableOnInstall": false` / `"overwrite": true`. `'false'` used to install a
36+
fresh package ENABLED, which is the opposite of what the caller asked for.
37+
`'true'` for `overwrite` was read as absent, which answered `409` on an
38+
installed id.
39+
- **Install options spelled on the BARE form (`enableOnInstall`, `overwrite`,
40+
`settings`).** FROM `{ "id": …, …, "overwrite": true }` TO the wrapped form,
41+
`{ "manifest": { "id": …, … }, "enableOnInstall": …, "overwrite": …,
42+
"settings": … }`. `overwrite` may also go on the query string instead
43+
(`?overwrite=true`), which a bare body may keep using. Before this change the
44+
door handled these key by key: `enableOnInstall` was ignored, but
45+
**`overwrite: true` and `settings` were HONOURED** (an installed id was
46+
overwritten, and the settings reached the install). All three were also
47+
stored as manifest keys. They are refused now. This is the part of the
48+
change that removes behaviour a caller could have been relying on.
49+
- **Any other constraint `ManifestSchema` declares.** Each one is now enforced
50+
at this door. Before, only the `id` and `version` legs were, and the manifest
51+
was stored as sent. The constraints include:
52+
- the `namespace` grammar: 2–20 characters, starting with a lowercase
53+
letter, with only lowercase letters, digits and underscores;
54+
- the closed value sets of `scope` (`cloud`, `system`, `project`),
55+
`runtime` and `packaging`;
56+
- the declared type of a key (for example a non-string `description`, or a
57+
non-string version in `dependencies`);
58+
- the retired manifest keys `configuration`, `capabilities`, `extensions` and
59+
`loading`, which the declaration keeps only as named refusals;
60+
- unknown keys inside the nested blocks the declaration closes. These are
61+
`contributes` (and its `kinds[]`), `data[]` (each entry is a `SeedSchema`
62+
seed), `navigationContributions[]` (and their items), `engine`, `engines`,
63+
and the structured form of `permissions`.
64+
65+
FROM the off-declaration value TO the value the declaration states. The
66+
refusal names the path.
67+
68+
The accept set only shrinks back to what the published declaration has always
69+
said. `PackageInstallBodySchema` in `@objectstack/spec` is a union of two
70+
branches: the wrapped request, or a bare manifest as the whole body.
71+
`ManifestSchema` declares the manifest's required keys (`id`, `name`,
72+
`version`, `type`) and the grammar or value set of the rest. It closes the
73+
manifest, and the nested blocks listed above, against unknown keys.
74+
The wrapped request types `enableOnInstall` and `overwrite` as booleans. Its
75+
docblock says a bare manifest carries no install options: «a caller that needs
76+
an option sends the wrapped form». Until now, the door parsed only two legs of
77+
the manifest (`id` and `version`) and read every other key positionally off
78+
the raw body. That is «declared ≠ enforced» on a published API contract. Now
79+
the door parses the body once through the declared union and reads
80+
`overwrite`, `settings` and `enableOnInstall` off the parsed request. Nothing
81+
in `@objectstack/spec` moves, and neither branch of the union is relaxed.
82+
83+
**What is not affected.**
84+
85+
- A well-formed body installs exactly as before, in both forms and on both
86+
install limbs (the protocol primitive and the bare-registry fallback).
87+
- The first-party SDK (`client.packages.install(m, { enableOnInstall,
88+
overwrite, settings })`) already sends the wrapped form with JSON booleans.
89+
Studio's create-package dialog sends `{ manifest }` with a declared `type`.
90+
- The manifest is stored as sent, with no parse-time defaults added.
91+
- The refusals for a missing or malformed `id` and `version` keep their own
92+
sentences and still come first.
93+
- Every request-shape refusal is still answered ahead of the duplicate-id
94+
`409`.
95+
- Boot-time and in-process installs reach `SchemaRegistry.installPackage` /
96+
`ObjectQL.registerApp` directly and never pass through this door.
97+
98+
**Still accepted, because the declaration accepts it.** An unknown key at the
99+
top level of the wrapped form (`{ manifest, bogus }`) is dropped, not refused.
100+
`PackageInstallRequestSchema` is declared in strip mode, and the door now does
101+
exactly what the declaration says.
102+
103+
**If you are refused.** The refusal names the path of each failed constraint,
104+
and the form it read the body as. A misplaced install option also gets the wrapped form (and, for
105+
`overwrite`, `?overwrite=true`) spelled out. So the prescription arrives with
106+
the `400` rather than in a changelog.
107+
108+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or reshaped: no spec key, no export, no stored row. `objectstack migrate meta` has nothing to reach, because there is no old spelling that maps to a new one — every refused shape is one the published declaration already refused, and a caller repairs it by sending the declared shape (a declared `type`, a `name`, the declared key or value, a JSON boolean, the wrapped form). The refusal itself carries the remedy. -->
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/formula": minor
4+
"@objectstack/driver-memory": minor
5+
---
6+
7+
`$like` / `$ilike`: `_` matches exactly one Unicode code point on every face, so an emoji or any other character outside the Basic Multilingual Plane is one `_`, as SQL `LIKE` and SQLite `GLOB` count it (#20143).
8+
9+
The SQLite faces (`driver-sql` on better-sqlite3, `driver-sqlite-wasm`, `driver-turso` local and remote) already answered by code points. The JavaScript faces did not: they compiled the spec's `likePatternToRegexSource` with no regular-expression flags, so `_` read one UTF-16 code unit, which is half of an emoji. The same REST filter returned a different row set depending on which driver backed the object. Measured at `e7f69dbb` over values holding `😀` (U+1F600) and `𝒜` (U+1D49C), 48 answer cells on the JS faces differed from the SQLite faces; after this change, none do.
10+
11+
- **`@objectstack/spec`**: a new export, `likePatternToRegExp(pattern, foldAscii?)`, compiles the translation with the `u` flag, the one compilation in which `_` is one code point. `matchesLikePattern` evaluates it. `likePatternToRegexSource` is unchanged and still exported; its source means one code point per `_` only under `u`. The `$like` description now says that a character is one Unicode code point.
12+
- **`@objectstack/formula`**: `matchesFilterCondition` answers `$like` / `$ilike` by code points, through the spec's `matchesLikePattern`. Its own CEL `size()` already counted code points.
13+
- **`@objectstack/driver-memory`**: all three `$like` doors (the `$like` filter and the AST `like` / `ilike` node through mingo, and the reference matcher) answer by code points.
14+
15+
The answer set moves in both directions on those three faces, only for values holding a character outside the BMP:
16+
17+
| pattern | a stored `😀` | `a😀b` | `a😀😀b` |
18+
|---|---|---|---|
19+
| `_` | now matches | — | — |
20+
| `__` | no longer matches | — | — |
21+
| `a_b` | — | now matches | — |
22+
| `a__b` | — | no longer matches | now matches |
23+
24+
`$ilike` moves the same way. No pattern is newly refused and no refusal is lifted. Values made only of characters inside the BMP answer exactly as before.
25+
26+
Clause-②: yes (narrowing)
27+
28+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author writes is removed or renamed: no spec key, no export and no config field moves, and no stored metadata representation changes, so `objectstack migrate meta` has nothing to rewrite. What moves is the row set a `$like` / `$ilike` query returns on the JavaScript faces, and only for stored values holding a character outside the BMP, where it now equals what every SQLite face already returned. -->

‎.changeset/20176-aggregation-temporal-storage-rule.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ What the rule does, now in one place:
2828
- A value the rule cannot read is compared as written, and so is every non-temporal column, presence tests (`$exists`, `$null`), the text operators and a `{ $field }` reference.
2929
- An object whose declared fields the engine cannot see keeps the previous comparison.
3030

31-
`@objectstack/core` exports the rule as `temporalStorageForm(value, kind)`, `kind` being `'datetime' | 'date' | 'time'`. `driver-sql` (`canonicalUtcDatetime`, `toDateOnly`, `canonicalTimeOfDay`) and `driver-memory` (`coerceTemporalValue`) each carried a copy of it; both now call it. The copies agreed on every shape measured when they were lifted, so no `where`, write or read answer of either driver changes. MySQL still binds a `datetime` in its own literal spelling.
31+
`@objectstack/core` exports the rule as `temporalStorageForm(value, kind)`, `kind` being `'datetime' | 'date' | 'time'`. `driver-sql` (`canonicalUtcDatetime`, `toDateOnly`, `canonicalTimeOfDay`) and `driver-memory` (`coerceTemporalValue`) each carried a copy of it; both now call it. The copies agreed on every shape measured when they were lifted, so the lift itself changes no `where`, write or read answer of either driver (#20203, in the same release, then reads an epoch-millisecond number on a `date` field as its UTC calendar day). MySQL still binds a `datetime` in its own literal spelling.
3232

3333
`@objectstack/objectql`'s `applyInMemoryAggregation(rows, ast, timezone?, fields?)` takes the object's declared field map as an optional fourth argument, and a per-aggregation `filter` reads a temporal comparand by the rule only when it is given. Called without it, the function answers as before.
3434

0 commit comments

Comments
 (0)