Skip to content

Commit b346089

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-5322-empty-combinator-identity
# Conflicts: # packages/services/service-analytics/src/strategies/filter-normalizer.ts
2 parents 0e470ac + 71f205d commit b346089

125 files changed

Lines changed: 9849 additions & 714 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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
docs(adr-0076): 把状态行从 `Proposed` 校准为 `Accepted`,并逐条记录 D1–D12 的真实落地状态(#5063)。参照 #4919 对 ADR-0078 的做法:不整体翻牌,每条决定给出在 `origin/main` 上核对到的证据 —— D3 完全未建、D10/D11 只落一半、D9 已越过 step-1 把联合类型彻底解散、D7 已有 CI 触发指标。同时给 D9 的 back-compat 条目补一条状态注记(组合别名已不存在)。纯文档改动,不发布任何包。
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
"@objectstack/types": minor
3+
"@objectstack/cloud-connection": minor
4+
"@objectstack/lint": minor
5+
"@objectstack/cli": minor
6+
"@objectstack/spec": patch
7+
"@objectstack/driver-sql": patch
8+
---
9+
10+
feat(types,cloud-connection,lint,cli): ADR-0120 17.x 收尾 —— `isolated` 安装期姿态硬门(D5e)、D5c 重拼写 advisory、成文契约扫荡与三姿态 conformance (#5081)
11+
12+
ADR-0120 17.x 波的第三块,也是最后一块。前两块已在 main 上:#5212(driver 侧
13+
D3+D4 —— `COALESCE(organization_id, '__global__')` 物化、drift 两侧同步、重复预检)
14+
#5208(spec 词汇 `'organization'` + D5a/D5b lint)。本次补齐三件事:安装期的
15+
姿态决策点、剩余的成文契约、以及把「一个 app 包跑遍三种姿态」从假设变成测试。
16+
17+
**D5e —— 装进 `isolated` 环境时的硬门。** 词汇本身是姿态无关的:作者说的是业务
18+
边界(`'organization'` 一个组织一份 / `'global'` 整个安装一份),没有任何索引形状
19+
读姿态。唯一的残留在一个方向上:`isolated` 下组织就是**不同客户**,此时 app 业务
20+
对象上的 `'global'` 唯一既跨客户过度约束,又变成跨客户的存在性预言机(S10/S14)。
21+
维护者裁定这是**硬门而非 advisory**:把带 `'global'` 唯一(非 `sys` 对象)的 app
22+
装进 `isolated` 环境会**停下来并逐索引列出**,安装者(通常是 AI agent)要么确认它
23+
确实是平台级的,要么改写为 `'organization'`;确认按 ADR-0104 attestation 风格
24+
留痕在安装清单里(`InstalledManifestEntry.globalUniqueAttestation` —— 确认了什么、
25+
谁确认的、何时、在哪个姿态下问的),**之后不复问**
26+
27+
- 停下的安装**什么都不留**:先于 hot-register 和任何 ledger 写入,所以作者改完
28+
元数据可以直接重试,不需要先卸载。
29+
- 逐索引确认是有牙齿的:`confirmGlobalUniques``true` 或明确的 id 数组,只确认
30+
其中一条仍会在剩下的那条上停住。
31+
- 升级引入的****约束会被问,老的答案继续算数。
32+
- 另一个姿态下给出的确认**不算同意** —— `isolated` 那个问题在 `single` 下从未被
33+
问过,所以按「未确认」处理(唯一不会静默放行跨客户约束的方向)。
34+
-**永不做成启动期告警**(#4884 纪律)。boot 时的 rehydrate 不评估此门;门够不到
35+
的两类存量 —— 门禁上线前的安装、装后姿态变更的环境 —— 由 `os doctor`
36+
`os migrate plan` 的 advisory 形态覆盖。
37+
38+
判定里有三条是承重的,别「简化」掉:声明索引上的裸 `unique: true` ****(D1 说它
39+
就是 `'global'` 的位置式拼写,排除它等于让整个 17.x 可以靠拼写绕过);字段级
40+
`true` **不算**(它是 `'organization'`,永久合法);`sys_`/`base_` 对象**不算**
41+
(S5 那批引擎幂等键天然就是平台级的,每次安装都问一遍就是 #4884 的误报类)。
42+
43+
CLI: `os package install` 新增 `--confirm-global-uniques`,并把 409 渲染成可读的
44+
逐条清单而不是一句 "Install failed (409)"。
45+
46+
**D5c —— 遗留手写组织复合索引的 advisory。** 新规则
47+
`unique/legacy-organization-composite`:声明的唯一索引自己列出了组织列
48+
(`{ fields: ['name','organization_id'], unique: true }`)—— 这是词汇出现之前手写
49+
per-organization 的写法。它读起来像「每组织唯一」,物化出来却是普通复合索引,而
50+
SQL UNIQUE 是 NULL-distinct 的:组织列为 NULL 的行上它**什么都不约束**(#5030),
51+
在单组织部署上那就是每一行。改写成 `unique: 'organization'`(`fields` 原样保留,
52+
driver 会把已列出的组织列**就地**变成 NULL-safe 形式)正是补上这个洞的动作。
53+
**永远只是 advisory,永远不自动修**:老拼写永久合法、零强制 drift,而 opt-in 是
54+
真实的物理收紧,要走 D4 的 `recreate_index` + 重复预检。
55+
56+
**D6 —— 成文契约扫荡。** `content/docs/data-modeling/indexing.mdx`
57+
§Two ways to say "unique" 全节按新词汇重写(含 `os:check` 代码块);
58+
`content/docs/protocol/objectql/schema.mdx` 的 §Uniqueness and tenancy 重写为
59+
§Uniqueness and scope —— 其中那句「单租户部署不受影响,租户列是常量,复合索引
60+
退化为单列索引」是 #5030 **证伪过的原话**,现已替换为 D3 的 NULL-safe 事实;
61+
`content/docs/deployment/cli.mdx``replace_unique_index` / `recreate_index`
62+
条目补上 NULL-safe 形状与重复预检;`content/docs/references/**`
63+
`gen:schema && gen:docs` 再生成,未手改。
64+
65+
按 ADR-0120 Resolved #2 的非规范性引导(官方示例/脚手架/生成器在新代码中输出
66+
显式拼写),`skills/objectstack-data/**` 的索引与校验规则整体扫过:声明索引一律
67+
说清 scope,并新增一节完整讲 `'organization'` 的 NULL-safe 语义与「永远不写姿态」。
68+
顺带修掉那里长期使用的 `tenant_id` —— 平台的列叫 `organization_id`
69+
`examples/**``create-objectstack` 模板与 `os generate` 经核查**根本没有声明任何
70+
唯一约束**,故无可扫;这是核查结论,不是遗漏。
71+
72+
**三姿态 conformance(ADR §Acceptance tests)。** 同一个 fixture app 在
73+
`single | group | isolated` 三姿态下启动,逐 S 行用**真实的违规插入**断言 enforcement
74+
(S1/S2/S3/S4/S5/S6/S7/S8/S9/S11/S12),并逐姿态捕获物化出的索引键,断言三者
75+
**逐字节相同** —— 「没有任何索引形状读姿态」这句话一旦有两者不同就是假的。相同性
76+
断言配了一条正向断言(对着期望的键形状),这样「三次都什么都没建」不会读成「一致」。
77+
外加 ADR 只要的那一条 transition smoke:在 `single` 下建库、`isolated` 下重新打开,
78+
drift op 为零。
79+
80+
对既有部署的影响:除新增的安装期确认外,本次不改变任何已有物化行为。字段级
81+
`unique: true` 一如既往合法。
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
"@objectstack/rest": patch
4+
---
5+
6+
fix(analytics,rest): an analytics filter refusal reaches the caller as `400 INVALID_FILTER`, not `500 ANALYTICS_QUERY_FAILED` (#5352)
7+
8+
Misspell an operator in a dashboard widget's filter and analytics refuses it —
9+
correctly, and loudly, which is the posture #3948 / #5240 / #5325 / #5334 each
10+
argued for one refusal at a time: dropping a predicate the compiler cannot
11+
express does not narrow the query, it **widens** it to rows the author excluded,
12+
and a chart drawn over the whole dataset looks like a working chart.
13+
14+
The refusal never reached the author. It landed as `500 ANALYTICS_QUERY_FAILED`
15+
— read as "the platform is broken" rather than "your filter has a typo", and
16+
counted by ops alerting as a 5xx. The identical mistake on `find()` has answered
17+
`400 INVALID_FILTER` since #3948, so one authoring error had two wire shapes,
18+
chosen by which face happened to catch it.
19+
20+
**One defect, two halves — either alone leaves it unfixed.**
21+
22+
- **Producer** (`filter-normalizer.ts`): seven of its nine refusals were bare
23+
`throw new Error(…)` carrying no `code`/`status`. All nine now go through the
24+
`invalidFilterError` helper #5334 introduced (`INVALID_FILTER` / 400), which
25+
becomes the module's only way to refuse.
26+
- **Consumer** (`rest-server.ts`, `POST /analytics/dataset/query`): the catch
27+
discarded `error.code` / `error.status` and re-derived the classification from
28+
a hardcoded list of message substrings — so a producer that took ADR-0112
29+
seriously was punished for it. It now reads the envelope **first**; the
30+
substring list is demoted to a fallback for the families that still carry no
31+
envelope.
32+
33+
**Observable behaviour change — read this if you alert or retry on status.**
34+
The same request that returned `500 ANALYTICS_QUERY_FAILED` now returns
35+
`400 INVALID_FILTER` (and, for two neighbouring conditions whose producers
36+
already declared an envelope this route was discarding, `400 INVALID_FIELD` for
37+
a measure over a field the object does not have, `404 CUBE_NOT_FOUND` for an
38+
unregistered cube). Monitoring that counted these as server faults will see the
39+
5xx rate drop and a 4xx rate appear; a client that retries on 5xx will stop
40+
retrying a request that could only ever fail the same way. Both are the intended
41+
correction — the condition was always the caller's mistake — but they are
42+
visible, so they are stated rather than buried.
43+
44+
**Which inputs are refused did not change.** This changes the SHAPE of the
45+
error and nothing about the judgement that produced it: no refusal condition
46+
was touched, no input that used to compile now refuses, and no input that used
47+
to refuse now compiles. That claim is pinned input-by-input (refusals *and*
48+
accepted inputs with their compiled trees) in
49+
`filter-refusal-envelope.test.ts`, which is green both before and after the
50+
change — only the envelope assertions move.
51+
52+
The message-substring list survives on purpose. All six of its entries were
53+
re-verified as bare `Error`s (`dataset-compiler.ts`, `native-sql-strategy.ts`,
54+
`dataset-executor.ts`, `read-scope-sql.ts`), so deleting it would regress those
55+
families from `400 DATASET_INVALID` to 500. It is a placeholder for their
56+
enveloping, not a second classification mechanism, and it is now documented as
57+
such: a new refusal should carry a `code`/`status` and be served by the
58+
envelope branch for free. The passthrough is deliberately **4xx-only** and
59+
requires **both** `code` and `status`, so an internal fault can never be
60+
re-labelled as the caller's fault, and this route never invents a code a
61+
producer failed to supply.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
fix(service-analytics): a `where` written as a `FilterArray` is lowered instead of silently dropped (#5334)
6+
7+
**Observable behaviour change.** An analytics query whose `where` arrived as an
8+
ARRAY had its filter **deleted**: `normalizeAnalyticsFilterTree` answered every
9+
array with `return null`, so no predicate was compiled, no error was raised, and
10+
the widget charted the **entire dataset**. The compiled SQL stayed perfectly
11+
valid — just broader than the author asked for — which is why it was invisible
12+
to every test that asserts a SQL string. The issue's own measurement:
13+
`generateSql({cube:'deals', measures:['total'], dimensions:['id'], where:
14+
[['stage','=','won']]})` emitted `SELECT id AS "id", COUNT(*) AS "total" FROM
15+
"deal" GROUP BY id` with an empty `params`. It now emits the bound `WHERE` and
16+
returns the two won deals.
17+
18+
`FilterArray` (`['stage','=','won']`, `['and', […], […]]`, `[[…], […]]`) is
19+
INPUT-ONLY authoring sugar (#5285), and #5158's ruling C says every door into
20+
the runtime lowers it through the single `parseFilterAST` sink before anything
21+
downstream sees a filter. #5329 closed ObjectQL's six entry points that way and
22+
deleted the four drivers' private array dialects. Analytics is the **fifth
23+
door**: it compiles `where` itself — to SQL (`NativeSQLStrategy`) or to a
24+
`FilterCondition` for the engine (`ObjectQLStrategy`) — so nothing upstream
25+
lowers for it. It now gives the same three answers the engine door gives:
26+
27+
- `[]` — "no filter", not a failed filter: no predicate, no error (unchanged).
28+
- A well-formed `FilterArray`**lowered** through `parseFilterAST`, so both
29+
spellings of one filter select the same rows on both strategies.
30+
- Any other non-empty array — **refused** with `INVALID_FILTER` / 400
31+
(ADR-0112), the envelope the drivers' `filterArrayReachedDriverError` uses.
32+
This is where the undeclared INFIX form (`[condA, 'or', condB]`) lands, and
33+
where a list of `FilterCondition` objects (`[{stage:'won'}]`) lands — neither
34+
is a `FilterArray`, `parseFilterAST` has no lowering for either, and dropping
35+
them is what returned the unfiltered dataset.
36+
37+
Lowering rather than refusing keeps one dashboard's metadata meaning one thing:
38+
the same `where` on a plain `find()` already lowers at the engine door, so
39+
refusing it here would have forked the product by which face read the metadata.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `api` is a declared metadata kind — `DEFAULT_METADATA_TYPE_REGISTRY` + `BUILTIN_METADATA_TYPE_SCHEMAS` (#5271, part of #5206)
6+
7+
`api` items were produced, indexed and executed while the spec declared the kind
8+
nowhere. Artifact ingest maps `defineStack({ apis })` to `api` metadata
9+
(`ARTIFACT_FIELD_TO_TYPE`), the endpoint matcher indexes them
10+
(`buildEndpointIndex`), and #5040's executor serves them — but
11+
`DEFAULT_METADATA_TYPE_REGISTRY` had no `{ type: 'api', … }` entry and
12+
`BUILTIN_METADATA_TYPE_SCHEMAS` had no `api` binding. So
13+
`getMetadataTypeSchema('api')` returned `undefined` and `saveMetaItem` took its
14+
documented "unregistered type is stored without validation" branch:
15+
`PUT /api/v1/meta/api/:name` accepted **any JSON** and answered 200. That is
16+
`declared ≠ enforced` read backwards — enforced but **undeclared**.
17+
18+
Both halves are now declared, which is one fix with two faces:
19+
20+
- **A body is validated.** The existing 422 `invalid_metadata` path applies to
21+
`api` like every other kind, with structured Zod issues naming the offending
22+
key. An endpoint with no `target`, or no `type`, is refused instead of stored.
23+
- **The type is describable.** `/meta/types` emits a real JSON Schema and a
24+
create seed for `api`, so the metadata-admin engine renders a form rather than
25+
a raw-JSON textarea, and the entry carries a real label, domain and file
26+
patterns instead of the synthesised `label: 'api'`, `filePatterns: []`
27+
placeholder a type with no registry row gets.
28+
29+
**The write door is unchanged.** `allowRuntimeCreate: true` records what the
30+
runtime already did: with no static registry entry, both write gates
31+
(`isRuntimeCreateAllowed`, `assertAllowed`) fall through to "runtime-creatable",
32+
and both name `api` in that comment. `allowOrgOverride` stays `false`, also its
33+
effective value today — an endpoint is the publishing package's outward URL
34+
contract, and a per-org fork could move `path`, flip `authRequired` or drop
35+
`rateLimit` on a URL third parties integrate against. Marking the type code-only
36+
instead (`allowRuntimeCreate: false` + `allowOrgOverride: false`) was considered
37+
and rejected: it would turn today's 200 into a 403 rather than validate it, and
38+
#5086's refusal runs before persistence for drafts too, which would leave
39+
#5206 step 2's `publishPackageDrafts` endpoint gate with no draft to gate.
40+
41+
**`ApiEndpointSchema` gains the ADR-0010 protection envelope, and stays open to
42+
unknown keys.** Every registered kind must declare the envelope its loader
43+
stamps (`_packageId` / `_provenance`), or it is dropped on every parse; that
44+
spread is added. Closing the shape against unknown keys was attempted and
45+
**measured to be unsafe**: the same schema parses stored rows as well as
46+
authored declarations (`buildEndpointIndex`, `gateApiItemsForPublish`), and a
47+
stored row carries the metadata layer's own bookkeeping (`packageId`, `state`),
48+
so `strictObject` turned 10 tests in `packages/metadata` red — the load-time
49+
backstop excluded endpoints and the publish gate reported a schema error in
50+
place of its ADR-0121 D6 verdict. `api` therefore joins `view` on the #4001
51+
campaign's `STILL_STRIP` list, with that measurement written into the list's own
52+
note, and the real fix (separating the stored envelope from the body at the
53+
metadata layer) is filed as #5309 rather than bought by teaching the authoring
54+
vocabulary two storage keys.
55+
56+
**This is a shape check, not a second servability judge.** ADR-0121's rules —
57+
the `apps/<namespace>` carve-out (D1/D2), anonymous-requires-an-armed-`rateLimit`
58+
(D6), the supported target subset, mapping and policy — stay with
59+
`validateApiEndpointDeclarations` / `identityFreeEndpointGateFailure`, which run
60+
at publish and again at load. A pin test asserts an anonymous unmetered endpoint
61+
parses green here and is still refused by the gate, so the two never grow
62+
competing opinions.
63+
64+
**Upgrade note (not purely additive).** A stored `api` row that does not satisfy
65+
`ApiEndpointSchema` is refused with 422 on its **next write**; reads and the
66+
existing load-time behaviour are unchanged (the matcher already excluded
67+
unparseable rows loudly, #5189). Every `api` declaration reachable in this repo
68+
— the two E8-migrated showcase endpoints and the two dogfood policy-fixture
69+
endpoints — was parsed against `ApiEndpointSchema` before landing this: all four
70+
clean. A live deployment's `sys_metadata` cannot be scanned from CI; an operator
71+
holding hand-written `api` rows should run `GET /api/v1/meta/diagnostics?type=api`
72+
(which now covers the type) before upgrading.
73+
74+
ADR-0088's admission test is satisfied on all three clauses: independent
75+
lifecycle (the matcher indexes and invalidates one item at a time), declarative
76+
governability (`allowRuntimeCreate` plus file patterns), and a real consumer
77+
(#5040's executor, boot-proven by #5040 E8). This does not reverse the `router`
78+
kind's retirement — `router`'s delivered forms are code contributions, whereas a
79+
single `ApiEndpoint` is a declarative artifact, exactly the "third, real
80+
delivered form" ADR-0088's own `router` row anticipated.

0 commit comments

Comments
 (0)