Skip to content

Commit 11427ba

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-18386-export-import-template
2 parents 21aea1a + 7a09eee commit 11427ba

643 files changed

Lines changed: 21910 additions & 2993 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
'@objectstack/plugin-audit': patch
4+
---
5+
6+
fix(platform-objects,plugin-audit): Setup and Studio navigation entries for the console's Audit Log and Integrations & APIs pages (#20142)
7+
8+
The console retired its System Hub card wall and its Developer Hub, which had been the only in-app links to several pages, and registered each page under a component-registry key instead. Framework navigation reaches a console page only through a `type: 'component'` item that names such a key, and no item named them, so each page was reachable only by a typed URL. Two entries now name the pages whose capability ships in the open framework:
9+
10+
| Entry | App / group | `componentRef` | Contributed by | Gate |
11+
| --- | --- | --- | --- | --- |
12+
| `nav_audit_log_browser` ("Audit Log Browser") | Setup / Diagnostics, directly under Audit Logs | `audit:log` | `@objectstack/plugin-audit` | none: it lives and dies with the plugin that owns `sys_audit_log` |
13+
| `nav_integrations` ("Integrations & APIs") | Studio / Developer, after Public Forms | `developer:integrations` | `@objectstack/platform-objects` | none beyond Studio's own `studio.access` |
14+
15+
**Two audit entries, on purpose.** The existing Audit Logs entry (the `sys_audit_log` object view) stays. It carries the named list views, search, and the actor and tenant rendered as resolved lookups. The new page adds one filterable table whose detail drawer pretty-prints a change's before and after JSON, where the record page shows `old_value` / `new_value` as raw text. Neither surface replaces the other.
16+
17+
**No entry for the console's AI Approvals page (`ai:approvals`) here.** Under ADR-0029 D7, each capability plugin contributes its own navigation entries into a Setup slot, and the Setup shell does not enumerate capability objects. The AI pending-action queue belongs to the AI capability, whose provider (`@objectstack/service-ai`) ships in Cloud/Enterprise, not in the open framework. Its entry is therefore that capability's to contribute.
18+
19+
The keys are the ones the console registers at the objectui commit this release's console is built from. Labels ship in all four locales (en, zh-CN, ja-JP, es-ES), with their source hashes recorded. Nothing is removed or renamed, and there is nothing to migrate.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
fix(cli)!: `objectstack validate`, `objectstack build` and `objectstack lint` read the project's `sdui.manifest.json` beside the config they were given, not in the directory they were run from (#20166)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) No metadata changes shape and nothing an author wrote is renamed or removed, so `objectstack migrate meta` has nothing to rewrite. What moves is which manifest file judges a run whose config path names another directory. -->
10+
11+
**BREAKING for runs given a config path in another directory.**
12+
13+
**What changed.** These commands check the `source` of each `kind: 'html'` page
14+
against an SDUI component manifest: the project's own `sdui.manifest.json` first,
15+
then the copy `@objectstack/console` ships. They located everything else about a
16+
project from the directory of its config, but looked for the project's own
17+
manifest in the directory the command was run from. So
18+
`objectstack validate path/to/app/objectstack.config.ts`, run from anywhere else,
19+
never read `path/to/app/sdui.manifest.json`, and a manifest that happened to sit in
20+
the directory it was run from judged a project it does not belong to. They now read
21+
the manifest beside the config.
22+
23+
## Which manifest each run reads
24+
25+
| the run | the project manifest it read | the project manifest it reads now |
26+
|:--|:--|:--|
27+
| `objectstack validate` / `build` / `lint` with no config path, in the project's directory | `./sdui.manifest.json` | `./sdui.manifest.json` (unchanged) |
28+
| the same commands given `path/to/app/objectstack.config.ts`, run from another directory | that other directory's `sdui.manifest.json` | `path/to/app/sdui.manifest.json` |
29+
30+
When the project carries no manifest of its own, both rows then fall back to the
31+
copy `@objectstack/console` ships, as before.
32+
33+
**Which runs change, and which way.** Only runs whose config path names a directory
34+
other than the one they run in. For those, the verdict can move in both directions:
35+
36+
- A page the project's own manifest does not declare is now refused
37+
(`jsx-forbidden-tag`, `jsx-unknown-component`, `jsx-unknown-prop`, exit 1), where
38+
the other directory's manifest, or the console's copy, used to admit it.
39+
- A project manifest that is present but not usable is now refused (exit 1), naming
40+
that file, where the run used to read some other file.
41+
- A project with no manifest of its own is now checked against the console's copy,
42+
where the other directory's manifest used to decide.
43+
- In the other direction, a page the other directory's manifest refused, and that
44+
the project's own manifest (or the console's copy) declares, is now admitted.
45+
46+
If such a run now fails, the manifest that belongs to the project is the one to
47+
keep beside its config.
48+
49+
**What is not affected.** A run in the project's own directory, with or without a
50+
config path, reads the same file as before. `objectstack init`'s check of a freshly
51+
generated scaffold keeps reading the directory it was run from.
52+
53+
**A correction to this release's console-fallback entry.** That entry says these
54+
commands "look first for the `sdui.manifest.json` in the directory the command runs
55+
in". From this release they look first beside the config the command was given,
56+
which is the same directory whenever the command runs in the project.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The `manifest`, `dataset` and `permission` liveness ledgers cite the commit that decided each note, or say the decision in words, instead of a tracker number that no longer resolves
6+
7+
Clause-②: no
8+
9+
Notes in these three ledgers named GitHub issues that no longer exist, so a reader could
10+
not tell why a row carries its verdict. Each such note now either names the commit that
11+
made the decision or, where the number alone carried the meaning, says what was decided. The
12+
`liveness/` ledgers ship in this package's tarball, which is why this is a release note at
13+
all. Note text only: no row's status, evidence, proof or date changes, and no schema,
14+
export or runtime behaviour changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-analytics': minor
4+
---
5+
6+
An authored analytics cube's measure `format` and time-dimension `granularities` now take effect on the analytics query doors, the way a compiled dataset's always have (#20282).
7+
8+
Clause-②: yes (narrowing)
9+
10+
<!-- adr-0087: registered analytics-cube-single-granularity-default-enforced -->
11+
12+
**BREAKING**: this narrows what `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` answer for one class of request. When an authored cube's time dimension declares exactly one granularity, a query that groups by that dimension without stating a granularity is now bucketed at the declared one. The raw-SQL path declines every bucketed query, so such a query now runs on the engine aggregate path, which answers `400 INVALID_FIELD` for every member it cannot evaluate: a custom-SQL measure (a measure of type `number`, `string` or `boolean` whose `sql` is an expression); and, on a cube whose members resolve through its `joins`, a measure or a `where` field over a joined object, a `timeDimensions` entry over a joined object (bucketed or a `dateRange` window, so grouping by a one-granularity time dimension over a joined object is refused too), a dimension that traverses more than one relationship, and an `avg` or `count_distinct` measure beside any dimension over a joined object. The raw-SQL path answers every one of these, with one group per distinct timestamp; each is now refused, exactly as it already was when the caller stated that granularity by hand. On a host that overrides `queryCapabilities` to offer raw SQL with no engine aggregate bridge (the plugin's default wires both), no strategy remains for a bucketed query, so every newly bucketed query, a plain `count` included, now answers "No strategy can handle query" instead of grouping raw timestamps. The remedy: run such a query without grouping by that dimension, or, if the dimension is not meant to have one default bucket, declare the granularities it offers as a list of two or more (or omit the key); on a raw-SQL-only host, add the engine aggregate bridge. It ships as `minor` under the launch-window convention; the widening half is two authored keys taking effect.
13+
14+
Until this change both keys were read on the compiled-dataset path only. One cube shape has three producers — cubes authored with `defineCube()` / `defineStack({ analyticsCubes })`, cubes the dataset compiler mints, and cubes inferred for an ad-hoc query — and only a compiled dataset's cube reached the two readers:
15+
16+
- **`measures.<metric>.format`** reached a caller as `fields[].format` only because the dataset door copies it from the DATASET measure. An authored cube has no dataset, so `POST /api/v1/analytics/query` described its measure columns with `name` and `type` alone. Now every measure column a query names carries the `format` its cube measure declares, whichever strategy answered, and a column that declares none carries no `format` key at all. `GET /api/v1/analytics/meta` is unchanged: its projection stays `name`, `type` and `title`, and a client reads `format` off the query result's `fields[]`, as the Data API page already says. The value is relayed verbatim; the vocabulary `fields[].format` documents is a numeral pattern such as `"$0,0.00"` or `"0.0%"`.
17+
- **`dimensions.<dimension>.granularities`** was the default bucket only for a compiled dataset, which the dataset executor filled in before querying. An authored cube's time dimension grouped raw timestamps whatever it declared. Now `query()` and the `generateSql()` dry run read it the same way, through the one rule both paths share: a single-entry list is the dimension's default bucket for a query that groups by it; a granularity the query states always wins, and one the list does not name is not refused (the dataset path compares against no list either); a list of two or more states no default; and a `timeDimensions` entry that carries only a `dateRange` for a dimension the query does not group stays a filter.
18+
19+
What to expect after upgrading:
20+
21+
- **A cube measure that declares `format`** now carries it on `POST /api/v1/analytics/query` results. A client that formats amounts from `fields[].format` starts formatting that column.
22+
- **A cube time dimension that declares one granularity** (`granularities: ['month']`) is now bucketed by it when a query groups by it without stating one: one row per month where there was one row per timestamp. Name another granularity in the query's `timeDimensions` to bucket differently.
23+
- **A cube time dimension that declares several, or none**, behaves exactly as before.
24+
- **Compiled datasets** (`POST /api/v1/analytics/dataset/query`) answer exactly as before: the value read off their cube is the one the dataset door already used.
25+
26+
In `@objectstack/spec`, `MetricSchema.format` and `DimensionSchema.granularities` now carry descriptions that state what the analytics service does with them (the metric's example values move from the names "currency" / "percent" to numeral patterns, the vocabulary the `fields[].format` slot documents), and the liveness ledger rows `analytics_cube.measures.format` and `analytics_cube.dimensions.granularities` move from `dead` to `live`, citing the new readers.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-analytics': minor
4+
---
5+
6+
`GET /api/v1/analytics/meta` now publishes an analytics cube's `description`, each measure's and dimension's `description`, and each measure's `format`, when the cube definition declares them (#20282).
7+
8+
Clause-②: yes (widening)
9+
10+
- `CubeMeta` (`@objectstack/spec/contracts`) gains an optional `description` on the cube and on each measure and dimension, and an optional `format` on each measure. `AnalyticsMetadataResponseSchema` declares the same members. A definition that declares none of them is published exactly as before.
11+
- `AnalyticsService.getMeta` copies what the definition declares and fills in nothing. A cube compiled from a dataset carries each dataset measure's `format` and no `description`.
12+
- The liveness ledger rows `analytics_cube.description`, `measures.description` and `dimensions.description` move from `dead` to `live`.
13+
14+
This supersedes one sentence of this release's note on an authored cube's measure `format` and `granularities`: it says `GET /api/v1/analytics/meta` is unchanged and keeps `name`, `type` and `title`. With this change `/meta` also publishes each measure's declared `format`. A client that formats a result column still reads `format` off the query result's `fields[]`.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): the stored-filter conversion rewrites a filter on a block whose rows are inline, as it does on any other block
6+
7+
The ADR-0087 D2 conversion `page-component-filter-record-to-rule-array` no longer leaves every filter of a page component whose rows are inline (`data: { provider: 'value', … }`, a `data` array, or `staticData`) as stored. Such a filter, the binding's `dataSource.filter` included, is now rewritten to the `[{ field, operator, value }, ...]` rule array exactly as it is on a block that queries an object. What still stays as stored, and is still reported as a TODO, is only a filter with a part that has no lossless rule spelling: a combinator, a null value, or an operator the rule vocabulary does not spell. That holds on any block.
8+
9+
Why the conversion declined, and why it no longer needs to: the `object-map`, `object-tree`, `object-calendar` and `object-gantt` blocks match that filter against their own rows in objectui's in-memory data source (`ValueDataSource.find`). The conversion was written against an objectui version whose `find` excluded every row for a rule array, so it left those filters alone and said so in the TODO. The objectui version this repository pins (`.objectui-sha`, the same pin the previous release shipped) lowers a rule array before it matches, and it selects the rows the stored form selected. That was measured over every operator the conversion maps: 114 filters on eight rows, null and missing values included. The same filters select no row on the objectui build just before that fix. So the decline was already protecting nothing: it only left convertible filters unconverted and reported TODOs that no longer needed to exist.
10+
11+
What an operator sees:
12+
13+
- `os migrate meta --stored` now lists such a page as a pending rewrite. It used to list it as a `skipped` row with a TODO. A preview over a database whose only legacy filters sat on inline-row blocks therefore exits 1 until `os migrate meta --stored --apply` rewrites them.
14+
- Until then, every stored-row read replays the same rewrite, so the block reads the rule array and shows the same rows.
15+
- Nothing an author writes is accepted or refused differently. The conversion stays retired from the authoring path, and no schema changes.
16+
17+
The migration entries `element-data-source-and-object-block-filter-rule-array` and `object-grid-default-filters-rule-array`, and the protocol-18 step rationale, no longer say that inline-row filters are left as stored.
18+
19+
ADR-0087 disposition: already registered. This changes the behaviour of the registered D2 conversion `page-component-filter-record-to-rule-array` and edits its two D3 entries. There is nothing new to register.
20+
21+
Clause-②: no
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/objectql': patch
3+
'@objectstack/service-automation': patch
4+
'@objectstack/runtime': patch
5+
---
6+
7+
Warnings, refusals and hints that cited a tracker number now say what was decided
8+
9+
Clause-②: no
10+
11+
Several runtime strings an author or operator reads sent the reader to an issue-tracker number for
12+
the reason behind them. Each now states that reason in the sentence itself:
13+
14+
- `@objectstack/objectql`: the two data-event warnings. A write that names no single record publishes
15+
no per-record event rather than one with an empty `recordId`; a predicate (`multi: true`) write
16+
publishes its own `data.records.*` event carrying the affected-row count and nothing else, so a
17+
driver result that is not a count publishes no bulk event either.
18+
- `@objectstack/service-automation`: the warning for a pausing node type that never declares
19+
`resumeAuthority`, the generic-route resume refusal (its log line and its error text), and the
20+
refusal of a suspension from a type that declares `supportsPause: false`. An undeclared
21+
`resumeAuthority` resolves to `'service'` (fail-closed), so the generic resume route refuses those
22+
pauses; guessing `'any'` is how a raw resume once walked past an approval decision no service had
23+
recorded.
24+
- `@objectstack/runtime`: the endpoint step's `NOT_IMPLEMENTED` message and its two hints (the
25+
composed runtime always threads the policy context and the execution wiring, because execution is
26+
reachable only past the policy chain), and the endpoint mapping refusals (the publish gate rejects
27+
the same shapes, so a declaration that reaches the runtime check was stored without passing it).
28+
29+
Text only: no error code, field name, status or behaviour changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
'@objectstack/core': minor
3+
'@objectstack/objectql': patch
4+
'@objectstack/driver-memory': patch
5+
'@objectstack/service-analytics': patch
6+
---
7+
8+
fix: `sum` / `avg` answer the same double on every face the platform owns, added with one compensated fold that `@objectstack/core` now exports as `compensatedSum` (#20544)
9+
10+
Clause-②: yes
11+
12+
**New export.** `@objectstack/core` exports `compensatedSum(nums)`: the sum of
13+
`nums`, added in order with Kahan-Babuska-Neumaier compensation, which is the
14+
summation SQLite (3.43 and later) uses for its own `sum` and `avg`. It moved
15+
here from `@objectstack/objectql`'s rows path (`in-memory-aggregation.ts`),
16+
which now imports it instead of keeping a private copy.
17+
18+
**What changed.** Three folds still added a group's values naively, and now call
19+
the same function:
20+
21+
- `@objectstack/driver-memory`'s `aggregate()` and `find()` with aggregations,
22+
the path `engine.aggregate` takes on an in-memory datasource;
23+
- `@objectstack/driver-memory`'s analytics face (`MemoryAnalyticsService`),
24+
whose `sum` / `avg` measures are now a `$group` `$accumulator` in place of
25+
mingo's `$sum` / `$avg`;
26+
- `@objectstack/service-analytics`' draft preview.
27+
28+
Over a `number` column holding `0.1`, `0.2` and `0.3`, each of them answered
29+
`0.6000000000000001` / `0.20000000000000004`. They now answer `0.6` /
30+
`0.19999999999999998`, as SQLite and the engine's rows path do. Over
31+
`1e16, 1, -1e16` they answered `0` and now answer `1`. On driver-memory,
32+
`engine.aggregate` gave two answers depending on its path: `having { s: { $eq:
33+
0.6 } }` kept the group on the rows path and dropped it on the native path. It
34+
now keeps it on both.
35+
36+
**What did not move.** Two addends, integers whose running total stays within
37+
2^53, and a non-finite total give the same answer as before. Which values count
38+
as addends did not change either: booleans as 1 / 0, and nulls and non-numeric
39+
strings left out, as each face already had it. `count`, `min` and `max` are
40+
untouched. The analytics face's pipeline dump (`result.sql`) now renders the
41+
accumulator's functions by name, so a `sum` measure and an `avg` measure still
42+
dump differently.
43+
44+
**Residual.** PostgreSQL and MySQL add their doubles natively without
45+
compensation, and the platform does not wrap that arithmetic. So over three or
46+
more fractions their native path can still differ from these faces in the last
47+
place. An exact `$eq` on a fractional sum compares doubles; compare with a range.

0 commit comments

Comments
 (0)