Skip to content

Commit a4b05d6

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20206-lint-packages-non-array-refused
2 parents 4d221c0 + ab82001 commit a4b05d6

137 files changed

Lines changed: 7957 additions & 630 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. -->

‎.changeset/19888-analytics-implicit-array.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ Ruling 乙 of #19757 refuses a list in the equality slot at the shared comparand
2727

2828
Each list is now handed to the shared face's equality arm before any node is built. Both spellings of one condition therefore get the same refusal, with the same wording, path and `$in` prescription. The draft-data preview runs the same gate, so a drafted chart refuses what the published chart refuses. Before, it compared each row against the list's string form.
2929

30-
Who is affected: nothing in this repository's examples, seeds or docs authors the shape (measured over `examples/**`, `packages/**` and the fenced code in the docs). Stored datasets, dashboard widget filters, report runtime filters and measure filters in a deployment were NOT measured. In this release the authoring schema refuses the shape too, when such a document is saved (a separate change in `@objectstack/spec`, ADR-0087 entry `filter-equality-array-comparand-refused-at-save`). One position is judged only here: a list inside a nested relation, which this door flattens to a dotted member. A document carrying that still publishes, and it is refused when it is charted. The refusal names the field and the path. `$ne` with a list is not part of the ruling and is not judged here. The list operators (`$in`, `$nin`, `$between`) keep their lists, and every scalar, `null` included, compiles as before.
30+
Who is affected: nothing in this repository's examples, seeds or docs authors the shape (measured over `examples/**`, `packages/**` and the fenced code in the docs). Stored datasets, dashboard widget filters, report runtime filters and measure filters in a deployment were NOT measured. In this release the authoring schema refuses the shape too, when such a document is saved (a separate change in `@objectstack/spec`, ADR-0087 entry `filter-equality-array-comparand-refused-at-save`). One position is judged here and not by the shared authoring schema: a list inside a nested relation, which this door flattens to a dotted member. A dataset `filter` or measure `filter` carrying one is refused when it is saved, in the same words (a separate change in `@objectstack/spec`, ADR-0087 entry `dataset-filter-nested-relation-equality-array-refused-at-save`). Any other `where` that reaches this door with one, such as a caller `where` or a dataset selection's `runtimeFilter`, is refused only when it is charted. The refusal names the field and the path. `$ne` with a list is not part of the ruling and is not judged here. The list operators (`$in`, `$nin`, `$between`) keep their lists, and every scalar, `null` included, compiles as before.

‎.changeset/19889-filter-schema-door-array-equality.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Two request doors parse these carriers, and they now answer before the analytics
2121
## What does NOT change
2222

2323
- **Nothing stored is rewritten, and nothing is dropped.** The parse fails and strips nothing. The read path does not re-validate stored rows, so a stored document keeps loading, and its next save is refused. Such a filter has failed every query since the equality-slot change, so the refusal is a repair.
24-
- **The reach is the face's, and no wider.** A field spec with no `$` key, such as the nested-relation condition `{ account: { region: ['a'] } }`, is not judged, because the face does not judge it either. The analytics `where` door does refuse that shape when a dataset or measure filter is charted.
24+
- **The reach is the face's, and no wider.** A field spec with no `$` key, such as the nested-relation condition `{ account: { region: ['a'] } }`, is not judged by `FilterConditionSchema`, because the face does not judge it either. The analytics `where` door does refuse that shape, because it flattens the relation to a dotted member. So the two carriers that door charts, the dataset `filter` and the measure `filter`, refuse it on save as well, in the same words. That is a separate change in this release, ADR-0087 entry `dataset-filter-nested-relation-equality-array-refused-at-save`.
2525
- **The data-engine calls' `where` option still parses.** Its type is a union whose first arm is an open record. The face refuses the shape when the call runs.
2626
- **`$ne` carrying an array is not judged.**
2727
- The list operators keep their arrays, `$in: []` and `$nin: []` included. Every scalar, `null`, a `Date` and a `{ $field }` reference pass as before.

0 commit comments

Comments
 (0)