Skip to content

Commit 38159d1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21445-object-grid-typed-members
2 parents 52c4c42 + 713b0fa commit 38159d1

85 files changed

Lines changed: 5682 additions & 858 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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/lint': patch
4+
---
5+
6+
`page.requires` says what the runtime now does with it: refused at save, reported at load (ADR-0080 §5).
7+
8+
Clause-②: no
9+
10+
The key's description used to say the list is "validated at save and load" while the liveness ledger recorded it as not enforced yet. Both are now true and say so. On a server that has the deployment's SDUI component manifest, saving a `kind: 'html'` page compiles its source, refuses a written `requires` that disagrees with it (`422 INVALID_METADATA`, `page-requires-disagrees-with-source`; a draft at its publish) and stores the derived list. At load, a stored page whose list names a plugin no manifest component carries is reported and still served. A server with no manifest checks neither and says so once at boot. Omit `requires`: it is derived from the source. The liveness row moves from `planned` to `live`, and the generated page reference carries the new description.
11+
12+
`validateJsxPages`' reason for staying off the runtime publish gate no longer says it parses through `typescript`/`sucrase`. It parses with the dependency-free `@objectstack/sdui-parser`, and it stays CLI-only because the save door already runs that compiler on every html page. The `ui-html-page-div-refused` upgrade-guide entry now names that save door too: on a server with a manifest, a `div` page saved from Studio or through the metadata API is refused under the same rule ids.
13+
14+
No schema accepts or refuses anything it did not before, and no runtime behaviour changes.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
'@objectstack/service-analytics': minor
3+
---
4+
5+
fix(service-analytics)!: both analytics strategies refuse a cube measure whose `type` names no aggregate, in the spec's words — the custom-SQL `EXPRESSION_METRIC_TYPES` partition is gone with the three types it named (#21000)
6+
7+
**BREAKING** — `@objectstack/spec` retired the cube metric types `number`, `string`
8+
and `boolean` from `AggregationMetricType` (a measure's `sql` is a column reference,
9+
so they had nothing left to compute). Every door that parses a cube refuses them;
10+
this release removes the runtime branches that still served them for a cube that
11+
reached the analytics service WITHOUT meeting that parse — one a host registers
12+
in-process from a literal, through `AnalyticsServicePlugin({ cubes })` or
13+
`AnalyticsService({ cubes })` (the registry never parses).
14+
15+
| | before | now |
16+
| --- | --- | --- |
17+
| `NativeSQLStrategy`, a measure typed `number` / `string` / `boolean` | served: the column emitted UNAGGREGATED in the statement (`amount AS "m"` beside `GROUP BY`) | refused, nothing executed |
18+
| `ObjectQLStrategy`, the same measure | refused `INVALID_FIELD` / 400 | refused, nothing executed |
19+
| either strategy, a type the spec never declared (`median`) | native: refused; ObjectQL: forwarded to `executeAggregate` as the method (the auto-bridge refused it; a host's own executor received it), and `/analytics/sql` echoed `MEDIAN(amount)` | refused, nothing executed |
20+
21+
**The one refusal** is `aggregateOfMeasure`'s, shared by both strategies and both
22+
doors (`POST /analytics/query` and `POST /analytics/sql`): it names the measure and
23+
the cube, then quotes the spec's own verdict on the type — for a retired type the
24+
retirement prescription (the six aggregates to choose from, and where a per-row or
25+
derived value goes instead), for anything else zod's message listing the six. It is
26+
a bare `Error`, the undeclared-500 tier this package assigns to a cube that never
27+
met the parse, so the HTTP answer is `500` with the message readable in the body
28+
(measured through the dispatcher's analytics route), never a caller-blaming `400`.
29+
The ObjectQL envelope for the three retired types therefore moves from
30+
`INVALID_FIELD` / 400 to that tier.
31+
32+
**The fix:** give the measure one of the six aggregate types — `count`, `sum`,
33+
`avg`, `min`, `max`, `count_distinct` — or parse the cube through `CubeSchema`
34+
before registering it, which refuses the same types with the same prescription.
35+
36+
**Removed export:** `EXPRESSION_METRIC_TYPES` from
37+
`strategies/native-sql-strategy.ts` (internal to the package; not re-exported from
38+
its entry point). **Unchanged:** every aggregate measure on both strategies, the
39+
auto-bridge's own parse of an engine method (still pinned, driven directly), and
40+
`GET /analytics/meta`, which keeps publishing each registered measure's `type` as
41+
registered.
42+
43+
Clause-②: no (narrowing)
44+
45+
<!-- adr-0087: registered cube-metric-expression-types-retired -->
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: retire the cube metric types `number`, `string` and `boolean` — a measure's `sql` is a column reference, so the custom-SQL-expression types had nothing left to compute (#21000)
6+
7+
**BREAKING** — three members leave `AggregationMetricType`, so a cube measure's
8+
`measures.<metric>.type` no longer accepts `number`, `string` or `boolean`. ADR-0049
9+
enforce-or-remove. They declared "a custom SQL expression returning a number /
10+
string / boolean": the measure's `sql` was the whole computation. A cube member's
11+
`sql` is a column reference since `cube-member-sql-expression-retired` (#20943), so
12+
the three were left naming nothing: measured before this change, the raw-SQL
13+
analytics path returned the referenced column UNAGGREGATED (a bare column in a
14+
grouped statement — by SQL's own rules an error on PostgreSQL and an arbitrary row's
15+
value on SQLite), and the ObjectQL path refused the measure. The six aggregates — `count`, `sum`,
16+
`avg`, `min`, `max`, `count_distinct` — are unchanged and are now the whole
17+
vocabulary.
18+
19+
### FROM → TO
20+
21+
| removed | what to write instead |
22+
| --- | --- |
23+
| `measures.<metric>.type: 'number'`, `'string'` or `'boolean'` | the aggregate the measure means: `sum`, `avg`, `min` or `max` over the column; `count` (over `'*'` for a row count, or over a column for its non-null values); or `count_distinct`. |
24+
| a measure whose old expression computed a value per row | keep that value as a field of the object (a stored or formula field) and aggregate the field. |
25+
| a measure whose old expression combined measures (a ratio, a difference) | `derived: { op, of: [...] }` on an ADR-0021 dataset over the same object. |
26+
27+
**The one-line fix: give the measure an aggregate type.** There is no mechanical
28+
rewrite — the column alone does not say whether `amount` meant its sum, its average
29+
or its largest value — so `os migrate meta` lists nothing for this change.
30+
31+
Each retired member is refused at parse with a prescription naming the six
32+
aggregates, at the measure's `type`, and in `tsc` (the members are gone from the
33+
`AggregationMetricType` type). A value the enum never declared keeps zod's own
34+
message.
35+
36+
### The retirement kit
37+
38+
- **Value-level retirement.** `AggregationMetricType` is declared through
39+
`enumWithRetiredValues` (`shared/retired-key.ts`), with the prescriptions
40+
module-private. No authorable KEY and no def changed, so nothing lands in
41+
`RETIRED_KEYS_BY_MAJOR`, and the four surface ratchets (`api-surface`,
42+
`authorable-surface`, `json-schema.manifest`, `api-surface-signatures`) are
43+
byte-identical.
44+
- **No D2 conversion, by design.** A stored or built cube that still carries one of
45+
the three is REFUSED, never rewritten or dropped: the boot door
46+
(`ObjectStackDefinitionSchema`, which a built artifact is parsed through), the
47+
`analytics_cube` write door and `defineStack` refuse it with the prescription, and
48+
the rehydration seam replays no conversion over it.
49+
- **D3 entry `cube-metric-expression-types-retired`**, with its step-18 rationale
50+
fragment, carries the judgement the upgrader owes: which aggregate each measure
51+
meant.
52+
- **Liveness.** The `analytics_cube` row `measures.type` stays `live`, re-verified
53+
2026-10-02, with the narrowing recorded.
54+
- **Docs.** The `data/analytics` reference page is regenerated.
55+
- **No deprecation window**, per the project's startup-stage posture.
56+
57+
### Reach, measured
58+
59+
- This repository authors no cube measure of the three types outside tests:
60+
`examples/**`, `packages/**` (the platform objects included) and the skills and
61+
docs carry none. The showcase cube's `type: 'string'` entries are dimensions,
62+
whose `DimensionType` is a separate enum and is unchanged.
63+
- objectui at its pinned commit carries no `AggregationMetricType` mirror and no
64+
cube measure of the three types.
65+
- Out-of-repo authored cubes: NOT MEASURED.
66+
67+
Clause-②: no (narrowing)
68+
69+
<!-- adr-0087: registered cube-metric-expression-types-retired -->
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Liveness ledger README: the "Author warnings" section now describes the model the liveness lint ships. A `dead`, `live-elsewhere` or `experimental` verdict warns on its own, and `authorWarn` only opts a `planned` row in.
6+
7+
Clause-②: no
8+
9+
- The section said warnings were opt-in per ledger row, and that only `experimental` warned without the marker. That stopped being true when the lint made a `dead` or `live-elsewhere` verdict warn on its own. The section now has one table of which verdicts warn, and under which rule id.
10+
- `authorHint` no longer "falls back to `note`". Every warning shows the row's `authorHint`, or else the verdict's default hint. The `note` never reaches an author.
11+
- Rule 1 now talks about the verdict, not the marker. Grading a row `dead`, `live-elsewhere` or `experimental` warns every author who sets the key, and fails their `os lint --strict` / `os validate --strict` run. No marker keeps it quiet, so a benign display key is measured against the designer-previews ruling before it is graded `dead`.
12+
- Rule 2 (booleans) now covers any key whose schema default materializes. It no longer points at an `_authorWarnSkipped` marker, which no ledger carries.
13+
- The coverage paragraph states the walk's real reach: the types it visits, one level of `children`, and that a governed type it does not visit warns no author through this lint.
14+
- Two sentences elsewhere in the README said a `dead` row needs `authorWarn` to warn. Both are corrected the same way.
15+
- ⛔ Documentation only: no ledger row, schema, export or lint behaviour changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@objectstack/objectql': minor
4+
'@objectstack/mcp': minor
5+
'@objectstack/plugin-audit': minor
6+
'@objectstack/service-analytics': minor
7+
'@objectstack/cli': minor
8+
---
9+
10+
fix(metadata-protocol)!: a metadata body's stored content hash is served and compared only in keyed form, never copied, and never evaluated (#21207)
11+
12+
Clause-②: yes (narrowing)
13+
14+
<!-- adr-0087: not-required (no-migration-prescription) the stored content hash of a metadata body stays the canonical hash at rest and no metadata body, authorable key, spelling or export moves; what changes is the form a door serves the hash in (a keyed digest: the crypto provider's, or a process-scoped ephemeral key's when none is registered), the form an inbound version token is compared in, and which query shapes the doors accept over the two hash columns, so `objectstack migrate meta` has nothing to rewrite. The operator-run rewrite this release asks for is of audit, activity and decision-audit copies, not of metadata. The other categories are closed on facts: every package here publishes (not `unpublished`); no ADR-0087 id covers a served version token or a refused query shape (not `registered` / `already-registered`); and the change is runtime behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
15+
16+
**BREAKING**: this narrows what the metadata doors serve and accept for the stored content hash of a metadata body — a hash over the whole stored body, withheld credential material included. Served beside the projected body it let a reader confirm a guess at that material offline; filtered on, it confirmed one online. It ships as `minor` under the launch-window convention for accept-set narrowings.
17+
18+
**Three things change for callers and operators.**
19+
20+
1. **A held version token gets one `409 METADATA_CONFLICT`.** Every door that hands out a metadata version token — the save, publish, package-publish and rollback receipts and the history read — now hands out a keyed digest of the stored hash instead of the hash itself, and the save and reset doors compare a token they are sent in that same form. The key is the crypto provider's; a host that registers none keys under a process-scoped ephemeral key instead, so a token is always issued and never empty. A token a client held from before the upgrade is refused once; take the token from the next read or receipt and retry. On a host with no provider the same happens after a restart, and on any host when a provider is first registered. An empty, withheld, raw or stale token is refused with the same `409`; it is never read as "no pin".
21+
2. **Filter, sort and group on the two stored content-hash columns, and on the version history's change note, now answer `400 INVALID_FIELD`** — on the generic data door, the MCP stdio reader and the analytics door, before the engine runs. The change note is included because a draft promotion that stated no message of its own recorded the draft's stored hash in it; the publish door now always states a hash-free message, and a note written before this release is served with the quoted hash in keyed form. A data-door search over the two stored-metadata tables no longer scans those columns or the stored body column, and an explicit search-field list naming one answers the same `400`. Every other column of the two tables is served, filtered, sorted and grouped as before, and every other object is unchanged.
22+
3. **Operators run `os migrate audit-metadata-bodies` once after upgrading, dry run first.** The audit ledger, the activity feed and the metadata decision-audit trail no longer copy the stored hash. The extended command drops it from the copies already written and withholds it in the decision-audit notes and their copies: a dry run by default, `--apply` to rewrite, idempotent. The version history stays the lineage.
23+
24+
**What else changes.** The data door serves the two hash columns of the stored-metadata tables in keyed form, under the same key as the version tokens. The MCP stdio reader serves them keyed under the crypto provider's key, and omits them on a host with no provider. A `409` conflict refusal carries keyed values or none. The ObjectQL engine gains a read accessor for the registered provider's keyed digest; it is additive. A member's read of these tables is refused as before.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/runtime': patch
4+
'@objectstack/objectql': patch
5+
---
6+
7+
fix: when the store refuses an uninstall's `sys_packages` delete, the uninstall now answers the failure and removes nothing else, instead of answering success and coming back after the next restart (#21276)
8+
9+
Clause-②: no
10+
11+
**`@objectstack/metadata-protocol`.** `deletePackage` now deletes the package's `sys_packages` row first, before its `sys_metadata` rows, its tables, its registry entry and the rows the uninstall cleanups own. When the `package` service refuses that delete, whether it returns `{ success: false }` or throws, `deletePackage` throws and nothing else is removed. A store fault answers `500`, with `DATABASE_ERROR` from a live SQL driver and `INTERNAL_ERROR` otherwise. A declared 4xx refusal is passed through unchanged. Before this, the refusal was logged as a warning, and `DELETE /api/v1/packages/:id` answered `200` after the package's metadata, tables and grants had been removed. The package then came back after the next restart.
12+
13+
Before that store delete, `deletePackage` now also asks the registry whether the uninstall would be refused because another package extends an object this package owns (ADR-0029). If so, it throws the registry's own refusal with nothing removed. A registry without the new question is not asked, and the refusal then surfaces at the registry withdrawal, as before.
14+
15+
**`@objectstack/objectql`.** New: `SchemaRegistry.assertPackageUninstallable(packageId)`. It throws the refusal `unregisterObjectsByPackage` and `uninstallPackage` raise for an object another package extends, with the same message, and it changes nothing. `unregisterObjectsByPackage` now calls it, so there is still one copy of that check.
16+
17+
**`@objectstack/runtime`.** `DELETE /api/v1/packages/:id` now asks `deletePackage` before it touches anything. It checks that the package exists with a read, and it withdraws the package from the running registry and clears its saved disable record only after `deletePackage` has answered. So when the store refuses, the door answers `500`, the same process keeps serving the package, and a package that was disabled stays disabled after a restart. Before this, the door withdrew the package and cleared its disable record first. A refused delete then left the package missing until a restart, and brought a disabled package back enabled.
18+
19+
An uninstall refused because another package extends an object this package owns still answers `500` with nothing changed: the stored rows, the registry entry and the disable record all stay as they were, in the same process and after a restart. That refusal is now decided before the store delete, instead of by the door withdrawing the package first. An ordinary uninstall, and a host with no `package` service, are unchanged.

0 commit comments

Comments
 (0)