Skip to content

Commit e6b5281

Browse files
committed
Merge origin/main into claude/issue-21663-readonly-shape-check
2 parents 196b217 + 251a7dd commit e6b5281

33 files changed

Lines changed: 1496 additions & 323 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The protocol 16 → 17 upgrade rationale no longer cites tracker numbers; each cited decision is stated in words
6+
7+
Clause-②: no
8+
9+
`MIGRATIONS_BY_MAJOR[17].rationale` in `@objectstack/spec` is the prose an author reads when upgrading metadata from protocol 16. `os migrate meta` prints it for each hop, and `docs/protocol-upgrade-guide.md` reproduces it word for word under "Protocol 16 → 17". It pointed at 116 issue-tracker numbers (91 distinct records, four of them in sibling repositories). Every number is gone. Where the sentence already said what was decided, the number was dropped. Where the number stood in for the decision, the decision is now stated. For example:
10+
11+
- The app-area paragraph says the fail-open area gates' caveat "was CLOSED by a server-side fix inside this same 17.0.0 window". The fix is described in the same sentence: `filterAppForUser` now runs the same `filterNav` over every `areas[].navigation`.
12+
- The datasource paragraphs name the change that validates `datasource.config` against the declared driver's own config contract, and the follow-up that retired the factory's legacy key aliases.
13+
- The aggregation paragraph names the freeze it relied on as the maintainer's freeze on the in-memory and MongoDB drivers, lifted 2026-08-11. It names the divergence class as the one closed when every SQL face got one aggregate spelling and one refusal.
14+
- The `view.exportOptions` paragraph says PDF export was declined platform-side as not planned.
15+
16+
Text only. No step, conversion, semantic entry, retired key or retired def changes: no id, order, schema or behaviour. The generated upgrade guide was regenerated from the new text. A tool that matched this rationale by its old text, for example by a tracker-number substring, needs the new spelling.
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+
The analytics SQL echo prints a date bucket only in the expression the driver itself groups it by, and refuses everywhere else, including on the in-memory and MongoDB drivers (#21647).
6+
7+
Clause-②: no
8+
9+
- **What was wrong.** At a `timezone` of `UTC`, or with none, the ObjectQL face of `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` echoed a date-bucketed dimension as `date_trunc('month', col)` (or the asked granularity) wherever the driver renders no bucket expression of its own, and documented that as representative. On `driver-memory` the engine only fetches the rows and buckets them itself, answering keys such as `2026-01` and `2026-W02`, while both faces printed `date_trunc(...)`, a statement nothing ran. `driver-mongodb`, which groups the bucket in its own aggregation pipeline, took the same path. So did any host that wires no `dateBucketSql` hook.
10+
- **What it does now.** Wherever no driver expression stands for the bucket, on every driver and dialect:
11+
- `POST /api/v1/analytics/sql` refuses with `NOT_IMPLEMENTED` / 501, declared as a refusal so its message reaches the caller. Its message names the cause. A non-UTC `timezone` and SQLite already answered this way.
12+
- `POST /api/v1/analytics/query` answers the same rows as before, and its answer carries no `sql`.
13+
- **Unchanged.** On PostgreSQL, MySQL and SQLite at `UTC` or with no `timezone`, the echo still prints the expression the driver groups by: `to_char(...)`, `date_format(...)` and `strftime(...)`.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/organizations": minor
3+
---
4+
5+
fix(organizations)!: a create that names an `organization_id` meets the Layer 0 write wall, as the update does — the insert stamp no longer rewrites it (#21666)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING.** On a walled posture (`isolated` / `group`), the insert stamp (Middleware A) overwrote a supplied `organization_id` with the caller's active organization in every user context. A create naming another tenant's organization answered `201` and stored the row in the caller's own organization, while the PATCH naming the same organization and the array insert (`createMany`) were refused `403 PERMISSION_DENIED`. One operation answered two ways, and the caller of the `201` had no signal that its input had been replaced.
10+
11+
The stamp now fills only an absent or empty `organization_id`, for every non-system context (ADR-0105 D5). A supplied value is left as sent and meets the Layer 0 write wall in `@objectstack/plugin-security` (ADR-0095 D1), which answers the create exactly as it answers the update:
12+
13+
- **Another tenant's organization** → `403 PERMISSION_DENIED`, nothing stored (was `201`, stored in the active organization). This holds for a member and for a platform administrator on a tenant object. A member's forged `organization_id` stays refused; the wall refuses it now, where the stamp used to rewrite it.
14+
- **No organization** → stamped with the active organization, as before.
15+
- **The caller's own active organization** → admitted, as before.
16+
- **Under `group`, a sister organization the caller holds** → admitted and stored in that organization, the same place the PATCH already moves a row to (was `201`, stored in the active organization). Where `organization_id` is the platform-injected column, the engine still strips it from a non-system payload as `readonly` and reports it in `droppedFields`, on the create as on the update.
17+
- **A platform administrator on a posture-permitting object** (`private`, platform-global, better-auth-managed) is exempt from the wall on the create as on the update.
18+
19+
Every door that writes one row at a time under the caller's context gives the same answer: `POST /data/:object`, the `create` operation of `POST /batch`, the clone route and the import runner's per-row fallback. Two of these change in ways worth knowing:
20+
21+
- An import row naming another tenant's organization is now reported as a failed row (`PERMISSION_DENIED`). Before, it was created in the active organization.
22+
- The clone route copies an `organization_id` that the object declares itself. So under `group`, a clone of a sister-organization row now lands beside its source instead of in the active organization.
23+
24+
System contexts are unchanged. The per-organization seed replay, the orphan claim, migrations and every other `isSystem` writer meet neither the stamp nor the wall. The `single` posture is unchanged too, because `objectstack serve` mounts this package only under a walled posture.
25+
26+
**What to do.** On create, either omit `organization_id` or name your active organization. If a platform operator needs a row in another organization, write it with a system-context write.
27+
28+
<!-- adr-0087: not-required (no-migration-prescription) a runtime write verdict: the insert stamp no longer rewrites a supplied organization_id, so the Layer 0 write wall judges it as it already judged the update and the array insert. No authorable key, spelling, export or stored shape moves, no stored row is read or rewritten, and which organization a caller meant to name is not something a ledger entry can rewrite. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this behaviour (not already-registered); and the change is a middleware verdict, not a TypeScript declaration (not runtime-interface-only or type-surface-only). -->
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/sdui-parser': minor
3+
---
4+
5+
A `kind: 'html'` page that hands a component input a literal of the wrong type now fails to compile, instead of compiling with a warning.
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) A severity change on an existing diagnostic of the html-tier compiler: no authorable key, input declaration, export or stored shape is removed, renamed or re-shaped, so there is no tombstone and nothing for `objectstack migrate meta` to rewrite. The values now refused live inside a page's `source` string, which no ledger entry reads, and which value the author meant (an object, a number, a boolean) is authoring intent no conversion can supply. The in-tree authored population is zero: every `kind: 'html'` page in the example apps and every html example in the published skills compiles without a type mismatch against the committed `sdui.manifest.json` (census on the PR). The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers a compiler severity and this diff adds none (not registered / already-registered); and the change is compiler behaviour, not a runtime interface or a type surface alone (not runtime-interface-only / type-surface-only). -->
10+
11+
**BREAKING**: an accept-set narrowing on the html page compiler, shipped as `minor` under the launch-window convention for accept-set narrowings. No export, type or diagnostic code changes.
12+
13+
**What changed.** `compile()` used to grade a `type-mismatch` as an `error` only when the input declared an `enum` arm, and as a `warning` otherwise. Every value the type check sees is a literal written in the source: a quoted attribute is a string, a bare attribute is `true`, and a braced value is the exact literal written. (A braced value that is not a literal is reported separately as `inert-expression`, which stays a warning.) A literal's type is known when the page compiles, so a mismatch is certain, and it is now an `error` for every declared type. `member-type-mismatch`, the same check applied to the members of an array or map, follows the same rule. Codes and messages are unchanged.
14+
15+
**Why.** `aggregate="count"` on an `object-metric` passed `os build` with one warning. The tile reads `aggregate.function` and `aggregate.field`, received a string, and drew no number.
16+
17+
**What an author now sees.** `os validate`, `os build` and `os lint` fail on the page, and the save door refuses it when the host has a component manifest, with the existing message, for example `<object-metric> prop "aggregate" expected an object`. To fix the page, write the value in the type the input declares: braces with JSON for an object (`aggregate={{"function":"count"}}`), braces for a number or a boolean (`limit={50}`, `invert={true}`), and braces with an array for an array (`fields={["name","amount"]}`). A string-typed input still takes a quoted value.

‎.claude/skills/pm-dispatch/references/execution-duties.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
- ⑤ 派发令点名排除清单:长得像家族而不是的成员及理由。
3232
- fold-or-serial 必答:≥2 张排队卡共享热文件时,必须以五门为判据显式答折叠或串行。
3333
- 答题处是串行队条目和/或链首认领;不答而默认串行是漏答。
34+
- 分诊折叠提议仅是建议,点名各卡皆无 open PR 才有效;否则各卡各自落地,拒折理由记一行。
3435
- 并行纪律四条(常规,非豁免):① 认领申报文件面到区域级,拿不准就串行。
3536
- ② 开 PR 前合一次 main;③ 兄弟卡落地后再合一次;④ 冲突交合并队列仲裁,⛔ 不手动排序。
3637
- 文件面不相交只保证文本可合并;跨文件语义耦合由队列 CI 逮住,⛔ 不读作不可能冲突。
@@ -39,6 +40,7 @@
3940
- 在飞卡衍生三分:不修它验收过不过;in-scope ⇒ 父卡 sub-issue,认领席自有、优先级继承。
4041
- 该 sub-issue 带父卡域与优先级直接派发,唯一绕过分诊;sweep 事后扫,重复关 `duplicate`。
4142
- 自在飞派发升级出的卡首行一句:本卡承哪一半、父卡留哪一半;缺此行不入队不入箱。
43+
- 分诊预见的后续工作是一张卡,触发条件落地时立卡;⛔ 分诊一句话永不授权无卡编辑。
4244
- 阻塞项无主 ⇒ 被挡席认领做掉,不限大小;在该卡走完整认领、尊重其热文件串行队。
4345
- 阻塞项在飞 ⇒ 等:`pm:blocking` 在其车道排最前、等待者写该卡;p0/p1 优先级沿链传递。
4446
- 取卡前置 = `docs/NORTH-STAR.md`「优先级」第 3 条:本车道队列开放 P0/P1 每次取卡现读。
@@ -55,7 +57,6 @@
5557
- 归属 = session ID,⛔ 不认作者字段;`Account:` 派生自 assignee,不等即半态、持卡席下笔补正。
5658
- 卡 assignee 归 PM,dev 恒不写;PR assignee = 卡的,接管 ② 同笔换;读者 `is:pr is:open assignee:LOGIN`。
5759
- dev `pr_create` 同轮 `label-write.mjs --issue PR_NUMBER --assign LOGIN`;席位自有 PR 指派自己。
58-
- 释放是显式动作:让卡离手者同笔清 assignee + `Release:` 行(会话/因/去向);下一任重新认领。
5960
- 部分落地(PR 带 `Refs #N (item k)`,⛔ 不 `Fixes`)即释放:合入同笔回 `pm:queue` + 清 assignee。
6061
- 同笔 `Release:` 行点名已落项与余项去向;余项需换道/拆分加 `pm:retriage`,自队列重新认领。
6162
- 派发前按序执行原子对:① Assign @me,并把 `pm:dispatched` 与摘 `pm:queue` 放进同一次标签写入。
@@ -160,7 +161,6 @@
160161
- 停摆永不自愈:携带任务中途状态的完成通知本身就是停摆信号。
161162
- 报告末句是意图不是结果 = 停摆非完成:当刻 SendMessage 续派,⛔ 不读作交付、不判死。
162163
- 停摆 ⇒ 立刻 SendMessage 附前台执行姿态句,⛔ 不等任何静默阈值(阈值是给没有回答的)。
163-
- 复位走梯度,每次比上一次更具体;第三次停摆判 unreliable,按接手协议重派。
164164
- 通知重放先算身份再读内容:与已验收那份同身份 ⇒ 记重放即结束。
165165
- 直接验收兜底:(a) draft PR 在且 CI 全绿 + (b) 探活确认已死或 ≥2h 无推送 + (c) 报告未达。
166166
- 三条件全立 ⇒ 直接按 PR 验收,复核判据不减;先探活后翻 ready。

‎content/docs/permissions/system-context.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ The largest single consumer — **17 of the 114 sites**.
182182
| 58 | Email-template / webhook provenance stamps skipped | plugin-email, plugin-webhooks | Lose: the row is not marked as an admin customization | `packages/plugins/plugin-email/src/email-template-provenance.ts#bindEmailTemplateProvenanceStamp`, `packages/plugins/plugin-webhooks/src/webhook-provenance.ts#bindWebhookProvenanceStamp` |
183183
| 59 | **Automation flow data nodes re-add the `owner_id` stamp** (the one place row 2's gap is compensated inline) | service-automation | Get: a flow-authored INSERT under system elevation still lands owned, when the run resolved a user. Fill-only — flow-authored values win | `packages/services/service-automation/src/runtime-identity.ts#stampSystemInsertOwner`, called from `packages/services/service-automation/src/builtin/crud-nodes.ts#registerCrudNodes` |
184184
| 60 | Inbox caller refusal names `isSystem` as what was carried | service-messaging | Get: nothing — the refusal still fires. The flag only shapes the diagnostic, because privilege is not an authorization subject | `packages/services/service-messaging/src/inbox-caller.ts#resolveInboxRecipient` |
185-
| 61 | **`organization_id` is not auto-stamped on INSERT** — the organization-axis twin of the `owner_id` gap above | organizations | Get: an elevated write may name another organization deliberately, which is what the per-organization seed replay, the orphan-row claim, imports and migrations all rely on. Lose: the authoritative stamp, so an elevated insert that names no organization lands `organization_id = NULL` and the wall hides it. ⛔ This is why a forged `organization_id` is overwritten on the non-elevated path and not here: elevation is the seam the legitimate cross-organization writers use | `packages/plugins/organizations/src/organizations-plugin.ts#start` |
185+
| 61 | **`organization_id` is not auto-stamped on INSERT** — the organization-axis twin of the `owner_id` gap above | organizations | Get: an elevated write may name another organization deliberately, which is what the per-organization seed replay, the orphan-row claim, imports and migrations all rely on. Lose: the fill-only stamp, so an elevated insert that names no organization lands `organization_id = NULL` and the wall hides it. ⛔ Neither path rewrites a supplied `organization_id`: the non-elevated path fills only an absent one, and a supplied value meets the Layer 0 write wall, which refuses a forged one `403 PERMISSION_DENIED` exactly as it refuses an update re-pointing a row. Elevation skips the stamp and the wall alike, which is why it is the seam the legitimate cross-organization writers use | `packages/plugins/organizations/src/organizations-plugin.ts#start` |
186186

187187
### 6. Reads that only carry the flag onward
188188

0 commit comments

Comments
 (0)