Skip to content

Commit 93c2e68

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21081-activity-field-values
2 parents 6acde3f + 670680e commit 93c2e68

64 files changed

Lines changed: 3818 additions & 273 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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the shipped `src/migrations/entries/README.md` "Reproduce any row" recipe now fetches both commits into the driver-less bare clone before `merge-tree`, and reads exit 1 with no tree id as a missing object, never a conflict (the old `--shared --no-local` form misread a clean pair as conflicted) (#20970)
6+
7+
Clause-②: no
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@objectstack/rest': patch
4+
'@objectstack/runtime': patch
5+
---
6+
7+
fix(rest,runtime): the published-snapshot read of a flow name a managed package ships answers the package's flow, as the layered read does (#21002)
8+
9+
Clause-②: yes (widening)
10+
11+
`flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. Since the previous half of #21002, the layered read, `GET /api/v1/meta/flow/:name/layers`, reports the package's flow as the effective layer for a name a managed package ships, and a stored flow of that name as a separate layer that does not take effect. The published-snapshot read, `GET /api/v1/meta/:type/:name/published`, and its runtime-dispatcher twin read that same layered answer, but served its stored layer whenever one was present. So for such a name they still answered `200` with the stored flow, not the package's.
12+
13+
Both published-snapshot doors now serve the layered read's effective layer when that read put the package's flow over a stored flow, which is the package's flow. They ask the metadata protocol's own check for that decision rather than repeating it. In every other case they answer exactly as before: a flow name no managed package ships, and every other metadata type, `object` included, still answer the stored layer when one is present, and an item with no stored layer still falls through to the code/package snapshot. The stored flow is not deleted, rewritten or refused.
14+
15+
**The widening.** `@objectstack/metadata-protocol` makes one existing method public: `ObjectStackProtocolImplementation.isShippedFlowName(type, name)`. It answers whether `name` is a flow name a managed package ships. It was private to the class, so a door in another package could not ask it any other way. Its answer is unchanged, and the layered read, the by-name read and the flow list keep calling it.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/objectql": minor
3+
"@objectstack/core": minor
4+
"@objectstack/driver-sql": patch
5+
---
6+
7+
fix(objectql)!: a per-aggregation `filter` refuses `$in` / `$nin` / `$eq` / `$ne` / an ordering / `$between` / implicit equality on a declared JSON-stored field with `INVALID_FILTER` / 400, in the words `where` refuses them in, instead of counting rows the stored arrays cannot support
8+
9+
Clause-②: yes (widening)
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of a QUERY shape at the engine's per-aggregation filter position: the operator x declared-type pairs refused are exactly the pairs driver-sql's where has refused on a JSON-stored column since its column-type gate landed, and the per-aggregation position now answers them the same way. No authorable key, spelling or stored metadata shape moves: FilterConditionSchema, AggregationNodeSchema and every object and dataset definition parse and save as before, and nothing reads or rewrites a stored row. There is nothing for objectstack migrate meta to rewrite, since what changes is which query the engine answers, not what any metadata says; the refusal itself names the spelling to use. The other categories are closed on facts: every bumped package publishes (not unpublished); no ADR-0087 id covers a filter operator on a JSON-stored column and this diff adds none (not registered / already-registered); and the change is runtime behaviour plus ADDITIONS only (three new @objectstack/core exports and one new optional trailing parameter on applyInMemoryAggregation), with no published interface or type narrowed or removed (not runtime-interface-only / type-surface-only). -->
12+
13+
**BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts in one position, `aggregations[i].filter`, on every driver and for every caller that reaches the engine: the REST query door (`POST /api/v1/data/:object/query`), a flow or hook, and the analytics strategy that lowers a dataset measure's filter onto `engine.aggregate`. The published `applyInMemoryAggregation(rows, ast, timezone, fields)` narrows the same way when it is handed a field map. It ships as `minor` under the launch-window convention for accept-set narrowings.
14+
15+
**What is refused.** On a field the object declares JSON-stored (a structured-JSON type such as `json` or `address`, an inherently multi-value option type such as `tags`, `multiselect` or `checkboxes`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`), a per-aggregation `filter` that compares the field with `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$in`, `$nin` or implicit equality (`{ "owners": "u1" }`) is refused with `INVALID_FILTER` / 400, whatever the comparand (`null` and an empty list included), at any depth under `$and` / `$or` / `$not`, and before any driver is asked for a row, so an empty table refuses it too. That is the set `driver-sql`'s `where` refuses on such a column, for the same reason.
16+
17+
**What an author sees now.** The same 400 body the same filter gets as a `where`: the filter WAS NOT APPLIED, the comparison can never equal one member of a stored list, and the spelling to use, `{ "FIELD": { "$contains": "a" } }` for membership, or an `$or` of `$contains` for any-of. The field and the operator are withheld from the message, as they are for `where`, and the full diagnostic, naming both and the aggregation position, goes to the server log.
18+
19+
**Why a refusal.** The engine evaluates a per-aggregation filter itself, and it compared the whole stored array against a scalar. Measured through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16 over six rows of a `multiple: true` lookup, two of them holding `u1`: `{ owners: { $in: ['u1', 'u9'] } }` counted 0, `{ owners: { $nin: ['u1', 'u9'] } }` counted all 6, the two rows it was asked to exclude among them, `$gt` / `$lte` / `$between` counted 4 / 1 / 5, and `{ tags: { $eq: 'red' } }` counted the row holding `['red']` by JS loose equality. The same filters in `where` were 400 on both dialects.
20+
21+
**Who is affected.** A dashboard, report, dataset measure or caller whose per-aggregation filter compares a JSON-stored field with one of those operators and read the count as a real answer. Also a host calling `applyInMemoryAggregation` directly with a `fields` map: it now judges each `aggregations[i].filter` against that map before any row (an empty `rows` array included) and throws the same `INVALID_FILTER` / 400. It takes an optional fifth argument, `reportWithheld(diagnostic)`, which receives the withheld field, operator and position; without it the diagnostic is dropped. A call without `fields` judges nothing, as before. Write `$contains` for "holds this member", an `$or` of `$contains` for "holds any of these", and `$not` around either for the exclusion.
22+
23+
**Unchanged.** `$contains` and `$notContains` (membership on such a field), `$exists`, `$null` and `$empty`; every operator on a field that is not JSON-stored; `having`; `where`; and a host whose engine has no declaration for the object, where nothing is judged.
24+
25+
**`@objectstack/core`** (three new root exports): `JSON_COLUMN_INCOMPATIBLE_OPERATORS`, `jsonColumnOperatorRefusalText(field, op, bare)` and its return type `JsonColumnOperatorRefusalText` (`{ message, diagnostic }`). They are the operator set and the two texts (the withheld message and the full diagnostic) of the JSON-column refusal, so `driver-sql`'s `where` and the engine's per-aggregation filter refuse with one set and one sentence.
26+
27+
**`@objectstack/driver-sql`**: no behaviour change. Its JSON-column gate reads the set and the text from `@objectstack/core`; every refusal it prints is byte for byte what it printed before.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
---
4+
5+
fix(metadata-protocol)!: stored metadata bodies read through the generic data door are served as their type's read projection, so stored credentials are withheld there too; grouping those tables by the body column is refused (#21086)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of a grouping TARGET on the generic data door: a groupBy entry naming the stored body column of sys_metadata or sys_metadata_history. No authorable key, spelling, export or stored shape moves (the helpers are internal to the package; `@objectstack/metadata-protocol` exports nothing new and nothing less), and no stored row is read differently by any metadata consumer or rewritten. A whole stored body has no meaning as a group key that a ledger entry could rewrite to. The other categories are closed on facts: the package publishes (not `unpublished`); no ADR-0087 id covers a grouping target (not `registered` / `already-registered`); and the change is runtime behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
10+
11+
**BREAKING**: this narrows what the generic data door's query accepts as a grouping target on two system tables. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
12+
13+
**What changes.** A row of `sys_metadata` or `sys_metadata_history` read through `GET /api/v1/data/:object`, `POST /api/v1/data/:object/query`, `GET /api/v1/data/:object/:id` (and anything that reads through the same `findData` / `getData`, such as the export route) now carries its `metadata` column as the body's type's read projection: the same object every `/meta` read exit serves, chosen through the same `@objectstack/spec/kernel` redactor registry. For a `datasource` body that means the stored credential material the datasource doors already withhold is withheld here too, decided by the same redactor. A body with nothing to withhold, and every body of a type that registers no redactor, is served as the stored bytes.
14+
15+
- A projection that names `metadata` without `type` (`?select=metadata`) still works: the door reads `type` to choose the redactor and does not serve it.
16+
- A body the door cannot judge is omitted rather than served: one whose row carries no `type`, and one that does not parse while its type registers a redactor.
17+
18+
**What an author sees now on a grouping.** `400 INVALID_FIELD` for a `groupBy` entry naming `metadata` on either table, located at the entry (`groupBy[0]`, or `groupBy[0].field` for the object form), saying the query was not run and naming the route: group by `type`, `name` or another scalar column, and read the bodies with a plain list. A group key stands for every row that shares it, and the redactor is chosen per row, so the key cannot be projected without changing which rows it counts.
19+
20+
**Unchanged.** Every other object, including one with a column of its own named `metadata`; every other grouping on these tables; and every internal reader of `sys_metadata`, which reads through the engine rather than through this door and keeps reading the stored body.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/rest': patch
3+
'@objectstack/runtime': patch
4+
---
5+
6+
fix(rest,runtime): reading `datasource` and `external_catalog` metadata through `/api/v1/meta` requires `manage_platform_settings`, the capability each type's own door already requires (#21087)
7+
8+
Clause-②: no
9+
10+
- A `GET` or `HEAD` of `/api/v1/meta/datasource` or `/api/v1/meta/external_catalog` (and their plural spellings) is now admitted only for a caller who holds `manage_platform_settings`. That is the capability the datasource admin door (`GET /api/v1/datasources`, `GET /api/v1/datasources/:name`) and the federation read door (`GET /api/v1/datasources/:name/external/tables`) already require for the same data. Every read route under the type is judged alike: the list, the item read and each of its query switches, `/published`, `/layers`, `/history`, `/audit`, `/diff` and `/references`. `/history`, `/audit` and `/diff` still also require an authoring capability, as before.
11+
- A caller without the capability gets `403` with `error.code` `PERMISSION_DENIED`, and a message that names the capability. The answer is the same whether or not the named item exists, and nothing is read from the metadata store first.
12+
- Holders of `manage_platform_settings` are served exactly as before. Platform administrators hold it through `admin_full_access`. Every other metadata type, and every write route, is unchanged.
13+
- Both transports answer the same way: `RestServer`, and the runtime dispatcher's `/meta` domain that a host mounting only the `/api/v1/*` catch-all is served by.
14+
- If you read either type with a caller that holds only an authoring capability (`manage_metadata`, `studio.access` or `setup.access`), grant `manage_platform_settings` to that caller, or read through a caller that already has it.

‎.claude/skills/pm-dispatch/references/platform-readings.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
- 它跑 `git merge` 的 merge-ort ⇒ 注册 `merge=os-regen` 的克隆照用驱动,未注册的退回文本合并。
3333
- 注册按克隆(`pnpm install` 的 prepare),服务端一个驱动都不跑 ⇒ 两侧答的不是同一个问题。
3434
- 驱动接手的路径上 exit 0 只说内容判断被推迟,⛔ 不是无冲突:它对内容什么都没说。
35-
- ⇒ 对照复现的是被测条件不只是命令:冲突证明从无驱动裸克隆 `clone --bare --shared` 探。
35+
- ⇒ 冲突证明探无驱动裸克隆,配方与读法见 `scripts/pm/os-regen-merge.sh`:exit 1 无树 id = 缺对象
3636
- ⛔ 永不用 `-c merge.os-regen.driver=` 覆盖:驱动不是被关掉而是跑失败,路由路径全报冲突。
3737
- 入队决策点才整对象 `get` 一次;挂了 flip 定点到点读,⛔ 不又查又等。
3838
- 状态核验用最小字段(search/list 加 `fields`)或等事件。

‎.github/workflows/fleet-write.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ jobs:
104104
# The action revokes the token in its post step.
105105
- name: Mint the fleet App token, narrowed to the target and the op table
106106
id: token
107-
uses: actions/create-github-app-token@v2
107+
uses: actions/create-github-app-token@v3
108108
with:
109109
app-id: ${{ vars.OS_FLEET_APP_ID }}
110110
private-key: ${{ secrets.OS_FLEET_PRIVATE_KEY }}

‎docs/qa/platform-checklist/FOLLOW-UPS.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,14 @@ declares exactly `.` and `./logger`. Do not re-derive the subpath.
280280
| D14 | `MigrationRecoveryPlugin` is composed by NO boot path — `serve.ts` auto-registers `PlatformObjectsPlugin` but never the recovery plugin; standalone-stack, default-host, the showcase config, and the migrate CLI boot all omit it; only its unit test instantiates it. Interrupted-migration detection therefore never runs on any shipped boot, while `sys-migration-journal.object.ts` argues recovery must need "zero host wiring" | `packages/runtime/src/index.ts` (exported); `serve.ts` (what IS auto-registered) | platform-core.interrupted-migration-boot-report (fixtures an explicit registration; knownGap names the composition hole) | correctness/composition — safe to file |
281281
| D15 | `extract-hook-body.ts`'s header promises "the build fails… no silent fallback" on a forbidden pattern, but the DEFAULT `os build` catches every extraction error and silently falls back to the.mjs bundle (`lower-callables.ts`), printing the warnings nowhere; only `--strict-body` (`compile.ts`) produces the worded refusals with exit 1. `hook-bodies.mdx` documents the warn-and-bundle default, so code comment and docs disagree with each other | `extract-hook-body.ts` vs `lower-callables.ts`, `compile.ts` | cli.hook-body-extraction-gates (default-path silent-fallback encoded as expected-fail contradiction clause) | correctness — safe to file |
282282

283+
⚠️ **Handle collision on D11 (#21060).** The D11 row above is the two-factor `get-totp-uri`
284+
re-reveal. It is **not** the private handle D11 that `access-security.rls-both-sides`
285+
clause 5 and `access-security.owd-sharing-matrix` clause 4 cite. That handle names a
286+
security-sensitive defect delivered to the maintainer privately (#7463), and it is
287+
deliberately not reproduced in this file. The two numbers collided when this table continued
288+
the D-series. Cite the item and its clause, never the bare handle, and never read one D11 as
289+
the other.
290+
283291
Two design notes captured inside items rather than as defect rows: `sys_user.mfa_required_at`
284292
is stamped lazily and never cleared anywhere in source, so post-disable re-gating branches on
285293
a pre-existing stamp (identity-auth.two-factor-disable-lifecycle, design-note clause); and

0 commit comments

Comments
 (0)