Skip to content

Commit baee423

Browse files
committed
Merge origin/main into claude/issue-19992-currency-config-precision-retire
Conflicts resolved by stacking both sides: #20217's defaultCurrency describe kept beside the removed precision; #20085's view-item conversion and rationale kept beside currency-config-precision-removed; dropped-refinements totals recomputed. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
2 parents a12e057 + 3cb84d0 commit baee423

117 files changed

Lines changed: 6004 additions & 552 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.

‎.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.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: a flattened list view overlay's legacy `options` bag is judged at the view write door, so an out-of-contract `options.KIND` key is refused by name exactly as the direct spelling is (#20051)
6+
7+
**BREAKING** accept-set narrowing on the `view` write door (`PUT /api/v1/meta/view/:name`, the Studio and MCP save). It ships as `minor` under the repo's launch-window convention for breaking changes. This is the door half of ruling A on objectui#10380 (maintainer 「其他同意」).
8+
9+
Clause-②: no
10+
11+
## What was wrong
12+
13+
The flattened list overlay member of `ViewMetadataSchema` re-opens its top level with `.strip()` so the console's round-trip keys survive. That strip also dropped a top-level `options` bag from the parse without looking inside it. `saveMetaItem` stores the request body, not the parse output, and objectui's interface page forwards a stored view's `options` into the list renderer, which merges `options.KIND` under the top-level `KIND` block. Measured on `origin/main` @ `8d1f7ab` through the real save: `timeline: { metaFields: [...] }` answered `422`, while the same key written as `options: { timeline: { metaFields: [...] } }` answered `200` and the row held it as sent.
14+
15+
## What it does now
16+
17+
- **The list overlay declares `options`.** Each `options.KIND` (`kanban`, `calendar`, `gantt`, `gallery`, `timeline`, `chart`, `map`, `tree`) is judged by that kind's own block schema: the same closed key set, the same per-key schemas and the same unknown-key message. The refusal names the key, with only the `options.` prefix added to the path. The kinds are derived from the list-view shape, not listed by hand.
18+
- **Key by key.** The renderer reads the bag as a per-key underlay of the top-level block, so the block's required keys are not asked of it. `kanban: { groupByField, columns }` beside `options.kanban: { titleField }` stays legal, which is the population objectui pins.
19+
- **The bag is closed.** A key that is not a kind (`options.foo`, `options.grid`) is refused by name at `options`. It is no longer dropped.
20+
- **The form overlay pins `options` absent.** Without this, a column-less, type-less list body that the list overlay refused over its bag would be accepted by the form overlay and stored unjudged. The refusal says the bag belongs to a list view.
21+
22+
The legacy `options.map` bag that objectui pins (`locationField`, `titleField`) is still accepted, and it round-trips.
23+
24+
## FROM → TO
25+
26+
| you wrote | write instead |
27+
|:--|:--|
28+
| `options: { kanban: { groupField: 'stage' } }` | `kanban: { groupByField: 'stage', columns: [...] }`, or `options: { kanban: { groupByField: 'stage' } }` |
29+
| `options: { calendar: { dateField: 'kickoff' } }` | `calendar: { startDateField: 'kickoff' }` |
30+
| `options: { timeline: { metaFields: ['region'] } }` | delete `metaFields`: the timeline block has no such key |
31+
| `options: { chart: { xAxisField, yAxisFields } }` | the dataset-bound block, `chart: { dataset, values, dimensions }` |
32+
| `options: { foo: 1 }` | delete `foo`: the bag carries per-kind blocks only |
33+
| `options: {...}` on a form overlay (`viewKind: 'form'`) | delete `options`: a form view has no per-kind blocks |
34+
35+
**The one-line fix:** read the refusal. It names the key and the block that refuses it; move the key to the top-level block's declared spelling, or delete it.
36+
37+
## Stored-row census (ruling item 3)
38+
39+
- **objectstack** @ `8d1f7ab` (examples, dogfood, fixtures, tests): zero view bodies carry a top-level `options` bag with a kind block. Authored views go through the strict authoring shape, which has always refused `options`.
40+
- **objectui**, at the `.objectui-sha` pin `f8a9d0fb0` and at `main` `c3a26ccda`: 28 `options` bag literals, plus the finding's own probe body, judged against this change. 17 pass and 12 fail, and every failure is an out-of-contract key refused by name. None fails for a missing key.
41+
- **Bodies that model a stored or authored view** (a console-merged `listViews` entry or a named view): 8, of which 5 pass, the pinned `options.map` path among them. The 3 that fail: `options.kanban.groupField` in `plugin-view` `ObjectView.tsx`'s docblock example (write `groupByField`), `options.calendar.dateField` in `ObjectView.calendarAliasRefused-8355.test.tsx` (write `startDateField`; that test already pins the alias as refused on objectui's side), and the finding's `options.timeline.metaFields` (delete it).
42+
- **Renderer-level `ListView` props** (21), which never reach this door: 12 pass. The 9 that fail spell the legacy keys objectui's own refusal pins already retire (`groupField`, `groupBy`, `dateField`, `metaFields`, and the object-bound chart keys `xAxisField` / `yAxisFields` / `aggregation`).
43+
- **Production `sys_metadata` rows: NOT MEASURED.** No deployment's store is reachable from the repository. A stored row that fails keeps being read and served exactly as stored. It is refused only on its next save, and the refusal names the key.
44+
45+
## Not in this change
46+
47+
Persisting the parse output instead of the request body (ruling item 2) is not in this change. This change leaves the save path's storage behaviour as it was: a body the door now accepts is stored as sent, so every stored `options` bag is one the door judged.
48+
49+
<!-- adr-0087: registered view-overlay-options-bag-judged -->

0 commit comments

Comments
 (0)