Skip to content

Commit d472aaf

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21320-agent-lifecycle-retired
2 parents d324044 + 53fd35e commit d472aaf

47 files changed

Lines changed: 3541 additions & 186 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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The `kernel/cli-extension` module documentation no longer tells plugin authors to run `os plugins install`. Step 2, "Discover", said the plugin is listed in `@objectstack/cli`'s `oclif.plugins` array, or that users install it with `os plugins install`. Neither is true: `@objectstack/cli` declares no `oclif.plugins` and ships no plugin manager, so `os plugins` is not a command. The step now says what loads a plugin: oclif loads a plugin that the CLI's own `package.json` lists in both `oclif.plugins` and `dependencies`. To add a plugin's commands to `os`, build an `os` distribution whose own `package.json` lists the plugin in both places. The generated reference page carries the same text.
6+
7+
Clause-②: no
8+
9+
Documentation only. No schema, export or type changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol): a view container saved for an object another package ships no longer replaces that package's views or its default
6+
7+
Clause-②: no
8+
9+
- **What was wrong.** A runtime view container expands each member to `<object>.<key>`. A `list` that names no key becomes `<object>.default`, a `form` becomes `<object>.form`, and every member that names a key uses that key. Saved under another name, in another package or in none, for an object a code package ships, those expansions replaced that package's views of the same names on `GET /api/v1/meta/view?object=<object>`. The replacements were still stamped with the shipping package's `_packageId` and `_provenance: 'package'`.
10+
- On an environment-scoped kernel, the by-name read `GET /api/v1/meta/view/<name>` kept the packaged view, so the two reads disagreed.
11+
- On an unscoped kernel, the by-name read served the replacement too, for a container saved into a package or environment-wide.
12+
- The container's own default kept `isDefault: true`. It either replaced the object's default view or stood beside it as a second list default.
13+
- **What it does now.** For an object a code package ships, a container that belongs to another package, or to none, expands every member under its own name:
14+
- a `list` that names no key becomes `<object>.<container name>`;
15+
- every other member becomes `<object>.<container name>.<key>`. That covers a `list` that names its key, each `listViews` and `formViews` entry, and `form`.
16+
17+
None of these views carries `isDefault`. Every name the shipping package serves answers its packaged view on both reads, unchanged, and the only `isDefault` views the object lists are the shipping package's.
18+
- **One exception.** When the shipping package itself serves `<object>.<container name>` (a container named after one of that package's keys), the container's default list becomes `<object>.<container name>.<container name>` instead.
19+
- **A container with no name of its own** expands nothing on such an object.
20+
- **What these views carry.** The container's own package as `_packageId` (none for a package-less container), and no other package's `_provenance` or protection envelope.
21+
- **What stays.** Three kinds of container expand exactly as before, `isDefault` included:
22+
- a container bound to the package that ships the object;
23+
- a package-less overlay of that package's own container, saved under that container's name;
24+
- a container on an object no code package ships.
25+
26+
A write to `<object>.default` by its own name still overrides it on both reads.
27+
- **What changes for a caller.** Such a container's views are now served under new names:
28+
- its default list as `<object>.<container name>`, instead of `<object>.default`;
29+
- each keyed member as `<object>.<container name>.<key>`, instead of `<object>.<key>`.
30+
31+
A navigation `viewName` or a form-action `target` that used an old name to reach one of these views now reaches the shipping package's view. Use the new name instead.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
fix(service-analytics): the ObjectQL strategy's echoed `sql` renders an offset with no limit in the dialect's own spelling, so SQLite runs the statement it prints
6+
7+
Clause-②: no
8+
9+
**Before**, the ObjectQL strategy wrote its own row window into the statement it echoes: `LIMIT n` when a limit was set, then `OFFSET n` when an offset was. An `offset` with no `limit` therefore echoed a bare `OFFSET`, which SQLite's grammar does not have. Measured through `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` on SQLite, for a composition served by the engine aggregate, with `order: { note: 'asc' }` and `offset: 1`: the rows were right (every group after the first), but the echoed `sql` and the `/analytics/sql` body both ended `ORDER BY "note" ASC OFFSET 1`, and SQLite refuses that statement with `near "OFFSET": syntax error`.
10+
11+
**Now** the statement ends with the same window clause the native-SQL strategy runs, for the dialect of the driver that serves the object: `LIMIT -1 OFFSET 1` on SQLite, which runs and answers the same rows. One function renders the window for both strategies.
12+
13+
**Unchanged.** The rows either strategy answers. A window with a `limit` keeps its bytes (`LIMIT 2 OFFSET 1`) on every dialect, and on PostgreSQL an offset with no limit still echoes `OFFSET 1` alone. A host that wires no `sqlDialect` hook gets the native strategy's dialect-neutral spelling, `LIMIT 9223372036854775807 OFFSET 1`. A date-bucketed dimension still echoes as `date_trunc(…)`, which SQLite does not run; this change touches only the window.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/cli': minor
3+
'@objectstack/runtime': minor
4+
---
5+
6+
The CLI's one-shot commands no longer write to the database as a side effect of booting. No `os migrate *`, `os meta resync`, `os secret orphans` or `os storage orphans` run loads the app's inline seed data, apply and delete modes included, and every mode that writes nothing now boots read-only.
7+
8+
Clause-②: yes (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) a CLI command's verdict on one edge, not a declaration: a no-write mode of os migrate value-shapes, os migrate recorded-by, os migrate resume, os secret orphans or os storage orphans pointed at a database that lacks a table it reads now exits 1 instead of creating the table and reporting nothing. No authorable key, spelling, export or stored shape moves: every stack parses and loads exactly as before, the write modes write exactly what they wrote before minus the seed loader's rows, and no stored row is read differently or rewritten. What an operator does about the refusal is point --database-url at the deployment's database or boot the deployment once, so there is no rewrite a ledger entry could carry. The other categories are closed on facts: both packages publish (not unpublished); no ADR-0087 id covers a command's verdict, and this diff adds none (not registered / already-registered); and the change is CLI behaviour plus one new optional runtime config key, not a TypeScript declaration change to an existing surface (not runtime-interface-only / type-surface-only). -->
11+
12+
**BREAKING** — a no-write run of `os migrate value-shapes`, `os migrate recorded-by`, `os migrate resume`, `os secret orphans` or `os storage orphans` at a database that lacks a table it reads now exits 1, where it used to exit 0. It ships as `minor` under the launch-window convention for accept-set narrowings.
13+
14+
**What was wrong.** Eight commands booted the full data stack in a mode their documentation says writes nothing: `os migrate value-shapes` (scan), `summary-nulls`, `files-to-references` and `recorded-by` (dry run), `os migrate resume` (list), `os secret orphans` and `os storage orphans` (report), and `os meta resync` without `--yes`. That boot ran schema sync and the app's inline seed loader. The seed loader upserts every seeded row, so each run bumped `updated_at`, stamped `organization_id` on seeded rows that had none, and put an operator's edit to a seeded row back to the seed's value. On `examples/app-crm` that was all 28 seeded rows on every run. On a database behind the app's schema, the boot also added columns and created tables. The apply and delete modes ran the same seed loader alongside the write the operator confirmed.
15+
16+
**What changes for an operator.**
17+
18+
- Every mode that writes nothing boots the way `os migrate plan` does: the schema sync is held back, no seed rows are written, and a SQLite file that does not exist is not created. The database is left byte-identical, and the report is the same as before.
19+
- No one-shot CLI boot loads the app's inline seed data. `--apply`, `--delete`, `os migrate resume --run` and `os meta resync --yes` write what they report and nothing else. Seeding stays with `os dev` and `os serve`.
20+
- The deferred schema sync now covers every SQL datasource the boot connects, not only the default one. `os migrate plan` lists a second datasource's pending tables, and `os migrate apply` creates them after you confirm.
21+
- One edge changes: a no-write run pointed at a database that lacks a table it reads (a SQLite file that does not exist, a database that was never booted, or the wrong `--database-url`) refuses and exits 1 instead of creating the table and reporting nothing. Point `--database-url` at the deployment's database, or boot the deployment once first. `os secret orphans --json` answers that refusal with `"error": "scan_failed"`.
22+
- `os migrate value-shapes --json` prints one JSON document when the scan fails its gate. It used to print a second one, `{"error":"EEXIT: 1"}`.
23+
24+
**For embedders of `@objectstack/runtime`.** `createStandaloneStack` accepts `armLifecycleSweep` (default `true`). With `false`, the ADR-0057 lifecycle sweep (rotation, retention reaping, archiving and the dangling-reference audit that rides its clock) is never armed on that boot, and an explicit `sweep()` call on it returns an empty report. The CLI passes `false` on every one-shot boot.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: the analytics row wildcard `'*'` is admitted only where a `count` consumes it — a cube or dataset measure over `'*'` under any other aggregate, and a cube dimension over `'*'`, are refused at parse (#21409)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING** — shipped as `minor` under the launch-window convention
10+
(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by
11+
this banner, the `(narrowing)` arm above and the ADR-0087 disposition below,
12+
never by the level).
13+
14+
`'*'` is the row wildcard: what a `count` aggregates (`COUNT(*)`), reading no
15+
field value. It is now admitted in exactly one place, a measure that counts:
16+
17+
- `MetricSchema.sql` — a cube measure's `sql` — admits `'*'` under
18+
`type: 'count'` only; under any other `type` it is refused at `sql`
19+
(code `custom`).
20+
- `DatasetMeasureSchema.field` — an ADR-0021 dataset measure's `field` — admits
21+
`'*'` under `aggregate: 'count'` only; under any other aggregate, or on a
22+
measure with no aggregate (a `derived` one), it is refused at `field`
23+
(code `custom`). A count may still omit `field`.
24+
- `DimensionSchema.sql` — a cube dimension's `sql` — never admits `'*'`
25+
(code `invalid_format`): it takes the column path without the wildcard arm,
26+
the pattern a dataset dimension's `field` already takes.
27+
28+
Each refusal names the slot and the aggregate the author wrote, and prescribes
29+
the two ways out: a `count`, or a column. A column or a relationship path parses
30+
byte-identically to before on every slot, and so does a `count` over `'*'`.
31+
32+
Why: no aggregate but `count` has a column to read over `'*'`, and a dimension
33+
has no aggregate at all, yet the contract admitted the wildcard on any measure
34+
and on a cube dimension, and the analytics strategies sent it to the database as
35+
written. Measured at `POST /api/v1/analytics/dataset/query` over a real SQLite
36+
driver, on the native-SQL and the ObjectQL strategy alike: a dataset measure
37+
aggregating `'*'` under `sum`, `avg`, `min`, `max` or `count_distinct` answered
38+
`500 DATABASE_ERROR`. A dataset measure compiles to the cube measure it names
39+
verbatim, so the same reading covers an authored cube measure. Such a member
40+
never produced an answer, so no working document changes meaning: the failure
41+
moves from the query to the authoring parse. The two measure slots ask ONE
42+
shared predicate; the rule is cross-field (the slot and its aggregate), so it is
43+
a refinement, which the published JSON Schema cannot carry — both sites are
44+
declared in `dropped-refinements.baseline.json`. The dimension half is a
45+
`pattern`, so `json-schema/**` states it.
46+
47+
## FROM → TO
48+
49+
```
50+
FROM { name: 'deal_metrics', label: 'Deal Metrics', object: 'deal',
51+
dimensions: [{ name: 'stage', field: 'stage' }],
52+
measures: [{ name: 'deals', aggregate: 'sum', field: '*' }] }
53+
-> DatasetSchema.parse accepted it; a dataset query selecting `deals`
54+
answered 500 DATABASE_ERROR
55+
TO -> DatasetSchema.parse throws a ZodError at measures.0.field (custom):
56+
`measures[].field` is the row wildcard `'*'` under `aggregate: 'sum'`. …
57+
defineStack({ datasets }) refuses it at datasets.N.measures.0.field (422
58+
STACK_SCHEMA_INVALID), and POST /api/v1/analytics/dataset/query answers
59+
400 VALIDATION_FAILED for an inline or a saved copy
60+
61+
measures: [{ name: 'deals', aggregate: 'count' }] // a row count
62+
measures: [{ name: 'deal_value', aggregate: 'sum', field: 'amount' }] // an aggregate of a column
63+
64+
FROM defineCube({ name: 'deals', sql: 'deal',
65+
measures: { total: { label: 'Total', type: 'sum', sql: '*' } },
66+
dimensions: { everything: { label: 'All', type: 'string', sql: '*' } } })
67+
TO -> refused at measures.total.sql (custom) and dimensions.everything.sql (invalid_format)
68+
69+
measures: { total: { label: 'Total', type: 'sum', sql: 'amount' } },
70+
dimensions: { stage: { label: 'Stage', type: 'string', sql: 'stage' } }
71+
```
72+
73+
**The one-line fix:** parse each cube and dataset; every refusal at `…sql` /
74+
`…field` naming `'*'` is one member to change — declare a `count` to count rows,
75+
or name the column the measure aggregates (a dimension names the column it
76+
groups by). On a `derived` dataset measure, delete `field`: nothing read it.
77+
There is no mechanical rewrite: `os migrate meta` rewrites nothing for it, and
78+
lists the entry `analytics-row-wildcard-outside-count-refused` as a manual
79+
change that requires your judgment.
80+
81+
**What a stored document meets.** A metadata read still serves it as stored,
82+
with the refusal on its read diagnostics (`_diagnostics`), and a re-save through
83+
the metadata write door is refused at the slot. `POST
84+
/api/v1/analytics/dataset/query` parses every dataset it is handed, inline or
85+
saved, so a stored dataset carrying such a measure answers `400
86+
VALIDATION_FAILED` at `measures.N.field` on every query — including a query that
87+
selects only its other measures, which used to answer — until the member is
88+
fixed: it fails closed. An authored cube reaches the analytics runtime through
89+
the stack definition, whose parse refuses it.
90+
91+
## The kit
92+
93+
- **Schema.** `data/analytics-column-reference.ts` (not published API) declares
94+
the predicate `rowWildcardOutsideCount` and its refusal once; `MetricSchema`
95+
and `DatasetMeasureSchema` call both from a refinement, and
96+
`DimensionSchema.sql` takes `ANALYTICS_COLUMN_PATH`. No export, key or enum
97+
member changes, so the api-surface, authorable-surface and JSON-schema
98+
manifest ratchets are unchanged.
99+
- **ADR-0087.** D3 entry `analytics-row-wildcard-outside-count-refused`. No D2
100+
conversion: rewriting to `count` would change the figure the author asked for,
101+
and only the author can name the column. No `RETIRED_KEYS_BY_MAJOR` row.
102+
- **Dropped refinements.** `data/Metric` and `ui/DatasetMeasure` gain their root
103+
site, and every published schema embedding them gains the embedded site.
104+
- **Liveness.** `analytics_cube` `measures.sql` / `dimensions.sql` and `dataset`
105+
`measures.field` stay `live`, re-verified, their notes re-pointed here.
106+
- **Docs.** The `ui/dataset` reference page is regenerated.
107+
- **Runtime.** Unchanged.
108+
109+
## Reach, measured
110+
111+
- This repository: no example, platform object, doc, skill, script or test
112+
fixture authors `'*'` outside a `count` at the three slots (`git grep` of every
113+
`field` / `sql` value spelled `'*'`, 173 hits, each read in its enclosing
114+
object: 154 under a `count`, the rest QueryAST aggregations, comments and
115+
strategy-level literals). One spec pin admitted `'*'` on a cube dimension; it
116+
now pins the refusal.
117+
- objectui at the pinned `.objectui-sha`: zero `field` / `sql` values spelled
118+
`'*'` (lit controls: 51 `aggregate: 'sum'`, 438 `field: 'amount'`).
119+
- Out-of-repo authored metadata: NOT MEASURED.
120+
121+
<!-- adr-0087: registered analytics-row-wildcard-outside-count-refused -->

0 commit comments

Comments
 (0)