Skip to content

Commit bc4fddb

Browse files
committed
Merge origin/main into claude/issue-7277-adr-0029-d9-overlay-layer
一处冲突:`scripts/adr-anchors.json` —— main 的 #7301 把 ADR 锚点注册表 按文件分片(`scripts/adr-anchors/*.json`)并删除了整块 JSON,本分支同时 改了其中两条锚点的内容。按**并集**处置,不是二选一:采用 main 的分片布局, 把两条锚点的新内容移植进对应分片。 - `packages__objectql__src__registry.ts.json` —— invariant 由「D9 是 DESIGN ONLY,不要实现」改写为已落地的模型 - `packages__metadata-protocol__src__protocol.ts.json` —— 依 D9 §8 加上 ADR-0029;移植前逐字节校验了 main 侧 ADR-0119 那句未变,确保是并集而非覆盖 生成物 `content/docs/references/**` 取 main 一侧后用 `gen:schema` + `gen:docs` 重新生成,未手工合并。 其余全部自动合并。main 的 #7260 把**记录**所有权枚举扩成 `user | business_unit | org | none`,与 D9 的**贡献**种类 (`own | extend | overlay`)是两个枚举,两侧改动均完整保留。 main 对 `registry.ts` 的改动是 `applySystemFields` 内的纯注释,与 D9 模型 无交集;`registerObject` / `resolveObject` / `getArtifactItem` / 两个 hydration 缝 / delete heal 均未被 main 触碰。任何钉子都未移动。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw
2 parents 426cf52 + 62b6a2f commit bc4fddb

176 files changed

Lines changed: 7919 additions & 1137 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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
'@objectstack/plugin-approvals': minor
4+
---
5+
6+
Point the account app's **Approvals** navigation entry at the Approvals Inbox component, and contribute an **Approvals Inbox** entry to Setup (#7234).
7+
8+
The entry point has not moved — the account menu still shows **Approvals** with the same
9+
label and icon in every locale. Its destination has. It used to open the raw
10+
`sys_approval_request` grid, which is an admin/diagnostic view of the engine's own table
11+
and cannot show an approver a single decision button: every action on that object is gated
12+
on `record.viewer.can_act || record.viewer.can_override`, and the `viewer` block is
13+
attached only by the approvals REST path, never by the generic data API the object route
14+
reads. The result was a correct-looking list of rows nobody could act on. The entry is now
15+
`{ type: 'component', componentRef: 'approvals:inbox' }`, so it opens the full inbox —
16+
decision actions, business vocabulary, node progress and the request drawer.
17+
18+
- **Account app**: `nav_account_approvals` becomes a component entry gated by
19+
`requiresService: 'approvals'`, so it disappears where `plugin-approvals` is not
20+
installed (the previous `requiresObject` gate does not apply to a component entry).
21+
- **Setup**: `plugin-approvals` contributes a new **Approvals Inbox** entry at the top of
22+
**Setup → Approvals**, above the three raw tables, which stay exactly as they were —
23+
admin-gated by `manage_platform_settings` and now unambiguously the diagnostic surface.
24+
Labels ship in all four locales (zh-CN 审批中心).
25+
- `sys_approval_request` is no longer surfaced raw to end users anywhere.
26+
- **Docs**: the approver's queue is documented as the Approvals Inbox, with a snippet for
27+
mounting it in any business app — one navigation entry naming the component-registry key
28+
`approvals:inbox`, never a console path.
29+
30+
Reaching the inbox end to end in the browser additionally requires the console pin bump,
31+
tracked separately.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
"@objectstack/plugin-audit": patch
3+
---
4+
5+
fix(plugin-audit): localize the tracked-change activity label and render lookup titles instead of raw ids (#7230)
6+
7+
`sys_activity.summary` is composed at write time and shipped verbatim to every
8+
feed surface at once — the record discussion feed, console home activity, the
9+
header inbox, the Setup `sys_activity` list, and mobile/REST/SDUI. Its
10+
tracked-change branch (ADR-0052 §5b, `"<label>: <old> → <new>"`) was producing
11+
strings like `Rating Owner: ∅ → oBK25…` at the bottom of an otherwise
12+
fully-localized page. Two independent causes, both fixed here:
13+
14+
- **The label was never localized.** `renderTrackedChangeSummary` was the one
15+
summary branch never handed the locale-bound `translate` its three siblings
16+
(`messages.activityCreated` / `messages.activityDeleted` /
17+
`messages.activityUpdated`, plus the object label via `displayLabelFor`) all
18+
resolve through — an oversight against ADR-0053 / #3039 write-time
19+
localization. The field label now resolves through the same translator, on the
20+
bundles' own key shape (`objects.<object>.fields.<field>.label`), and falls
21+
back to the authored `label`, then the machine key, exactly as before.
22+
- **A reference value printed its raw id.** `displayFieldValue` resolved
23+
select/picklist option labels only, so a `lookup` / `master_detail` / `user`
24+
value fell through to `String(value)` — the stored 32-char id. It now renders
25+
the referenced record's title, resolved through ADR-0079's
26+
`resolveDisplayField` (`nameField` → deprecated `displayNameField` alias →
27+
derivation) rather than a local name-guessing heuristic.
28+
29+
The `∅ →` notation is unchanged, and so is every other summary branch. The
30+
change is restore-invariant: an id that cannot be resolved — a target removed
31+
out of band, an unregistered object, a failing read — renders exactly as it did
32+
before.
33+
34+
**Read cost, measured with a counting driver** (the same technique that measured
35+
#6656 / PR #6977's retirement of the redundant pre-image read from this write
36+
path, and pinned as cases in `audit-lookup-summary.test.ts`):
37+
38+
- **0 added reads** on every create, every delete, every update that moves no
39+
tracked reference field, and every update that moves an *untracked* one —
40+
including on rows that do carry references. #6977's counts (1 `findOne` per
41+
single-id write, 0 per predicate write) are untouched.
42+
- **1 read per distinct target object** on an update that does move a tracked
43+
reference: both sides of the change are answered by a single
44+
`id: { $in: [...] }` selecting only the id and title columns, however many
45+
tracked reference fields point at that object.
46+
47+
Historical `sys_activity` rows keep their original write-time composition — only
48+
new writes improve.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/objectql': patch
4+
'@objectstack/driver-sql': patch
5+
---
6+
7+
refactor(spec,objectql,driver-sql): the autonumber counter readback is one shared pure function, beside the renderer it inverses (#6560)
8+
9+
`packages/spec` gains `readAutonumberCounter(value, prefix, suffix)`, the declared
10+
inverse of `renderAutonumber`, and both consumers call it instead of holding their
11+
own copy.
12+
13+
**Why the inverse belongs where the composition already lives.** `renderAutonumber`
14+
composes `prefix + zero-padded(seq) + suffix` and its file header states it is
15+
"shared by the ObjectQL engine and the SQL driver so both paths render identical
16+
record numbers". PR #6553 (#6468) had to teach both seeding paths to read a counter
17+
back out of a stored value — and landed that reading as two hand-written copies of
18+
the same four lines, one in `packages/objectql`, one in
19+
`packages/drivers/driver-sql`. That is the exact shape of the defect those copies
20+
were fixing: two independent readings of one composition rule had already drifted
21+
into two *different* wrong answers over one dataset (`001-2026` read as `2026` by
22+
the engine and `12026` by the driver), so the record-number band a tenant received
23+
depended on which driver happened to run, and numbers burned that way cannot be
24+
reclaimed. A cross-package `runtime` parity test caught the drift once; it does not
25+
force a future single-side edit to run it.
26+
27+
**What moved and what did not.** Only the ANCHORED rule — the one both sides must
28+
apply identically — is now spec's: the counter is the digit run at the start of
29+
what follows the rendered `prefix`, after stripping the rendered `suffix` when the
30+
value carries it (stripped when it matches, never required to match, since one
31+
counter spans the years a dynamic suffix renders). Out-of-scope values read as
32+
`undefined`, which also gives the SQL driver back its JS-side re-check of a `LIKE`
33+
that matched looser than `startsWith` under a case-insensitive collation.
34+
35+
The UNANCHORED case (neither affix declared) stays per-side, because the two sides
36+
deliberately differ there and #6553 preserved both byte-for-byte: the engine reads
37+
the last digit run, the driver concatenates every digit. Spec returns `undefined`
38+
rather than pick one — a shared contract that claimed an agreement which does not
39+
exist would be worse than no shared contract. Each side documents its own fallback
40+
at its own call site.
41+
42+
**Zero behaviour change.** Every call site keeps its existing guards and its
43+
existing result for every input; the `packages/runtime` cross-side parity suite that
44+
pins the two seeding paths against each other is unmodified and passes as-is, which
45+
is the evidence the semantics moved without changing. Per the maintainer's ruling on
46+
#6560 (2026-08-08, twice, re-confirmed 2026-08-10): a non-authorable export — no
47+
Zod, no new vocabulary, no acceptance-face change — so this is api-surface
48+
bookkeeping plus two call-site swaps.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/plugin-audit": patch
4+
---
5+
6+
docs(spec,plugin-audit): record that the parent-record write gates match ownership at `own` BY DESIGN (#7144)
7+
8+
Documentation only — no gate changes what it returns for any input.
9+
10+
`ISharingService`'s write gates widen ownership "by write DEPTH", but that depth
11+
is an INPUT the caller supplies: the CRUD middleware resolves it for the object
12+
of the operation in flight and stamps it on the operation context. The
13+
`sys_comment` gates (`@objectstack/plugin-audit`) and the `sys_attachment` kit
14+
(`@objectstack/service-storage`) ask this service about the PARENT record's
15+
object, so the stamped depth belongs to a different object and is dropped — and
16+
the owner-match runs at its narrowest, `own`. A caller whose write depth on the
17+
parent is `unit` / `unit_and_below` / `org` can therefore edit that parent
18+
directly and is refused when editing a comment or attachment on it.
19+
20+
That divergence is deliberate and runs in the restrictive direction (refusals,
21+
never a leak). The contract now says so, and — the part that matters for anyone
22+
tempted to "fix" it — says WHY the alternative is not merely unimplemented:
23+
`ISecurityService.resolveWriteScope`, the only tool a package outside
24+
`plugin-security` has for the parent's depth, fails OPEN, because
25+
`getEffectiveScope` returns `'org'` when no permission set mentions the object
26+
at all — indistinguishable from a genuine `modifyAllRecords` holder. Handed to a
27+
write gate as the depth it becomes authoritative on its own and the owner-match
28+
short-exits `true` for every owned row of that object. Inheriting the parent's
29+
edit authority starts with a depth primitive that can tell "org depth" from
30+
"nothing matched", not with these gates.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): `validate-ai-agent-authoring` 新增 `app.defaultAgent` 取值检查(warning 档,#6041)
6+
7+
`app.defaultAgent` 的 Zod 类型是 `SnakeCaseIdentifierSchema`,任何 snake_case
8+
字符串都能 parse、build、通过 `os:check` —— 但运行期只解析平台 agent 名单
9+
(`ask`/`build` 及其历史别名 `data_chat`/`metadata_assistant`,ADR-0063 §2),
10+
表外的名字会静默回落到平台默认值。#5985 实测:把坏例子
11+
`defaultAgent: 'sales_copilot'` 放回语料后,`check:skill-examples` 仍 208
12+
全绿、EXIT=0 —— 现有门禁对这类缺陷结构性失明,这正是坏语料当初得以发布的机制
13+
(语料本身已由 PR #6030 修复)。
14+
15+
本 PR 是 `validate-ai-agent-authoring` 已有规则(此前只扫描 `stack.agents`
16+
数组)的取值半边:遍历 `stack.apps[].defaultAgent`,取值不在
17+
`PLATFORM_AGENT_NAMES`(复用同文件既有名单,未新建重复列表)内即产出一条
18+
`warning` 级 finding(规则 id `default-agent-outside-roster`),消息中点名
19+
实际取值与允许集合。维护者裁定(2026-08-07,2026-08-09 重申)为 **A 档**:
20+
warning 而非 error —— 危害等级是静默回落而非崩溃,且不惩罚存量元数据;
21+
schema 本身不收窄为 enum(ADR-0063 已经撤回过一次 breaking 的收紧)。
22+
23+
落地前已按裁定要求测量现存 in-repo `app.defaultAgent` 取值:仅
24+
`packages/platform-objects/src/apps/studio.app.ts` 一处真实赋值
25+
(`defaultAgent: 'metadata_assistant'`,合法平台别名),对该值实际跑规则
26+
0 条 finding —— "不惩罚存量" 的前提已验证而非假设。
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/metadata": patch
4+
---
5+
6+
fix(spec): `EngineQueryOptionsSchema.search` accepts the bare query string ADR-0061 D1 calls canonical (#7178)
7+
8+
Two sibling schemas in `packages/spec` described the same key and disagreed.
9+
`BaseQuerySchema.search` (`query.zod.ts`, hence `QueryAST`, hence `DriverQuery`)
10+
has been `z.union([z.string(), FullTextSearchSchema])` since its own drift
11+
repair, with a doc comment saying why: the bare string **is** the canonical
12+
Tier-1 contract (ADR-0061 D1 — "the client sends only the query text; the server
13+
resolves which fields to search from object metadata"), it is what every surface
14+
sends, and it is what the dogfood HTTP proof pins.
15+
`EngineQueryOptionsSchema.search` — the options type of `IDataEngine.find` /
16+
`findOne` — declared the structured `FullTextSearchSchema` **only**.
17+
18+
The runtime never agreed with that narrowing. `expandSearchOnAst`
19+
(`objectql/src/engine.ts`) reads `search` through `normalizeSearch`, whose first
20+
line is `if (typeof raw === 'string') return { query: raw }`, and
21+
`protocol-data.test.ts` asserts the protocol layer hands the engine a bare
22+
string. So the type forbade what the engine serves, and callers paid the
23+
standard price: `as any` on the query argument — which does not suppress
24+
`search` alone, it switches off checking for `where` / `orderBy` / `fields` in
25+
the same literal. Since this schema is not `.strict()`, an unknown key there is
26+
**silently dropped**, so the cast this divergence forced was precisely the cast
27+
`check:query-options-erasure` exists to stop.
28+
29+
This is the same-family drift REPAIR, not a new dialect — the identical fix
30+
`BaseQuerySchema.search` already carries, for the identical reason. On the query
31+
side the divergence surfaced as a validation failure the moment #3899 started
32+
validating request bodies; here it surfaced as a type error, when #6231 retyped
33+
`DatabaseLoader`'s read helpers to `DriverQuery` and the **engine** branch alone
34+
refused to compile (TS2345 — `DriverQuery` not assignable to
35+
`EngineQueryOptionsParsed`, purely because of `search`; nothing else differs).
36+
37+
Consumer census before landing, per the card's own guard: every site that reads
38+
object-form members off an engine-options `search` already narrows with `typeof`
39+
`engine.ts` (`typeof raw === 'object' ? raw?.fields : undefined`),
40+
`search-filter.ts` `normalizeSearch`, and `metadata-protocol/protocol.ts`'s
41+
`searchFields` ingress gate. No consumer needed a guard added, and none changes
42+
behavior: they were all written for the union already. `count` is untouched —
43+
`EngineCountOptionsSchema` declares no `search` key at all.
44+
45+
With the schemas agreed, the casts the divergence forced are deleted:
46+
`DatabaseLoader`'s three engine-branch `as any` (`_find` / `_findOne` /
47+
`_count`), which restores real `where` / `orderBy` / `fields` checking on the
48+
metadata main read path, and the seven `as any` in
49+
`engine-findone-contract.test.ts` that were passing the canonical spelling.
50+
`scripts/query-options-erasure-baseline.json` is ratcheted down accordingly.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/core": minor
3+
"@objectstack/runtime": patch
4+
"@objectstack/rest": patch
5+
---
6+
7+
refactor: one shared `ExecutionContext` assembler, two named anonymous entries (#6216)
8+
9+
`resolveAuthzContext` already made AUTHORIZATION resolution single-sourced; the
10+
step after it — turning the resolved envelope into the `ExecutionContext` that
11+
reaches enforcement — was still one hand-written copy per transport, and the
12+
copies drifted twice for real: **#6071** (the REST copy never set
13+
`principalKind`, so every enforcement judgment reading it was silently
14+
never-true on that face) and **#6206 / #6551** (a dropped `accessible_org_ids`
15+
produced real 403s on the share-link faces).
16+
17+
**@objectstack/core** gains the single assembly, with the anonymous divergence
18+
as named API rather than drift (maintainer ruling 2026-08-08 on #6216, Option
19+
A):
20+
21+
- `assembleExecutionContext(input)` — the **fail-closed default** entry. No
22+
resolved principal → `undefined`, and the surface answers 401.
23+
- `assembleExecutionContextOrGuest(input)` — the **explicit guest** entry. No
24+
resolved principal → a first-class guest envelope (`principalKind: 'guest'`,
25+
`positions: ['guest']`), whose consumers are live (`explain-engine`'s
26+
guest ⇒ `EXTERNAL` posture floor). Adopted only by a surface whose product
27+
semantics serve anonymous principals.
28+
- The field set is **closed by type**: `ExecutionContextEntryFields` requires a
29+
decision for every `ExecutionContext` field that is not explicitly declared
30+
non-entry-resolved, so a new field cannot reach one transport and miss
31+
another. Also exported: `ENTRY_EXECUTION_CONTEXT_FIELDS`,
32+
`EntryExecutionContextField`, `ExecutionContextAssemblyInput`,
33+
`OAuthTokenProvenance`, `EntryLocalization`.
34+
35+
**@objectstack/runtime** (`resolveExecutionContext`, the runtime / MCP
36+
dispatcher) and **@objectstack/rest** (`computeExecCtx`) now assemble through
37+
that module — the dispatcher via the guest entry, REST via the fail-closed
38+
default.
39+
40+
**No runtime behaviour change on either surface.** The remaining per-face
41+
divergences are required inputs rather than silent omissions: REST passes
42+
`accessToken: undefined` (it has never carried the session bearer on the
43+
envelope, and `session.accessToken` is a published hook surface) and
44+
`oauth: undefined` (OAuth bearers are honoured on the `/mcp` door alone). The
45+
one measurable difference is that a key whose value was `undefined` is now
46+
omitted rather than spelled — invisible to `ctx.x` reads, to `JSON.stringify`
47+
and to spreading the envelope.

.changeset/hip-planes-shake.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
Unify the approval-status vocabulary across the `sys_approval_request` i18n bundles (#7232).
6+
7+
A request rendered through the generic object surfaces used a different word than the same
8+
request in the Approvals Inbox and in the account-app navigation. The bundles now say what
9+
those surfaces already say:
10+
11+
- **zh-CN**: the `status` option `pending` reads 待审批 (was 待处理), and the `my_pending`
12+
view reads 待我审批 (was 我的待办), matching the account-app nav entry; the view's
13+
empty-state title was aligned to the same wording.
14+
- **en**: the `status` options are humanized — `Pending` / `Approved` / `Rejected` /
15+
`Recalled` / `Returned` — instead of shipping the raw enum values as labels.
16+
- **ja-JP / es-ES**: the `my_pending` view label now matches the nav wording (承認待ち /
17+
Aprobaciones pendientes).
18+
19+
Status **values** are unchanged — this is display wording only, so no stored data, filter,
20+
or API payload is affected.

0 commit comments

Comments
 (0)