Skip to content

Commit 436f8e2

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-17385-chartconfig-precedence-half-2
2 parents 64ce88a + 74fb2f7 commit 436f8e2

39 files changed

Lines changed: 2269 additions & 205 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`enableOnInstall` is declared in three published schemas; each one now says which of the three governs it, and the two that are not the authority say what they are (#18605).
6+
7+
The install door already honours the key — `POST /api/v1/packages` writes the registry row's `enabled` from `enableOnInstall ?? true` (#18058). What was left was three declarations that looked identical (`z.boolean().default(true)`, same description) with nothing saying which one an author should read.
8+
9+
Clause-②: yes
10+
11+
**The authority**
12+
13+
`PackageInstallRequestSchema` (`api/package-api.zod.ts`) is the one authority, because it is the request contract of the door that honours the key. Its published description now says so: "honoured at POST /api/v1/packages: the installed row's `enabled` is written from this key". Its doc block carries the map to the other two, so a reader never has to guess which of three identical-looking declarations governs.
14+
15+
**`kernel/InstallPackageRequest.enableOnInstall` — a COPY of the request key**
16+
17+
Same type, same default, same meaning, restated on the in-process protocol primitive `ObjectStackProtocol.installPackage`. Its published description now records that this layer does not read it: the implementation reads `manifest` and `settings` only, and the HTTP door does not forward the key down that seam — it calls `installPackage({ manifest, settings })` and performs the enable/disable flip itself, because the durable half must follow the row that door returned rather than the request's intent.
18+
19+
The copy is held to the authority by a **parity pin** rather than by a structural reference. The structural spelling is not available in this direction: the authority is built from `ManifestSchema` and `InstalledPackageSchema`, both declared in `kernel/package-registry.zod.ts`, so `PackageInstallRequestSchema.shape.enableOnInstall` spelled there is an import cycle, and under `OS_EAGER_SCHEMAS=1` — the mode `gen:schema` and `check:authorable-surface` run in — it dies with `ReferenceError: Cannot access 'InstalledPackageSchema' before initialization`. `api/package-install-one-authority.test.ts` parses both declarations over one matrix (absent, `false`, `true`, a string, `null`) and reds on any cell where they disagree.
20+
21+
**`marketplace/MarketplaceInstallRequest.enableOnInstall` — not this key at all**
22+
23+
It stays, and its published description says what it is: the marketplace channel's own install option. That request's subject is a listing (`listingId`, `version`, `licenseKey`, `tenantId`), not a manifest; its door is the control plane's `POST /api/v1/marketplace/install`, of which a runtime mounts only a read-only proxy; and the channel resolves the artefact and validates the licence before mapping what it holds into a platform install. It is one translation upstream of the door key, owned by a different party on a different release cadence, so folding it would let a narrowing at the platform door silently narrow a control-plane contract.
24+
25+
**What does not move**
26+
27+
No key is added, removed, renamed or retyped, and no default changes: the accept set of all three schemas is byte-for-byte what it was, and `api-surface`, `api-surface-declarations`, `authorable-surface` and `authorable-defaults` are all unchanged. What moves is the published description text of three keys and the reference pages generated from it. The `Clause-②` declaration is `yes` as the conservative arm, because three published declarations' stated meaning moves.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/objectql": patch
3+
"@objectstack/runtime": patch
4+
---
5+
6+
Installing a package no longer reverts an operator's most recent enable/disable. The install contract is now 「缺省 = 保持,有旗 = 设置」: an install that was not asked to move the lifecycle state does not move it (#18877).
7+
8+
`SchemaRegistry.initialDisabledPackageIds` is a boot hydration input — filled once, before any registration, from the durable disable file, and never updated by `enablePackage` / `disablePackage`. It was nevertheless consulted by every `installPackage` call, so once an id was in the boot seed set, every re-install within that boot re-landed it DISABLED whatever the operator had most recently done. Since the durable write started following the row the door returns (#18752), that stopped being memory-only:
9+
10+
```text
11+
boot 1 operator disables the package → disk lists the id
12+
boot 2 seeded from disk; the package installs disabled
13+
PATCH /packages/:id/enable → 200, registry true, disk CLEARED
14+
install(m, { overwrite: true }) (no flag) → the seed still listed the id
15+
→ row disabled, disk written DISABLED
16+
boot 3 the operator's enable is gone, with no error anywhere
17+
```
18+
19+
Reachable with nothing exotic: disable → restart → enable in Studio → an SDK upgrade with `overwrite`.
20+
21+
- **`installPackage` reads the ROW first.** An existing row keeps its own `enabled`, `status` and `statusChangedAt`; the boot seed decides only for an id that has no row yet (boot hydration and a genuinely fresh install). A fresh id the seed never named still lands enabled, the declared default.
22+
- **`enableOnInstall` now sets the state in BOTH directions.** `true` ⇒ `enablePackage`, `false` ⇒ `disablePackage`, and an ABSENT flag makes no lifecycle call at all — previously only `false` was read, and the `true` case was carried by the re-install restamping every row enabled. The bare (unwrapped) body form still honours nothing: no schema declares the key there.
23+
- **`DELETE /packages/:id` clears both records.** The id leaves the boot seed set with its row, and its durable disable entry is cleared, so the next install of that id is a fresh install. Previously the durable record was immortal — a delete left a disable behind that named a package that no longer existed.
24+
- ⚠️ **Behaviour change for a flag-absent re-install of an EXISTING row.** It used to return the package to the declared default (enabled); it now preserves what the row says. An upgrade flow that relied on a re-install to clear a disable must now send `enableOnInstall: true` — the same key, the same door, now honoured in that direction. A fresh install is unaffected.
25+
26+
Maintainer decision batch #157 item 5, letter C. Item 4 of that ruling re-rules the #18058 F1 pin 「flag-absent re-install clears the durable disable」 to 「preserves」; F1b stands unchanged.
27+
28+
Clause-②: no
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
`buildSummaryIndex` no longer drops a declared `summary` field silently when the roll-up's `reference` carrier cannot be read — the skip now reports itself at `error`, naming the field, the consequence and the fix (#19082).
6+
7+
The child→parent foreign key is resolved by scanning the child object's `master_detail` / `lookup` fields for one whose `reference` names the parent. That comparison read the carrier raw (`cd.reference === parent.name`), so a carrier **no reader can read** — a non-string, where `FieldSchema.reference` declares an optional string — compared `false` against every name, `fkField` stayed unset, and
8+
9+
```ts
10+
if (!fkField) continue; // can't resolve the relationship — skip
11+
```
12+
13+
removed the roll-up from **both** summary indexes. `recomputeSummaries()` then had nothing to do after every insert / update / delete of the child, so the parent's stored summary value kept whatever it held while each of those writes reported success, and nothing anywhere said so. It is the second way this one function invents *"nothing to recompute"*; the first, its registry read, was closed as #9154.
14+
15+
- **⛔ The resolution rule is deliberately unchanged.** Loosening the comparison would trade a silent stall for a **mis-matched foreign key**, which is more expensive: a roll-up quietly aggregating the wrong children reads exactly like a correct one. PR #18503 recorded this site in its C2 list and the #18550 round left it there on purpose; that boundary still stands. What ends is only the silence.
16+
- **The carrier is read through the one arbiter**, `referenceCarrierOf` — the same accessor #19080 routed the two delete-cascade seams through. Its refusal is **caught** here rather than propagated, because this is a *scan* looking for the foreign key across every relation field: a propagating refusal on one unreadable field would hide a readable sibling that really is the FK, turning a roll-up that works today into a hard failure of every write to that child.
17+
- **`error`, not `warn`**, and said once per index build rather than once per write. A persisted summary that silently stops tracking its children while every write keeps reporting success is the durability class, and the line it prints carries both halves an operator needs: what is not being maintained and will not recompute, and the two ways to fix it — spell the carrier as the target object's name, or name the FK explicitly with `summaryOperations.relationshipField`.
18+
- **Absence is untouched.** `undefined`, `null` and `''` mean "this field names no target", which is a legal thing to declare; they skip silently exactly as before. Every readable carrier resolves exactly as before.
19+
20+
No schema changed, no key was added or removed, and nothing that resolved before resolves differently now. `engine-summary-index-unreadable-carrier.test.ts` pins both directions — the unreadable carrier reporting its skip, and a normal `reference` still resolving `fkField` — because without the second one, a change that simply stopped resolving anything would look identical to a fix.
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+
`ActionSchema.undoable` — the published description now names the WRITTEN set, not `patch` alone (#19148).
6+
7+
**FROM** — "`operation: 'update'` is the declared form of that action — its `patch` names exactly the fields whose prior values are captured."
8+
9+
**TO** — "`operation: 'update'` is the one declared operation and the declared form of that action: what the undo captures is the prior value of EVERY field the action writes — the merged write bag, `patch` UNDER the collected `params`, not `patch` alone. An action with no `operation` declares no write set, so nothing anchors the capture there."
10+
11+
An `operation: 'update'` action writes two sources: the static `patch` AND whatever its `params` collect. On any params-carrying action, "exactly the `patch` fields" is a strict subset of what the action writes, so an Undo built to the old sentence restores part of the change and reports the action as undone.
12+
13+
- **Prose only — no schema change, no accept/reject outcome moves.** The same author input parses the same way before and after; `Clause-②: no`.
14+
- **The executor already captured the union.** `executeDeclarativeUpdateAction` keys `undoData` off `Object.keys(data)`, `data` being `declarativeUpdateWrite`'s merged bag `{ ...patch, ...params }`. The sentence was the outlier, and the EXECUTOR CONTRACT doc block ~200 lines above in the same file already read "exactly the fields written".
15+
- **One operation, one rule.** The `operation` enum carries exactly one member, `'update'` (`'delete'` and `'custom'` are refused with their reason), so the per-operation capture rule is a one-row rule and is written as one.
16+
- The describe text renders into three generated reference tables (`ui/action`, `data/object`, `kernel/metadata-plugin`), regenerated here; the hand-written protocol page `content/docs/protocol/objectui/actions.mdx` carried the identical claim and is corrected in the same edit.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
---
4+
5+
feat(platform-objects): declare the `set_user_manager` row action on `sys_user` (#19249)
6+
7+
`sys_user.manager_id` drives the approvals `{ type: 'manager' }` rung and the ADR-0057 `own_and_reports` read scope, and `POST /api/v1/auth/admin/set-user-manager` (#16678 Phase 3) has been its only product write surface since it landed — with nothing in the Console reaching it. This declares that affordance: a `set_user_manager` row action on `sys_user`, offered from the Users list row menu and the record-detail header, collecting the new manager through an inline `sys_user` lookup and POSTing `{ userId, managerId }` to the admin endpoint.
8+
9+
Three properties of the declaration are decisions rather than detail:
10+
11+
- **It posts the admin endpoint, never the generic data API.** `sys_user` is `managedBy: 'better-auth'` and the ADR-0092 D2 managed-update whitelist is `{name, image, locale}`, so a picker writing `manager_id` through `/api/v1/data` would be refused by the identity write guard — correctly — and would read as a Console bug. The field keeps `readonly: true`; the endpoint reaches the column by system context.
12+
- **Its `visible` predicate carries the directory-sync term and not the self-service one.** A directory-owned identity (`source: 'idp_provisioned'`) is refused by the endpoint, so the button is hidden for one — the same term the three self-service identity actions on this object already spell. Their `record.id == ctx.user.id` half is deliberately not carried over: this is an admin action on someone else's row.
13+
- **No second copy of the server's refusals.** Self-assignment, cycle, depth, cross-organization and directory-owned identity are enforced at the write, in one derivation, and surface from there. Nothing is re-derived client-side.
14+
15+
Additive: no existing action, field or predicate changed. The `manager_id` field and its read-only rendering are untouched, and `sys_business_unit.manager_user_id` (Business Unit Head) is a separate, independent relation that this does not read or write.

‎.claude/agents/os-dev.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ model: opus
276276
- 分区 pin `test/vitest-tiers-partition.test.ts` 在某个测试文件两层皆无或皆有时变红。
277277
- 用 `git push -u origin claude/issue-<n>-<slug>` 推上去,网络失败退避重试;pre-push 拒卡片 trailer。
278278
- Draft PR 指向 `main`,正文首行 `Fixes #<n>`;合并不应关卡时用 `Part of #<n>`,并说明留下哪一半。
279-
- 正文行首照抄认领的 `Clause-②:` 行:`Check Changeset` 读正文不读卡,开 PR 那一笔就带上。
279+
- 认领带 `Clause-②:` 行才照抄进正文行首:`Check Changeset` 读正文不读卡;无则零写,⛔ 不自造。
280280
- ⛔ 永不 `Fixes` 一张还在决策箱的卡:合并会静默关掉它,而收件箱过滤只读 open。
281281
- ⛔ 不写否定式的关单句,它照样关掉点名的卡:解析器无视否定,只匹配关键词 + `#<n>`。
282282
- 关键词是 `fix/fixes/fixed/close/closes/closed` 与 `resolve/resolves/resolved`;让它们远离其它卡号。

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
- 细节禁止复述:逐卡理由已作审计评论落卡,简报只指不拷。
5959
- 硬预算正文 ≤3000 字符,收班自报字符数;连续两轮超限即漂移。
6060
- 互斥最小尾页读法:尾页配方只覆盖座位贴的两个读数,判空缺另对正文与标题。
61-
- 第三读数(车道 `pm:dispatched` 卡最新非本 session `Claim:`)出自车道盘点,不在座位贴线程。
61+
- 第三读数(本车道 `pm:dispatched` ∪ `pm:queue` 卡最新非本 session 同席号 `Claim:`)出自车道盘点。
6262
- 配方:`issue_read get` 拿评论总数,按小 `perPage`(~15)算末页页码只读尾页。
6363
- 尾页缺读数再回翻一页,⛔ 不读整线程;返回数少于总数 ⇒ 记录可能已消失,报缺口。
6464
- 长线程状态读外包:>~15 条的线程派下限档只读子代理返回结构化摘录,尾部优先。

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,12 @@ index is built from TRACKED sources only, deliberately. An untracked, generated
261261
or dependency-owned declaration is one this census never saw, and «never saw it»
262262
must not be spelled the same way as «read it, not an engine».
263263

264+
⚠️ One arm here says something else again: `type-text-not-round-trippable` is a
265+
receiver whose declared type the census STORED whitespace-collapsed and could
266+
not read back — the source parsed, the re-serialisation of it did not, so the
267+
door rule could never be read off it. That is a fault in this tool rather than
268+
a fact about the corpus, and it is the one row here that also fails the gate.
269+
264270
| what | count |
265271
| :--- | ---: |
266272
| write calls subtracted with no defensible reason | **1** |
@@ -290,11 +296,11 @@ holds still. They are required to be HERE and to say WHEN they were true;
290296
their values are not compared. The reasoning, and the measurement behind it,
291297
are in `scripts/check-tenant-audit-census.mjs`.
292298

293-
Measured on 2026-09-18 at `30def652e`.
299+
Measured on 2026-09-20 at `215840f43`.
294300

295301
| corpus scale (not enforced) | count |
296302
| :--- | ---: |
297-
| tracked non-test sources scanned | 573 |
303+
| tracked non-test sources scanned | 576 |
298304
| engine-shaped types recognised | 63 |
299305
| declared objects in the registry | 117 |
300306
| same-named calls subtracted as non-engine | 144 |

‎content/docs/protocol/objectui/actions.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ The `type` field selects how an action is dispatched. The complete enum is **`sc
5151
| `api` | Call an API endpoint (`method` defaults to `POST`). | Yes |
5252
| `form` | Open a FormView by name, routed to `/_console/forms/:name` **inside the console shell**; the submit lands on the created record. | Yes |
5353

54-
The one thing that is not a type: the **declarative single-record field write**. It is spelled `operation: 'update'` + `patch` — the same words a list view's `bulkActionDefs` uses for the bulk form — and it rides the default `script` route: the platform action route performs the write on the data plane **as the caller** (never system-elevated), so the caller's permissions, the object's hooks and its validations fire exactly as for a user edit. Beside `operation: 'update'` an action carries no executor binding of its own — `target`, `body`, `method`, `bodyExtra` and any other explicit `type` are refused — and `patch` is merged under whatever the `params` dialog collects, so a fixed value can be declared without exposing it in the dialog. `undoable` has its anchor here: the patch names exactly the fields whose prior values are captured.
54+
The one thing that is not a type: the **declarative single-record field write**. It is spelled `operation: 'update'` + `patch` — the same words a list view's `bulkActionDefs` uses for the bulk form — and it rides the default `script` route: the platform action route performs the write on the data plane **as the caller** (never system-elevated), so the caller's permissions, the object's hooks and its validations fire exactly as for a user edit. Beside `operation: 'update'` an action carries no executor binding of its own — `target`, `body`, `method`, `bodyExtra` and any other explicit `type` are refused — and `patch` is merged under whatever the `params` dialog collects, so a fixed value can be declared without exposing it in the dialog. `undoable` has its anchor here: the capture is the prior value of every field the action writes — the merged bag, `patch` under `params`, not `patch` alone.
5555

5656
```ts
5757
{ name: 'task_complete', label: 'Complete', operation: 'update', patch: { status: 'done' },

0 commit comments

Comments
 (0)