Skip to content

Commit 86d1000

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-13135-widened-customization-retirement
2 parents 9d35c73 + 33184fd commit 86d1000

53 files changed

Lines changed: 4683 additions & 194 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.

.changeset/khaki-donuts-refuse.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
'@objectstack/driver-mongodb': patch
3+
---
4+
5+
`driver-mongodb` refuses an aggregate function it does not lower, instead of
6+
answering it as a silent SUM (#12818).
7+
8+
`buildAccumulator`'s `switch` on `agg.function` ended with
9+
`default: return { $sum: fieldRef ?? 0 }`, so ANY name this driver does not
10+
lower — a typo (`median`), a miscased spelling (`COUNT_DISTINCT`), a function
11+
added to the contract but not to this file, or an unnarrowed `method` arriving
12+
from `StrategyContext.executeAggregate` (#12776) — was answered as a **sum of
13+
that column**, under the alias the caller asked for, with no error, no envelope
14+
and no log. It is the worst available answer precisely because it is
15+
arithmetically plausible: a dashboard tile renders the number without complaint,
16+
so nothing downstream can tell "your function ran" from "your function was
17+
silently replaced". The field-less spelling was quieter still — `{ $sum: 0 }`,
18+
i.e. `0`, which reads as "no matching rows".
19+
20+
The refusal is the two-class ADR-0112 envelope both SQL faces already answer
21+
with (#5907), first sentence for first sentence, so one condition cannot have
22+
two wire identities depending on which backend served it:
23+
24+
- a name the Query Protocol does not declare answers `INVALID_QUERY` / **400**
25+
and names the declared vocabulary (`@objectstack/spec AggregationFunction`);
26+
- a DECLARED name this backend does not lower answers `NOT_IMPLEMENTED` / **501**
27+
and names what it does lower. That class is empty today — every member of
28+
`AggregationFunction` lowers here — and is pinned as a positive assertion, so
29+
the day the spec grows a function this driver misses, the suite goes red
30+
rather than quietly stopping to cover anything.
31+
32+
Judged case-sensitively, which is what the enum is: `COUNT_DISTINCT` is not
33+
`count_distinct`, and telling its author the backend has a capability gap would
34+
be false.
35+
36+
**Graded `patch`, deliberately.** No correct query's answer moves: all six
37+
declared functions and the two retired ones this face still lowers
38+
(`array_agg` / `string_agg`, an existing divergence from the SQL faces, recorded
39+
and filed as #13075 rather than closed here) are byte-identically unchanged,
40+
pinned by controls that compute their values in the same suite. The only inputs
41+
whose behaviour changes are ones this driver was already answering *wrongly*, so
42+
there is no working capability being removed — the same shape, in this same
43+
package, that #10576's per-aggregation-`filter` refusal shipped as a patch.
44+
45+
Nothing to migrate. A caller that was reaching the old `default` arm was reading
46+
a SUM in place of the function it asked for; the refusal now names the function
47+
and the remedy.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@objectstack/plugin-security": patch
3+
---
4+
5+
fix(plugin-security): report the two swallowed `tryUpdate` refusals outside the catalog seed (#12970)
6+
7+
Both sites call the shared `tryUpdate` in `permission-set-projection.ts`, which
8+
answers `false` on refusal. That answer is byte-identical to "nothing to do",
9+
and neither caller passed the optional refusal log the helper already accepts —
10+
so a refused write was indistinguishable from a clean pass.
11+
12+
**`permission-set-drift.ts` — a refused diagnostic write silenced its own
13+
report.** `persistPermissionSetDriftDiagnostics` counted only the writes that
14+
landed, and `runPermissionSetDriftDiagnostics` reported only when that count was
15+
non-zero. A boot on which every drift write was refused computed the drift
16+
correctly, persisted none of it, and printed nothing at all — indistinguishable
17+
from a deployment with no drift, while the sets kept enforcing grants that
18+
differ from the shipped artifact. The pass now records refusals, answers a
19+
`refused` count beside `updated`, reports them once per pass on the durability
20+
channel, and emits the drifted-set line when writes were refused as well as when
21+
they landed. A steady-state boot (nothing to write, nothing refused) stays
22+
exactly as quiet as before.
23+
24+
**`permission-set-overlay-discard.ts` — the audit line could describe a discard
25+
that did not happen.** On the degraded-kernel branch the resync write's result
26+
was discarded entirely. On refusal the row was re-read unchanged, so
27+
`objectGrantsAfter` equalled `objectGrantsBefore` while the `info` entry still
28+
announced a completed "sanctioned operator action": every field individually
29+
true, the entry as a whole false. The result is now read, and a refused resync
30+
emits one entry stating what did and did not land — the overlay row deletion
31+
(which had already succeeded) and the refused resync, with the un-healed grant
32+
count named as such — **instead of** the success line, never alongside it.
33+
34+
Both new lines go through the shared durability channel with its mandatory
35+
`warn` fallback, so they still print against a host sink that has no `error`.
36+
They reuse the shared refusal *accumulator* (`createSeedWriteRefusals`, with its
37+
cross-dialect classification and value-free driver-code channel) but not
38+
`reportSeedWriteRefusals`, whose prose is specific to seeding the RBAC catalog
39+
and would misdiagnose either of these paths.
40+
41+
No API is removed or narrowed. `persistPermissionSetDriftDiagnostics` and
42+
`runPermissionSetDriftDiagnostics` answer one additional field (`refused`), and
43+
what `discardPermissionSetOverlay` returns to its caller is deliberately
44+
unchanged.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/core': minor
3+
'@objectstack/plugin-auth': minor
4+
---
5+
6+
`PLATFORM_ADMIN` can now be anchored on deployment CONFIGURATION instead of a stored grant row: an account whose `sys_user.email` is on `OS_PLATFORM_OWNER_EMAIL` **and** whose `email_verified` reads verified resolves `PLATFORM_ADMIN` with the declared `admin_full_access` capability set, derived live on each authorization resolution (#11663 leg L2, design accepted 2026-08-25 as bundle 1A/2B/3A/4A/5A/6A/7A).
7+
8+
**Additive — nothing is revoked.** The legacy unscoped `admin_full_access` grant still confers exactly as it did; a holder whose standing rests on the row alone now gets a once-per-process pointer at the configuration line that re-anchors them. A deployment that has declared no administrators resolves byte-identically to before: the config list is empty, the derivation answers "not an admin" before it reads any row, and the pinned batch-equivalence query multiset is unchanged.
9+
10+
**The variable takes a list.** `OS_PLATFORM_OWNER_EMAIL` accepts one address or a comma-separated list of them — one normalization (`trim().toLowerCase()`), duplicates collapsed, blank entries dropped. ⛔ Any entry that is not an address **fails the whole variable closed** with a loud refusal naming it, rather than being skipped: silently dropping a typo would leave a narrower administrator set than the operator declared, with nothing anywhere to notice. Unset, blank or refused all mean **zero** config-derived administrators.
11+
12+
**Verified-email match only.** An unverified account holding a configured address confers nothing, and an ABSENT `email_verified` column reads unverified. The match reads the caller's own **stored** `sys_user` row, never the caller-supplied session email.
13+
14+
New exports from `@objectstack/core`: `resolvePlatformAdminEmails`, `parsePlatformAdminEmails`, `matchesConfiguredPlatformAdmin`, `normalizePlatformAdminEmail`, `PLATFORM_ADMIN_EMAIL_SEPARATOR`, `ADMIN_STANDING_NON_TABLE_INPUTS` and the test hooks beside them. `@objectstack/core` now depends on `@objectstack/types` (measured acyclic: `types` depends only on `spec`).
15+
16+
`@objectstack/plugin-auth`'s break-glass guard follows the derivation, as it must: `ADMIN_STANDING_SURFACE.sys_user` is reclassified `derives`, the last-administrator enumeration counts config-derived administrators through the resolver's own predicate, and a fifth write shape is judged — a change of address or an `email_verified` reset that would leave the environment with no administrator is refused, naming the configuration as the remedy. An ordinary profile write still costs the guard no reads.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"@objectstack/service-storage": patch
3+
---
4+
5+
fix(service-storage): stamp `sys_upload_session.organization_id` from the acting session (#12928)
6+
7+
`StorageMetadataStore.createSession` inserted into `sys_upload_session` with no
8+
execution context, so the SQL driver's `injectTenantOnInsert` had no `tenantId`
9+
to stamp from and every chunked-upload session row landed with
10+
`organization_id` NULL — on a tenancy-ENABLED object (the declaration carries no
11+
`tenancy` key, so `applySystemFields` provisions the column unconditionally).
12+
This is the `sys_upload_session` sibling of the `sys_file` gap fixed in #12745,
13+
and the chunked-upload door already held the value: it threads the identical
14+
`session?.organizationId` into the `createFile` immediately above.
15+
16+
`createSession` now takes the same optional `StorageWriteContext` as
17+
`createFile` and hands the engine `{ context: { tenantId } }`, so the platform's
18+
existing insert-side chokepoint decides the rest — whether the object has a
19+
tenant column at all, and whether an explicit value on the row wins. A caller
20+
with no organization passes no options and the row lands unstamped exactly as
21+
before.
22+
23+
Maintainer ruling 2026-08-29, verbatim and untranslated: 「同意」 — forward stamp
24+
only. There is deliberately **no backfill**: rows already NULL age out through
25+
this object's own ADR-0057 TTL sweep. That premise is verified rather than
26+
assumed — `sys-upload-session-ttl-sweep.test.ts` drives the shipped declaration
27+
through the real `LifecycleService` against live SQL and pins that an expired
28+
NULL-organization row is reaped, that a stamped row is reaped by the same
29+
sweep, that a live session survives it, and that a run with no declaration reaps
30+
nothing.
31+
32+
Why an unstamped row mattered even without a cross-tenant read: both walled
33+
Layer 0 predicates are exclusive (`{ organization_id: <id> }` under `isolated`,
34+
`{ $in: [...] }` under `group`), and neither matches NULL — so on a walled
35+
deployment an unstamped session row was invisible to its own tenant, the same
36+
silent-empty class `sys_api_key` was renamed to avoid.

.claude/agents/os-dev.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ pin 要防的失效 —— 而不是回退到本行。 -->
109109
款」的 ablation 条)。
110110
7. **排队不是停摆 —— 在轮内主动等。** 持锁的是你不拥有的进程,它的完成不以任何方式
111111
唤醒你:⛔ 永不为「等锁」结束一轮(实测:这么做的每个 agent 都无通知停摆)。循环:拿到
112-
99 就把间隔花在无锁工作上(写测试、changeset、PR 正文、包内 `typecheck`)⇒ 再跑一次;循环
113-
全程设 `OS_VERIFY_LOCK_SLOT=<>` —— 没排到的调用把排位**寄存**,同名再来续原到达戳;不
114-
设它,每次离开都从队尾重排(该机制只在 `--help` 与 99 的出错文本里自我介绍,读到那里已
115-
经丢过一次位)**排队 ~20 分钟无进展 ⇒ 先看这次检查能否收窄到不必持锁(收窄
112+
99 就把间隔花在无锁工作上(写测试、changeset、PR 正文、包内 `typecheck`)⇒ 带同名再跑
113+
一次;`OS_VERIFY_LOCK_SLOT=<稳定名>` **第一次尝试之前**就设好 —— 没排到的调用把排
114+
**寄存**,同名再来续原到达戳;不设它,每次离开都从队尾重排,事后补名开的是新排位,救
115+
不回已丢的那个**排队 ~20 分钟无进展 ⇒ 先看这次检查能否收窄到不必持锁(收窄
116116
要申报,见「干净收尾」);收窄不了就停下报 `blocked` 并点名持锁者** —— `os-verify-lock.sh
117117
--status` 打印持锁者、已持时长与队列;一动不动的持锁者本身就是真发现。沉默是唯一错
118118
误答案。

.claude/skills/pm-dispatch/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ devx@objectui;触发即规则 4 预登记的拆分条件),各席独立座位贴
263263
264264
| 标签 | 包家族 |
265265
|:--|:--|
266-
| `domain:engine` | `packages/objectql``packages/core``packages/formula`(CEL / `matches-filter` / RLS 谓词求值)、`plugin-pinyin-search`(落点在编译/查询核心);`packages/metadata*`(加载、注册、持久化、缓存、目录)、`packages/platform-objects`;`packages/drivers/driver-*`(维护者 2026-08-05 对 `driver-memory` / `driver-mongodb` 族有投入冻结指令;`formula` / `driver-sql` 不受影响)—— 车道合并(维护者 2026-08-19:「engine-core + metadata + drivers -> engine, identity + services → services 怎么样?」),原 `domain:engine-core` / `domain:metadata` / `domain:drivers` 三标签**退役**(只退出流通;GitHub 标签对象保留 —— 删标签会剥光已关卡、毁掉存档,止住流通的是本表) |
266+
| `domain:engine` | `packages/objectql``packages/core``packages/formula`(CEL / `matches-filter` / RLS 谓词求值)、`plugin-pinyin-search`(落点在编译/查询核心);`packages/metadata*`(加载、注册、持久化、缓存、目录)、`packages/platform-objects`;`packages/drivers/driver-*` —— 车道合并(维护者 2026-08-19:「engine-core + metadata + drivers -> engine, identity + services → services 怎么样?」),原 `domain:engine-core` / `domain:metadata` / `domain:drivers` 三标签**退役**(只退出流通;GitHub 标签对象保留 —— 删标签会剥光已关卡、毁掉存档,止住流通的是本表) |
267267
| `domain:services` | `packages/services/*``packages/connectors/*``packages/triggers/*``plugin-approvals``plugin-webhooks``plugin-email``plugin-reports``embedder-openai``knowledge-*`;`plugin-auth``plugin-security``plugin-sharing``plugin-audit`(2026-08-19 并入,原 `domain:identity` 标签**退役**,同上只退流通不删对象) |
268268
| `domain:devx` | `packages/lint``packages/sdui-parser``content/docs/**``apps/docs``scripts/`(门禁类;与 `domain:skills` 的分界按门禁的 SUBJECT:治理 agent 指令面/governed 面的归 skills,治理代码/文档质量的归本域)—— 与 `domain:spec` 相交的三面按「是否围着 spec 契约转」切分 |
269269
| `domain:skills` | 两个技能根:`.claude/skills/**`(含本文件)+ `skills/**`(维护者 2026-08-11 裁定单设座位,与维护者走专题讨论;skills 更新 ADR-class,见 Guardrails);指令架构文件:根 `AGENTS.md` + 根 `CLAUDE.md`(所有面向 agent 的宪法文本);governed 面(统一定义见 ACCEPT 路径分叉)的治理执行文件:`.github/CODEOWNERS`(治理路由半边)+ SUBJECT 是 governed 面本身的门禁/审计(现为 `scripts/pm/check-governed-merges.mjs`,一句话见守卫索引;未来同类同判)—— 维护者 2026-08-18 裁决:「skills 相关的应该都归你管,为什么派给了 devx」 |

.claude/skills/pm-dispatch/references/compile-surfaces.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
| 4 | service-analytics filter-normalizer | `packages/services/service-analytics/src/strategies/filter-normalizer.ts:1235`(`lowerAnalyticsWhere`) | analytics / cube 侧 |
1515
| 5 | `formula` | `packages/formula/src/matches-filter.ts:73`(`matchesFilterCondition`) | RLS 写侧 `check` 与公式求值;JS 两值语义的基准面 |
1616
| 半面 | objectql `having-filter` | `packages/objectql/src/having-filter.ts:92` / `:98`(`applyHaving` / `matchesHaving`) | 聚合****过滤。算半面是因为词表是子集,**但申报义务不打折** —— 它是**唯一没有 conformance 表覆盖的面**(`FILTER_LOGIC_CASES` 不驱动 HAVING 路径),所以漏了它连门禁都不会红 |
17-
| 冻结 | `driver-memory` / `driver-mongodb` | | 维护者 2026-08-05 投入冻结:**pin-annotate,不翻转**。冻结面仍要申报,结论是「不在范围 + 冻结指令」。现场注释见 `read-scope-sql.ts:176``having-filter.ts:41` |
17+
| 已解冻 | `driver-memory` / `driver-mongodb` | `packages/drivers/driver-memory/src/memory-matcher.ts:134`(`checkCondition`)、`packages/drivers/driver-mongodb/src/mongodb-filter.ts:700`(`translateFieldOperators`) | 2026-08-05 投入冻结,**2026-08-11 两条裁决解除**「不在范围 + 冻结指令」作废,按普通面申报 |
1818

1919
**这张表本身由 PR 维护 —— 与域表同一纪律。** 增删一面(新驱动、新求值器、某面被合并
2020
或退役、冻结状态变化)的那个 PR 顺手改这里,不留给下一次裁决重新数。清单****过期是

.claude/skills/pm-dispatch/references/dispatch-runbook.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ fire 附带一次:
6666
- **>24h 无自有产出(与既有回收线同一条)⇒ 当场降级**:标题改 `⏳ vacant` + 摘 assignee
6767
**同笔**,留证据评论(最后自有产出的时间戳与链接);在飞认领照认领协议由原认领者跟
6868
完。
69-
- 惰性判定(接管冲突时)照旧,本巡查是其常设出口(细则见 `seat-post-protocol.md`)。
7069

7170
## 分诊席读数成本三则(维护者 2026-08-20)
7271

@@ -96,7 +95,10 @@ fire 附带一次:
9695
`state=CLOSED` + `since``Blocked-by:` 反向索引求交,命中走既有回队双查;健康指标 = 逐
9796
标签 `perPage: 1` 只读 `totalCount`。每日层(当日首 fire)= 四仓全量对账 + 归集本就日频的
9897
职责(finding 集中轮、`Restart-when:` 判据批扫、决策箱回填)。成本靶:干活轮 ≤50k,空转轮
99-
守既有探针预算(契约复审的常设承担归项目总监席人工召唤,见 `contract-review.md`)。
98+
守既有探针预算(契约复审的常设承担归项目总监席人工召唤,见 `contract-review.md`)。每条枚举
99+
都比对返回数与 `totalCount`:不等 ⇒ 本轮没读完,报 `sweep INCOMPLETE` 点名缺口,⛔ 永不报干
100+
净(同 `check-governed-merges.mjs` 对未审计仓的标准);分页只治当次,治不了下次涨过页宽,故一个
101+
谓词、⛔ 不加第二条代码路径。
100102

101103
## 落卡与裁决记录细则(维护者 2026-08-13)
102104

@@ -112,8 +114,6 @@ fire 附带一次:
112114
一 —— open 卡标题带决策标记或线程含裁决记录、却仍挂 `needs-user-decision`;旗二 ——
113115
`pm:blocked` 卡的 `Blocked-by:` 目标已关闭。「交付了一半的阻塞」是判断不是 grep,归四件
114116
之③,⛔ 不进机械旗。
115-
- **落卡分析模板 + 业务写法六项**(中文,2026-08-19 裁;业务角度,2026-08-20 裁):模板骨
116-
架、六项写法要求与四棱块固定形状全部细则移 `references/decision-analysis.md`
117117
- **行文纪律**:出处一行(日期 + 原话);实测叙事至多压成一行 —— 故事住在卡上,不进操作
118118
文本。
119119

0 commit comments

Comments
 (0)