Skip to content

Commit 9cc3146

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-5619-sink-engine-dispatch
2 parents e7fc6e7 + 55c74de commit 9cc3146

77 files changed

Lines changed: 4800 additions & 3222 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: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `ActionSession` declares `positions` as canonical and deprecates `roles` (#5779)
6+
7+
The action-body `ctx.session` contract gains `positions`, the ADR-0090 D3 spelling
8+
of the caller's position names, and demotes `roles` to a deprecated alias of it.
9+
This is the **spec half** of #5613 phase 2, under the maintainer's contract-first
10+
ruling ("C skeleton + A semantics"): phase 1 (#5697) declared the shape the runtime
11+
already built, and this opens the rename on top of that declaration.
12+
13+
**What was wrong.** `buildActionSession()` copies `ExecutionContext.positions` into
14+
a key spelled `roles` — the one spelling ADR-0090 D3 bans — so an author met two
15+
different answers to one key name on one platform: `session.roles` is rejected in a
16+
hook (retired in #5050) and live, populated, and load-bearing in an action body.
17+
Phase 1 declared that reality without endorsing it and deliberately withheld a
18+
`positions` key, because minting a second live spelling with no closing date is the
19+
defect rather than the fix. This change mints it **with** a closing date.
20+
21+
**Migration prescription — do this now.**
22+
23+
- Read `ctx.session.positions`. It is the canonical key and it carries exactly the
24+
array `roles` carried; the rename is a rename, not a semantic change.
25+
- `ctx.session.roles` still resolves for the length of the deprecation window and
26+
is removed after it, on the path `session.tenantId` already walked (#3280
27+
deprecated, #3290 removed in v11). A body still reading it at that point sees
28+
`undefined` with nothing to catch the change — which is why the read moves inside
29+
the window, not at its close.
30+
- Do **not** migrate an access check by renaming it. `roles.includes('admin')`
31+
rewritten as `positions.includes('admin')` migrates the defect: neither array is
32+
an authorization input. Privilege is judged by the security service, which
33+
evaluates capability grants, placements and the derived posture (ADR-0095).
34+
35+
**Sequencing — the contract leads its producer.** This release ships the contract
36+
only. The producer change (`buildActionSession()` emitting both keys, plus the two
37+
already-tracked wrong sentences in its docblock) is #5613's runtime half and lands
38+
separately. Until it does, a built session still carries only `roles`, so
39+
`positions` is meaning-fixed but not yet presence-guaranteed; both keys are
40+
optional, which is what lets the declaration lead without breaking anything. A
41+
reader that must straddle the seam may read `positions` and fall back to `roles`
42+
for the window's duration only — that fallback expires with the alias.
43+
44+
Additive and non-breaking on its own: adding an optional key rejects nothing that
45+
parsed before, and the runtime consistency pin
46+
(`packages/runtime/src/action-session-shape-contract.test.ts`) is unchanged and
47+
still green.
48+
49+
The reader-facing announcement is the ADR-0087 semantic migration
50+
`action-session-roles-to-positions`, which carries the prescription above and its
51+
acceptance criteria into `spec-changes.json`, the generated upgrade guide and the
52+
`spec_changes` MCP tool.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
fix(plugin-auth): `convertWhere()` 补齐 `not_in` / `starts_with` / `ends_with`,未识别算子改为响亮拒收 (#5813)
6+
7+
`convertWhere()` 的分支链只覆盖 better-auth 十一个算子里的八个。
8+
`not_in` / `starts_with` / `ends_with` 落在链尾之外:**`filter` 里不写任何键**,
9+
不告警,链尾也没有 `else` 兜底。一个只带这类条件的 `where` 因此编成 `{}`
10+
11+
**丢谓词不是把结果变窄,是变宽 —— 而且发生在身份表上**(#3948 反复论证过的形状,
12+
driver-memory 的匹配器 `default:` 臂与 objectql 的 `having` 都为此改成了拒收):
13+
14+
- `findMany` / `count` 变成**全表**(仅受 `limit` 截断)。已挂载的
15+
`GET /api/v1/auth/admin/list-users`(`auth-route-ledger.ts:161`)把查询参数直接
16+
推进 `where`,而 `searchOperator` 的枚举是 `contains | starts_with | ends_with`
17+
`filterOperator` 的枚举**就是整张算子表**。于是
18+
`?searchValue=abc&searchOperator=starts_with` 返回的是「全部用户」而不是「以 abc
19+
开头的用户」,`?filterField=email&filterOperator=not_in&filterValue=…` 不排除任何人。
20+
管理台的用户检索是它的主要消费者。
21+
- `update` / `delete` / `consumeOne` / `incrementOne` 走的是「先 `findOne(filter)`
22+
再按 id 写」,`{}``findOne` 返回**任意一行**(实测是第一行),于是写到了错误的
23+
记录上。实测证据:对四行表执行「删除 `name``zed` 开头的用户」,修复前删掉的是
24+
`u_abc1`(第一行),不是 `u_zed`
25+
26+
## 改了什么
27+
28+
**一、三个算子按词表直译**(三个 ObjectQL 算子都在 `FILTER_OPERATORS` 里,
29+
五后端都必须求值):
30+
31+
| better-auth | ObjectQL |
32+
|:--|:--|
33+
| `not_in` | `$nin` |
34+
| `starts_with` | `$startsWith` |
35+
| `ends_with` | `$endsWith` |
36+
37+
大小写语义两侧同向,直译不开契约缝:better-auth`Where.mode` 默认
38+
`"sensitive"`,`$startsWith` / `$endsWith`#5701 Q2=A 在契约层也是大小写敏感。
39+
40+
**二、链尾未识别算子响亮抛错**,不再静默丢。错误信息带算子名、字段名与受支持算子
41+
清单,本身就是操作指引。这是 restore-invariant:否则 better-auth 下次加算子时,
42+
这个洞会以完全相同的方式重开一次。
43+
44+
## 对使用方的影响
45+
46+
- 用上述三个算子的查询**从「返回全表 / 写错行」变成「按谓词正确过滤」**。这是缺陷
47+
修复,不是可依赖行为的移除 —— 但依赖「`starts_with` 检索能列出全部用户」的脚本会
48+
看到结果变化。
49+
- 传入**词表之外**的算子从「静默忽略该条件」变成**抛错**。今天没有活体调用方能命中
50+
这一支(`/admin/list-users` 的两个参数都由 better-auth 自己的 zod 枚举把关),它面向
51+
的是将来:better-auth 长出第十二个算子时,查询会在第一次执行就失败,而不是悄悄放大。
52+
该分支同时是编译期哨兵(`never` 收敛),`pnpm --filter @objectstack/plugin-auth
53+
typecheck` 会先一步报错。
54+
- `Where.mode: 'insensitive'` **不在**本次范围内,也不会被这条拒收波及 —— `mode`
55+
`operator` 的兄弟字段而非算子,今天仍被忽略(#5814,决策箱中)。
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): `--update-base` re-anchors forward or refuses — never backwards, never mid-merge (#5370)
6+
7+
#5358 made re-anchoring `packages/spec/authorable-surface.base.json` an explicit act
8+
(`gen:authorable-surface-base`). It settled **when** the anchor may be written, not **where
9+
from**: the baseline is still `merge-base(HEAD, origin/main)`, and that is not always ahead
10+
of the anchor already committed.
11+
12+
The reported way in is a stopped merge. Until the merge is committed, `HEAD` is the branch
13+
tip from *before* it, so the merge base is the branch's **old fork point** rather than the
14+
main tip being merged in, and re-anchoring there rolls `baseRev` backwards. Measured on the
15+
#5312 sync relay: `1c3da1f``5aae790`, returning the 109 keys #5321 had just retired.
16+
17+
Nothing could catch it. The older rev is a genuine `origin/main` ancestor and the keys
18+
written are that commit's surface verbatim, so the regressed file is **authentic**
19+
`verifyCommittedSurfaceBase`, `check:authorable-surface` and the pre-commit `os-regen` guard
20+
are green before and after. The only trace is a reverse `baseRev` move in the diff, which
21+
reads like the #4650 attack shape and was written by the generator itself.
22+
23+
Two refusals, both in `--update-base` only:
24+
25+
- **Mid-merge**: `MERGE_HEAD` present (resolved via `git rev-parse --git-path`, so linked
26+
worktrees are handled) refuses before a single schema is generated, the way
27+
`--check --update-base` already did, and prescribes the remedy — commit the merge, then
28+
re-anchor. `scripts/regen-artifacts.mjs` states the same rule for the merge driver's side
29+
("Re-anchor after the merge is committed, or not at all"); this enforces it for the human
30+
who types the command anyway.
31+
- **Monotonicity**: the write happens only when the committed `baseRev` is an ancestor of
32+
the newly resolved rev. Equal keys still take the existing "nothing to re-anchor" path.
33+
34+
Ancestry that cannot be established refuses too, rather than defaulting to either verdict:
35+
`merge-base --is-ancestor` is read as three answers (`0` / `1` / anything else with a
36+
`fatal:`), and a `1` from a **shallow** checkout is discarded as unusable — truncation makes
37+
git report "not an ancestor" about commits that plainly are one. A `0` is trusted
38+
everywhere, shallow included, because a truncated walk can only lose reachability, never
39+
invent it.
40+
41+
`gen:schema` and every build are untouched: since #5358 they do not write this file at all,
42+
so a build during a merge behaves exactly as before.

.changeset/changelog-ships-in-tarball.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
"@objectstack/types": patch
6767
"@objectstack/verify": patch
6868
"create-objectstack": patch
69-
"objectstack-vscode": patch
7069
---
7170

7271
chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)

.changeset/config.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@
7575
"@objectstack/embedder-openai",
7676
"@objectstack/account",
7777
"create-objectstack",
78-
"objectstack-vscode",
7978
"@objectstack/connector-openapi",
8079
"@objectstack/verify"
8180
]
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
"@objectstack/spec": major
3+
"@objectstack/metadata-core": patch
4+
---
5+
6+
refactor(spec)!: retire `indexes[].type` and `indexes[].partial` — two authorable index keys no driver ever read (#5248, #4943)
7+
8+
`IndexSchema` declared five keys; only three of them ever reached a `CREATE
9+
INDEX`. `SqlDriver.syncDeclaredIndexes` builds every declared index through
10+
knex's `table.index(fields, name)` / `table.unique(fields, { indexName })`, and
11+
the drift differ's `DeclaredIndexInput` carries `name` / `fields` / `unique` /
12+
`nullSafeColumns`. So:
13+
14+
- **`partial`** — documented as *"Partial index condition (SQL WHERE clause)"*
15+
produced a **full** index with the predicate silently discarded. This was the
16+
damaging half, because it reads as a correctness control: the platform's own
17+
`sys_metadata` declared `partial: "state = 'active'"` for overlay uniqueness,
18+
and what the declaration alone materialized was an *unrestricted* unique index.
19+
- **`type`** additionally carried `.default('btree')`, so it appeared in **every**
20+
parse output of **every** index — an access-method knob that had never
21+
influenced a single statement, rendered as live configuration. (It was pinned
22+
as such in a `sys_presence` test, on an object that never declared it.)
23+
24+
Both are the ADR-0078 no-silently-inert / ADR-0049 enforce-or-remove shape.
25+
Remove was chosen over enforce: enforcing needs per-dialect algorithm mapping
26+
(`gin`/`gist` Postgres-only, `fulltext` MySQL-family), raw-SQL `CREATE INDEX …
27+
WHERE` on the dialects that have partial indexes at all (MySQL does not), and a
28+
redesign of how `isSyncReproducibleIndex` excludes partial indexes from
29+
incremental sync — design cost for a capability with no demand. If a real need
30+
appears it returns enforce-first.
31+
32+
## Migration
33+
34+
| FROM | TO |
35+
| :--- | :--- |
36+
| `indexes: [{ fields: […], type: 'gin' }]` | `indexes: [{ fields: […] }]` — create the specialised index from a database-layer migration |
37+
| `indexes: [{ fields: […], partial: "state = 'active'" }]` | `indexes: [{ fields: […] }]` — issue `CREATE [UNIQUE] INDEX … WHERE …` from a runtime migration |
38+
39+
**One-line fix: delete the key.** Neither removal changes any DDL, because no
40+
DDL ever depended on them — verified byte-for-byte against the `CREATE INDEX`
41+
statements SQLite actually stores
42+
(`packages/drivers/driver-sql/src/declared-index-retired-keys.test.ts`).
43+
44+
Both capabilities remain available where they are implementable. The index
45+
method is the driver/dialect's choice. A partial index is issued as raw SQL from
46+
a runtime migration — exactly what `metadata-protocol`'s `ensureOverlayIndex`
47+
already does for `sys_metadata`, and what actually delivers that table's
48+
active-row-scoped uniqueness today.
49+
50+
⚠️ **Not affected:** driver-sql's own `partial` flag (`parseIndexDdl` /
51+
`introspectIndexes` / `isSyncReproducibleIndex`). That is a boolean parsed back
52+
out of the *database's own* DDL for drift detection — the opposite direction —
53+
so migration-created partial indexes stay recognized and exempt from incremental
54+
sync, unchanged.
55+
56+
## The retirement kit
57+
58+
- `retiredKey()` tombstones at `IndexSchema` (the shape is deliberately
59+
`.strip()`, so a plain delete would swap one silent no-op for another): writing
60+
either key is now a `tsc` error and a parse error carrying the prescription.
61+
They sit at the bottom of the shape per the #5606 renderer note.
62+
- **ADR-0087 D2 conversion + D3 chain step** (`object-index-type-partial-removed`,
63+
`toMajor: 17`, wired into the existing step-17 chain): strips both keys from
64+
`objects[]` and `objectExtensions[]`; `os migrate meta --from 16` rewrites sources
65+
mechanically. A pure lossless delete — there was no effect to lose.
66+
- **Producers flipped:** `sys_metadata` (`idx_sys_metadata_overlay_active`, the
67+
case #4943 named) and `sys_view_definition` (`idx_sys_view_def_active`), both
68+
with their comments corrected to say what is actually materialized.
69+
- Published skill (`objectstack-data`), `content/docs/data-modeling/objects.mdx`,
70+
liveness ledger note and generated baselines updated.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
fix(metadata-protocol): 分层读的 overlay 读失败不再被画成「这一项没有定制」(#5707)
6+
7+
`getMetaItemLayered` 是 Studio「code / overlay / effective」对比视图背后的那次读
8+
(`GET /api/v1/meta/:type/:name?layers=true`)。它的 `sys_metadata` overlay 读裹着一个
9+
`catch`,注释写着 "DB unavailable — overlay stays null" 然后照「没有 overlay 行」
10+
返回。
11+
12+
那不是一个中性的兜底值。这个信封在**同一次响应里同时给出三个正面断言**,而且是 200:
13+
14+
- `overlay: null` —— 「这一项从来没有被定制过」;
15+
- `overlayScope: null` —— 「org 和 env 两个作用域都没有行」;
16+
- `effective === code` —— 「现在生效的就是打包件原样」。
17+
18+
对比视图存在的意义正是回答作者「我改过什么」。故障期它回答「什么都没改过」——
19+
#5532 同一个错误(可用性故障被讲成作者的声明事实),只是落在 diff 视图而不是 404 上。
20+
本次沿用 #5532 / PR #5705 的判定,补上该 PR 按 scope 刻意没有覆盖到的这一处读。
21+
22+
**改了什么**:这一处 `catch` 改为调用同文件的 `rethrowUnlessMetadataStoreUnprovisioned`
23+
—— `isMissingTableError`(表尚未建 → 确实没有 overlay 行)良性放行,其余上抛
24+
`status: 503` / `code: SERVICE_UNAVAILABLE`,驱动原始错误挂在 `cause` 上。没有新增
25+
判定逻辑,也没有新的返回形状:分层信封仍是 code / overlay / effective 三****,而不是
26+
每层三**** —— 「读不到」不是一层,所以照失败上报,不再冒充某一层的取值。
27+
28+
**wire 可见变化**
29+
30+
| 场景 | 之前 | 之后 |
31+
|---|---|---|
32+
| `sys_metadata` 不可达 | `200` + `overlay: null` / `overlayScope: null` / `effective = code` | `503` + `SERVICE_UNAVAILABLE`(`cause` 带驱动报文),可重试 |
33+
| org 作用域读失败、env 行本可读 | `200`,连那行 env overlay 也一并报告为「没有」 | `503`,同上 |
34+
| `sys_metadata` 尚未建表 | `200` + 只有 code 层 | 不变 |
35+
| 存储正常 | 不变 | 不变 |
36+
37+
REST 侧无需改动:`?layers=true` 与普通读共用同一个 `handleRouteError`,#5437 / #5464
38+
的消毒与日志口原样接住。已测量的消费方处置也都已就位:objectui
39+
`MetadataClient.layered()` 对非 2xx 一律 `throw`(只有 404 映射为空信封),
40+
ResourceEditPage 的加载 `try/catch` 把它渲染成错误态而不是空白页;
41+
`plugin-security` 的三个消费点里,两处本就有 `catch` 兜底,唯一没有的
42+
`projectPermissionMutation` 在 503 化后反而更安全 —— 此前的静默 `null` 会让权限集
43+
投影悄悄退回打包基线(`customized: false`),没有 declared body 时甚至会把记录
44+
retire,而协议的 `runMutationProjector` 契约是 never throws,会把 503 收敛成
45+
`projectionApplied: { success: false }`

0 commit comments

Comments
 (0)