Skip to content

Commit 77b9fc1

Browse files
committed
merge origin/main into claude/issue-19228-view-row-ceiling-retire
Two hand-written conflicts, both keeping both intents: - packages/spec/src/ui/view.zod.ts: main re-measured the citations of the per-kind view `limit` docblock at the new console pin; this branch deletes that docblock and its helper with the key. Kept the branch side; main's re-measured text goes with the key it describes. Every other main change to the file auto-merged. - packages/spec/src/ui/component.zod.ts: main re-measured the anchors of the object-kanban "third door" paragraph (the view-face `kanban.limit` spread); this branch deletes that paragraph with the key. Kept the branch side, and annotated main's re-measure list in the same docblock: the seven anchors it records for that paragraph are no longer cited by the block. Generated artifacts are regenerated in the next commit. Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude <noreply@anthropic.com>
2 parents e4204b9 + 5b9402d commit 77b9fc1

226 files changed

Lines changed: 14516 additions & 1084 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: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
'@objectstack/cloud-connection': minor
3+
---
4+
5+
fix(cloud-connection): `POST /api/v1/marketplace/install-local` parses the package id it installs through the manifest declaration (#19576)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING for callers of the install-local door** — a manifest whose `id` is
10+
not reverse-domain notation is now refused `PLUGIN_MANIFEST_INVALID` (`400` for
11+
an inline manifest, `502` for a cloud-fetched snapshot) before anything is
12+
registered or written. It used to install and answer `200`.
13+
14+
The accept set only shrinks back to what the published declaration has always
15+
said. `MANIFEST_ID_PATTERN` (`@objectstack/spec/kernel`) is the one declaration
16+
of a package id, and the other two package-install doors — `POST
17+
/api/v1/packages` and the protocol install primitive — already refuse the ids it
18+
refuses. This door is a separate path: it never calls the protocol primitive,
19+
so neither gate covered it. It derived the id as `manifest.id ?? manifest.name`
20+
and parsed nothing, so `late-app`, `com.example.my_erp`, a number, or a manifest
21+
carrying only a `name` installed cleanly and became the key for the on-disk
22+
ledger entry and for every `:manifestId` route.
23+
24+
The door now asks the declaration **by reference** — `ManifestSchema.shape.id`
25+
— at the one point where the inline branch (after a compiled bundle is
26+
flattened) and the cloud branch have converged, ahead of the `409
27+
MANIFEST_CONFLICT` collision check, the posture gate, the hot-register and the
28+
ledger write. The sentence the caller reads is the declaration's own
29+
(`manifestIdRefusal`), surfaced rather than reworded. Posting `id: 'late-app'`
30+
now answers, in `error.message`:
31+
32+
```text
33+
Invalid package id 'late-app' on `manifest.id`. Expected reverse-domain notation
34+
('com.steedos.crm', 'org.apache.superset') — lowercase dot-separated segments;
35+
hyphens allowed inside a segment, underscores are not. Did you mean
36+
'com.example.late-app'?
37+
```
38+
39+
**`manifest.name` is no longer read as an id.** `ManifestSchema` declares `id`;
40+
`name` is a display label with no pattern. A manifest with no `id` is refused
41+
with the same sentence, naming `manifest.id`. The inline branch's earlier
42+
message for that case — which said the manifest needed an `"id"` or a `"name"`
43+
— is gone with the fallback it described.
44+
45+
**What is not affected.** A conforming id installs exactly as before, on both
46+
branches and for both the flat and the compiled-bundle shape. Ledger entries
47+
already on disk are not re-judged: an entry an older build installed under an
48+
id the declaration refuses still rehydrates at boot and can still be removed
49+
with `DELETE /api/v1/marketplace/install-local/:manifestId`; only a fresh
50+
install under that id is refused. Boot-time and in-process registration
51+
(`manifest.register`, `AppPlugin`) never passes through this door.
52+
53+
**If you are refused.** Give the manifest an `id` in reverse-domain notation —
54+
lowercase dot-separated segments, hyphens allowed inside a segment, underscores
55+
not. The refusal names the key, echoes what was sent and, where a mechanical
56+
repair exists, offers one it has already checked against the rule. Artifacts
57+
built by `os build` from `defineStack()` already carry a conforming id, because
58+
the same declaration refuses anything else at build time.
59+
60+
<!-- 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 — a caller supplies an id the declaration already required, and an id's repair changes the package's identity, so no mechanical mapping could be prescribed even in principle. Existing ledger entries are left exactly as they are. The refusal itself carries the remedy. -->
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/objectql": minor
4+
---
5+
6+
fix(spec,objectql)!: `scale` is retired from the `currency` field type — refused at parse, and no longer enforced on currency writes (#19629)
7+
8+
Clause-②: no (narrowing)
9+
10+
**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. A `currency` field that declares `scale` — any value, `scale: 0` included — no longer parses. The hand-migration prescription is registered under protocol major 18 as `field-currency-scale-refused`.
11+
12+
A currency's decimal places are the currency's, not a setting. On a currency field the key was three-faced. The metadata-admin field designer offered it as stored metadata; the amount's cell never read it, because a currency amount's fraction digits come from its currency's own ISO 4217 minor unit; and the record validator's `max_scale` branch still refused writes carrying more decimals. An author who set `scale: 3` bought a narrower write contract and no visible change. The maintainer's rulings retire the key from the type rather than aligning the money faces to it.
13+
14+
**`@objectstack/spec`** — `FieldSchema` refuses `scale` on `type: 'currency'` with a located issue at `scale`. Its remedy: delete the key; the currency's ISO 4217 minor unit decides how the amount displays, and the field's write allowance stays unconstrained. The remedy names no other key to carry the value. No alias and no grace window. `scale` on `number`, `percent`, `rating`, `slider` and `formula` is untouched, and the key's describe now names that set. Studio's object editor no longer offers `scale` on a currency field: the fields grid of the `objectForm` this package registers in `METADATA_FORM_REGISTRY` now shows it only for `number` and `percent`.
15+
16+
**`@objectstack/objectql`** — the record validator's `max_scale` branch no longer reads `scale` for `currency`, so the type leaves the enforced set. A field definition that reaches the validator without passing `FieldSchema` (stored before this release, or built by hand at runtime) therefore narrows nothing either. `min`, `max` and the finite-number check still apply to `currency`, and `number` / `percent` / `rating` / `slider` still refuse over-scale writes exactly as before. A currency write with more decimals than a former `scale` is now ACCEPTED: the write allowance stays unconstrained, the contract every currency field without `scale` already had. Enforcing a currency width on writes instead was offered to the maintainer and not taken.
17+
18+
## FROM → TO
19+
20+
| you wrote | write instead |
21+
|:--|:--|
22+
| `Field.currency({ label: 'Amount', scale: 2 })` | `Field.currency({ label: 'Amount' })` |
23+
| `{ type: 'currency', scale: 2, currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'USD' } }` | `{ type: 'currency', currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'USD' } }` |
24+
25+
The one-line fix: delete `scale` from every `currency` field. Nothing replaces it, so ⛔ do not re-declare the value under any other key. The currency's ISO 4217 minor unit decides how the amount displays.
26+
27+
What an upgrade changes beyond the refusal:
28+
29+
- **Writes.** A currency value with more decimals than the deleted `scale` is accepted where it used to answer `VALIDATION_FAILED` with field code `max_scale`.
30+
- **Two console faces.** At the console pin measured when this change was written, the grid summary footer and the dashboard metric widget read a currency column's `scale ?? 0`. This change lands only after the console derives both faces from the currency, the way the cell does, and after this repository's console pin has moved past that console change. So in the console bundled with this release, deleting `scale` changes neither face.
31+
32+
## Who is affected, measured
33+
34+
AST sweep on `origin/main` `1f89ba0d70`: 15 `Field.currency` declarations in `examples/` (app-crm 4, app-showcase 11) and 13 documentation code examples carried `scale`, every one `scale: 2`. All were deleted in this change. No platform object, seed or JSON fixture in the tree declares it. Seven test fixtures pinned the old shape and were re-judged. One of them, a flow oracle that needed a live `scale` gate, moved its field from `currency` to `number`.
35+
36+
<!-- adr-0087: registered field-currency-scale-refused -->
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
A metadata form's option-value refusal now says what to do: `defineForm`'s module-load refusal of an inline option `value` that fails the system-identifier grammar names the derive path, and the form field's `options` describe states the rule it belongs to (#19678, #19907).
6+
7+
Clause-②: no
8+
9+
A form option `value` is a lowercase system identifier — `FormSelectOptionSchema` reuses `SelectOptionSchema.value` by reference — so an enum member carrying a hyphen or a capital (`object.managedBy`'s `system-data`, `action.openIn`'s `new-tab`, `action.execution`'s `perRecord`) cannot be written as an inline option at all. That bound stays. An enum-typed metadata-form row may still carry an inline `options` list, to give its members human labels or to offer a deliberate subset. A row whose members cannot be spelled as option values omits `options`: the control derives the members from the served JSON Schema, and their meanings go in `helpText`.
10+
11+
- **The refusal names the remedy.** `defineForm` still throws a `ZodError` at module load with the same issues and codes (`invalid_format` for the pattern, `too_small` for the two-character floor). The grammar message on an inline option's `value` is kept, and now carries the derive path after it, for a row whose members cannot be spelled as option values. Only schema-bound forms built by `defineForm` get this sentence. The grammar message where it is declared (`SystemIdentifierSchema`) is unchanged, because it also bounds object-field options and three object-storage names, where omitting `options` is not the answer.
12+
- **The describe states the rule** on `FormFieldSchema.options`: an inline list is allowed on an enum-typed row, and the derive path is named for a row whose members cannot be spelled. That text is served in the JSON Schema and on the generated reference page.
13+
- ⛔ **No accept-set change.** Every value refused before is still refused, and every value accepted before is still accepted. No key, export or schema shape moves.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/service-analytics": minor
3+
---
4+
5+
fix(service-analytics)!: the analytics `where` door refuses a list in the equality slot instead of reading it as `IN` (#19888)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (already-registered filter-equality-array-comparand-refused) the transition from a list in the equality slot to the operator it stood in for was registered by #19757 for the FilterCondition equality slot at the runtime filter doors; this change adds no new transition, it brings the analytics object-form `where` and the draft-data preview under the one already on the ledger, and the entry's own prescription and acceptance criteria apply verbatim -->
10+
11+
**BREAKING**: this narrows what the analytics faces of `@objectstack/service-analytics` accept. A caller `where`, a dataset `filter` or a measure `filter` that carries a list in the equality slot, `{ field: [...] }` (the empty list included) or `{ field: { $eq: [...] } }`, at any depth under `$and` / `$or` / `$not` or inside a nested relation, compiled before this change. It is now refused with `INVALID_FILTER` / 400. It ships as `minor` under the launch-window convention for accept-set narrowings. The remedy is `$in`:
12+
13+
| you wrote | write instead |
14+
|:--|:--|
15+
| `{ stage: ['won', 'lost'] }` or `{ stage: { $eq: ['won', 'lost'] } }`, meaning "one of these values" | `{ stage: { $in: ['won', 'lost'] } }` |
16+
| `{ stage: ['won'] }`, meaning one value | `{ stage: 'won' }` |
17+
| `{ stage: [] }` | `{ stage: { $in: [] } }`, which matches no row, as the bare list did |
18+
19+
`$in` charts the rows the implicit list charted before. The refusal also names `{ "$contains": "…" }` for "the stored list holds a value" on a multi-value field.
20+
21+
Ruling 乙 of #19757 refuses a list in the equality slot at the shared comparand-shape face in `@objectstack/spec`, for every driver at once. The analytics `where` door met that face only for the `FilterArray` spelling (`['stage', '=', ['won', 'lost']]`), which was already refused. The object spelling was compiled by the analytics filter normalizer, which read one condition four ways:
22+
23+
- `{ stage: ['won', 'lost'] }` compiled to `stage IN (...)`. On the ObjectQL path the engine received `{ stage: { $in: [...] } }`, so the engine's own shared-face check never saw the list.
24+
- `{ stage: { $eq: ['won', 'lost'] } }` compiled to `stage = 'won'`, and `'lost'` was dropped without a word.
25+
- `{ stage: { $eq: [] } }` compiled to no predicate at all, so the chart was drawn over every row.
26+
- `{ stage: [] }` compiled to the FALSE constant.
27+
28+
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.
29+
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. The authoring schema still admits the shape, so such a document 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.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/driver-turso': minor
3+
'@objectstack/cli': minor
4+
---
5+
6+
fix(driver-turso): a REMOTE `TursoDriver` refuses to plan the ADR-0104 media column move instead of answering that there is nothing to move, and `os migrate files-to-references` reports that refusal as a column step it could not judge (#19894)
7+
8+
Clause-②: yes (narrowing)
9+
10+
**BREAKING for callers that plan the media column move on a remote Turso datasource** — `TursoDriver.planMediaColumnMove()` in `remote` transport mode (for example a `libsql://` URL) now throws a `NOT_IMPLEMENTED` / `501` error, where it used to answer `{ plans: [], refusals: [] }`. The `local` (`:memory:` and `file:`) and `replica` modes plan exactly as before, and the local modes plan exactly what `SqlDriver` plans for the same declaration.
11+
12+
What the refusal replaces, measured on the transport's SQLite-backed test double: a table with a `file` and an `image` field, synced through each of the three remote schema doors (`syncSchemasBatch`, `syncSchema`, `initObjects`), held its two TEXT media columns on the remote database, and the remote face answered an empty scan on every door. The inherited planner walks the objects `SqlDriver`'s own schema sync registers, which no remote schema door reaches, and probes each table through the placeholder in-memory Knex connection a remote driver is built with. The local and embedded-replica faces planned two `unquote` moves for the same declaration. `os migrate files-to-references` printed that empty scan as "Column step: nothing to move — this datastore declares no single-value media column".
13+
14+
- **The command reports the refusal instead of failing on it.** `os migrate files-to-references` calls the planner only after the backfill and its self-check have passed, and an `--apply` run has recorded the deployment flag by then. Measured on the command's own test doubles before this change, a planner that throws ended the run with the error alone (`--json`: `{"error": …, "code": "NOT_IMPLEMENTED"}`) and exit 1, with no backfill report, no verify report and no word about the flag it had recorded. The column step now catches a `NOT_IMPLEMENTED` refusal by its code and reports it as a skip: the text face prints `Column step: NOT JUDGED` with the driver's message, and `--json` gains `columnMoveRefused` — `{ error, code }` when the driver refused, `null` otherwise — beside `columnMove: null` and `columnsMovedAt: null`. The backfill, verify and flag reports are emitted as on any other run, nothing is stamped, and the exit code is the self-check's, as it already was for the command's other column-step skips.
15+
- **Any other throw from the planner still fails the command**, through the same error report and exit 1 as before.
16+
- **A genuinely empty scan still reads "nothing to move".**
17+
- **No new error code.** `NOT_IMPLEMENTED` / `501` is a standard code, the envelope this transport already uses for its remote transaction, auto-number, deferred-DDL and drift-detection refusals.
18+
19+
**If you are refused:** the backfill, its self-check and, on `--apply`, the deployment flag are unaffected. A remote Turso datasource keeps its single-value media columns on the JSON encoding: measured on the same double, the remote face writes a file id as a JSON string and reads it back as the id, and it does not read the record of a completed column move.
20+
21+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or reshaped: no spec key, no export, no stored row and no config key — `planMediaColumnMove` keeps its name and its signature, and the command's `--json` document only gains a key. There is no old spelling that maps to a new one: the refused call asked the remote transport for a capability it never delivered, and the refusal itself says what the datasource keeps. -->

0 commit comments

Comments
 (0)