Skip to content

Commit d2cc0dd

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-7127-default-value-discriminator
# Conflicts: # content/docs/protocol/objectql/schema.mdx
2 parents e2417fe + dadf67a commit d2cc0dd

172 files changed

Lines changed: 11839 additions & 1530 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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
refactor(spec)!: retire `global_nav` from `ACTION_LOCATIONS` — a location the product never rendered, and the designer previewed anyway (#6888, ADR-0049)
6+
7+
`ACTION_LOCATIONS` is the canonical vocabulary for where an action surfaces in a
8+
running app, and `global_nav` — "global navigation/command-palette level
9+
actions" — has been in it since the vocabulary was written. **No running-app
10+
surface ever rendered it.** The console's ⌘K palette
11+
(`app-shell/src/chrome/CommandPalette.tsx`) builds its groups from nav items,
12+
objects, dashboards, pages, reports, recent items, record search and theme; its
13+
`actions` group is hard-coded chrome; and the file references neither
14+
`global_nav` nor any action-metadata source at all. Of the five references to
15+
the value in the whole UI repo at the vendored SHA, four were the Studio
16+
designer and the fifth a doc comment.
17+
18+
What lifts this above ordinary inert-declaration cleanup is the direction of the
19+
lie. `metadata-admin/previews/ActionPreview.tsx` drew the author a mock
20+
`⌘K · Command palette` frame, so the **authoring tool promised a surface the
21+
product does not have**. An author declares the location, watches it "render" in
22+
the designer, ships it, and it reaches no user — the ADR-0078
23+
declares/renders/does-nothing shape, arriving through the location vocabulary
24+
rather than through a missing key. For an AI author reading the corpus (ADR-0033)
25+
that preview is evidence the capability exists, which is exactly how dead
26+
metadata multiplies.
27+
28+
Retired rather than implemented (maintainer ruling, 2026-08-09): no user has
29+
asked for command-palette actions, and the only two declarers were our own
30+
showcase corpus. Wiring the palette would have been capability expansion with no
31+
pull. If real appetite appears it re-enters through the front door,
32+
implementation first.
33+
34+
FROM → TO:
35+
36+
| Was | Now |
37+
|:--|:--|
38+
| `locations: ['global_nav', 'record_header']` | `locations: ['record_header']` — drop the value, keep the served locations |
39+
| `locations: ['global_nav']` on an action with a UI home | place it where a renderer serves it: `list_toolbar`, `list_item`, `record_header`, `record_more`, `record_related`, `record_section` |
40+
| `locations: ['global_nav']` on an action with no UI home (e.g. object-less, invoked over REST/MCP/AI) | `locations: []` — the documented **headless** declaration, which keeps the capability gate, param contract and audit trail |
41+
42+
The retirement kit:
43+
44+
- This is an enum **VALUE** retirement, so there is no `retiredKey()` tombstone:
45+
the enum's own error map carries the prescription, keyed on the received value
46+
so only the spelling that used to be legal is told it "was removed" (the
47+
`crypto.hash` / `HookBodyCapability` precedent, #4391, and `array_agg` /
48+
`AggregationFunction`, #6188). A mis-spelling still gets zod's list of the
49+
legal locations. For the same reason nothing lands in `RETIRED_KEYS_BY_MAJOR`
50+
and the four surface ratchets are byte-identical — no def and no authorable
51+
key changed.
52+
- **ADR-0087 D2 conversion + D3 chain step**
53+
(`action-global-nav-location-removed`): `os migrate meta --from 16` strips the
54+
value from `action.locations`, one notice per rewritten action.
55+
- **The key is kept when the array empties**`locations: []`, never
56+
`delete locations`. On this surface the two are different declarations, not
57+
two spellings of one: the empty array is the documented headless shape
58+
("Headless actions: declare it, then hide it"), while an absent key means
59+
nobody placed the action — which is what `packages/lint`'s
60+
`action-no-placement` warns about, in those words ("an author who said
61+
'nowhere, deliberately' (`[]`) and one who never said anything at all").
62+
Dropping the key would convert a deliberate placement into a lint finding and
63+
discard the author's own statement of intent. This is the
64+
`hook-body-crypto-hash-removed` shape ("the `capabilities` key itself stays —
65+
an empty grant set is legal"), not the
66+
`dataset-measure-array-string-agg-removed` shape, which drops its item only
67+
because the stripped remainder would fail the dataset's own refinement.
68+
- The QA platform checklist's `records-forms.action-location-matrix` loses its
69+
`global_nav` variant. That variant was **unrunnable**, not merely obsolete:
70+
its step "new_task from the palette" could never pass. `enumSource.expect`
71+
moves 7 → 6, which is the ratchet that would otherwise have caught this drift.
72+
- The two showcase declarers become headless. `showcase_portfolio_snapshot` is
73+
object-less by design and its docblock already said `global_nav` was chosen
74+
"for the same reason" it has no `objectName` — so `[]` is the declaration it
75+
always meant.
76+
77+
**Behaviour that changes:** none at runtime. An action declaring `global_nav`
78+
rendered nowhere before this change and renders nowhere after it; what changes is
79+
that the declaration is now refused at parse, with the reason, instead of being
80+
accepted and silently ignored.
81+
82+
<!-- adr-0087: registered action-global-nav-location-removed -->
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): the object-form `params` refusal prescribes per action type — `bodyExtra` for `api`, `target` interpolation + `openIn` for `url` (#6828)
6+
7+
`params` has always been `z.array(ActionParamSchema)`, so writing it as an object
8+
has always been refused. What changed in #5777 is the *message*: it stopped being
9+
the unactionable "expected array, received object" and started naming
10+
`bodyExtra`, the key the maintainer's 2026-08-06 ruling created for a
11+
`type:'api'` action's static request body.
12+
13+
That prescription is right for exactly one action type. On a `type:'url'` action
14+
the object form meant something else entirely — objectui's `ActionRunner` read a
15+
non-array `params` as the `${param.X}` interpolation scope for `target`, and
16+
`params.newTab` as a legacy new-tab flag. Telling that author to use `bodyExtra`
17+
sent them to an api request-body key that is neither an interpolation scope nor a
18+
new-tab control. (The same asymmetry is why the
19+
`inline-action-api-params-to-body-extra` conversion guards on `type === 'api'`:
20+
rewriting a url action's object `params` into `bodyExtra` would be lossy, and
21+
ADR-0087 D2 requires losslessness.)
22+
23+
The maintainer's 2026-08-10 ruling on #6828 **retired the url meaning** rather
24+
than giving it a key — a key with three meanings and no authorized spelling for
25+
the third is the de-facto-contract shape AGENTS.md #0.1 forbids, the schema
26+
already refuses it, and nothing in the reachable corpus authors it. Both halves
27+
already have sanctioned spellings:
28+
29+
| Retired reading | Sanctioned spelling |
30+
|:---|:---|
31+
| statically authored `${param.X}` scope | put the value in the `target` string itself (`${param.X}` interpolates what the params **dialog** collected; `${ctx.X}` the action context) |
32+
| `params.newTab` | `openIn: 'new-tab'` (declared, and already read with priority by the runner) |
33+
34+
So the refusal message now carries both arms, and the authoring docs
35+
(`ui/actions`, `protocol/objectui/actions`) state the refusal where inline and
36+
url actions are described.
37+
38+
**No acceptance-face movement**: `params` is still `z.array(ActionParamSchema)`,
39+
the object form is still refused with `invalid_type` at path `params`, and the
40+
array form still parses on every action type. This is a message-and-docs change —
41+
hence `patch` — pinned on both arms and on both the inline and registered
42+
surfaces.
43+
44+
The two objectui reads this ruling makes dead vocabulary (`interpolateTarget`'s
45+
non-array `params` scope, and the `params.newTab` escape hatch) are objectui's
46+
card, filed contract-first behind this one.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/objectql": minor
3+
"@objectstack/metadata-protocol": minor
4+
"@objectstack/spec": minor
5+
---
6+
7+
ADR-0029 D9: a tenant object overlay registers as its own contributor LAYER instead of splicing the packaged owner out
8+
9+
租户对 `object` 的定制(`sys_metadata` 行)此前以默认的 `own` 身份进入 `SchemaRegistry`。当该行的 `package_id` 与代码包所有者相同时,`registerObject` 会走"重复注册"分支把**打包的 contributor 直接摘掉**——打包定义不是被遮蔽,而是在写入时被销毁,注册表里不存在第二份副本;`loadMetaFromDb` 每次启动都无声重放这次销毁。
10+
11+
D9 把这个层次关系显式化:
12+
13+
- **第三种非拥有的 contributor 种类 `overlay`**,对基础层是替换语义。解析变成 `base = overlay ?? own`,extender 照旧叠在上面。**解析结果逐字节不变**(含 `_provenance: 'org'`)——变的只是注册表"记得"什么:打包的 owner 依然在下面。
14+
- `assertSingleOwnerPerObject` **一字未改**(overlay 不是 owner),新增一类违规:孤儿 overlay(有 overlay 没有 owner)。
15+
- **基础层的选择问"种类",永远不问优先级**`DEFAULT_OVERLAY_PRIORITY = 150` 只用于列举顺序:extender 的优先级是作者声明的,不能让某个包用 `priority: 140` 把租户的 overlay 挤出基础层。
16+
- **artifact 身份改为读 owner contributor 的层**,而不是合并后的文档。这一条不是层次化改动的自然推论:合并结果按设计仍带 `_provenance: 'org'`,所以只有从 owner 层读,`isArtifactBacked` 才不再说谎。
17+
- `provisionPrimary` / `provisionSearchCompanion` 的门从"是不是 `own`"改成"**是不是基础层**",否则每个被 overlay 的对象的 `nameField` 都会变。
18+
- 行上的 `package_id` 是层的**来源标记**,从来不是所有权主张:同包正常;**无包(`sys_metadata` 哨兵)予以接受**(此前的抛错是借用 `own` 槽位的副产品);绑定到**其他包**的行在生产者侧被明确拒绝,新错误码 `OBJECT_OVERLAY_PACKAGE_MISMATCH`(422),启动时计入 `loadMetaFromDb``errors`
19+
- **迟到安装**:代码包为一个租户行已占据的对象名注册时,代码层成为 owner,租户的贡献被重新归类为它的 overlay 层——不再抛 "already owned by",也不再把租户的定制吞掉。
20+
- 删除退化为**减法**`SchemaRegistry.removeObjectOverlay(name)` 只摘掉 overlay 层,打包 owner 原地不动,因此"恢复"根本不是一次重新注册。
21+
22+
**行为变化(记录在案的成本)**:谓词诚实之后,`object` 声明的 `allowOrgOverride: false` 会被**一致地**执行——对打包对象的 overlay 写入**每次**都以 `NOT_OVERRIDABLE` 拒绝,而不是只拒第一次(此前第一次被拒、并因销毁证据而让后续每次都从 `allowRuntimeCreate` 那一档混过去)。同一谓词也喂给 `deleteMetaItem` 的两档鉴权与仓库的 `assertAllowed`,所以重置该定制同样需要那道文档化的运维口子 `OS_METADATA_WRITABLE=object`——现在它必须在定制的**整个生命周期**内保持打开,而不只是第一次保存时。
23+
24+
`ObjectContributor.ownership``ObjectOwnershipEnum` 的联合类型因此加宽(loader 设定,永不可由作者书写),这是 `objectui` / `cloud` 消费方可见的公开类型变化。
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/plugin-audit": patch
3+
---
4+
5+
fix(plugin-audit): localize select option labels in the tracked-change activity summary (#7289)
6+
7+
`sys_activity.summary` is composed at **write time** and shipped verbatim to
8+
every consumer at once — the record discussion feed, console home activity, the
9+
header inbox, the Setup `sys_activity` list, and mobile/REST/SDUI.
10+
`displayFieldValue` rendered a select/picklist value by scanning `field.options[]`
11+
and returning the matching option's **authored** `label`. `field.options` comes
12+
from `engine.getSchema(name)`, which is locale-independent metadata, while the
13+
shipped bundles carry those same labels under
14+
`objects.<object>.fields.<field>.options.<value>` (`sys_audit_log.fields.action.options.create = "创建"`).
15+
Nothing on this path read them.
16+
17+
After #7230 localized the field label, that left a zh-CN workspace with
18+
19+
```
20+
阶段: Proposal → Closed Won
21+
```
22+
23+
— a half-localized string at the bottom of a fully-localized page. The tracked-change
24+
branch now resolves the option label through the same locale-bound translator its
25+
field label already uses, on the bundles' own key shape, with the authored label as
26+
the fallback. A bundle miss returns `undefined`, so the authored label and then
27+
`String(value)` answer exactly as before: the change can only replace an authored
28+
label with that label's translation, never the reverse.
29+
30+
**The fired-milestone branch is deliberately left alone**, and the opt-out is by
31+
construction rather than by omission — `renderMilestoneSummary` passes no option
32+
resolver, so a select token there still renders its authored label byte-for-byte.
33+
A milestone summary is an author-written sentence with no bundle key of its own,
34+
and #7290 ruled leaving templates untranslated a contract decision. #7290's own
35+
change (a reference id → the referenced record's title) is locale-*independent*
36+
data — the same string in every locale — which is why it could be added to an
37+
untranslated sentence; an option label is locale-*dependent* rendering, so reading
38+
the bundle there would guarantee a split sentence (`Deal moved to 已赢单`) in
39+
exactly the case the bundle exists for. The tracked-change branch has the opposite
40+
geometry: its frame is fully localized, so there the authored value is the mismatch.
41+
42+
**Read cost is unchanged.** This is a bundle lookup, not I/O: zero added reads on
43+
every write shape, so the #6656 / PR #6977 retirement (2 → 1 reads per single-id
44+
write, 3 → 0 per predicate write) that #7291 and #7333 preserved still stands,
45+
and `displayFieldValue` stays synchronous.
46+
47+
Historical rows keep their write-time composition; only new writes improve.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
`GET /api/v1/automation/:name/runs` now refuses a malformed query parameter with a
6+
proper ADR-0112 refusal (`400`, `error.code: VALIDATION_FAILED`, `details.fields[]`
7+
naming the parameter with an ADR-0114 field code) instead of coercing it into a
8+
value nobody asked for — the same gate `GET /api/v1/notifications` grew in the
9+
previous release, now shared between the two routes rather than copied.
10+
11+
Wire-visible for raw-HTTP callers only — the typed SDK's `limit` is a `number` and
12+
its `cursor` a `string`, so neither could produce these:
13+
14+
- `?limit=abc` coerced to `NaN`, which nothing downstream catches: the automation
15+
engine's `options?.limit ?? 20` does not catch NaN (`??` tests for null/undefined
16+
only) and its final `.slice(0, NaN)` is `[]`. So a typo in the window answered
17+
**200 with an empty run list** — "this flow has never run", stated confidently
18+
about a flow with runs. Non-integers (`1.5`, `Infinity`, `10abc`, a repeated
19+
`?limit=1&limit=2`) are refused on the same rule.
20+
- `cursor` was forwarded raw into a slot the contract types `cursor?: string`
21+
(`IAutomationService.listRuns`), so a repeated `?cursor=a&cursor=b` handed an
22+
array to a service that had declared it would receive a string. The shipped
23+
engine ignores the option entirely today, which is why the boundary is the right
24+
place to close it: the first implementation that starts honouring cursors must
25+
not be the one that discovers the type was never enforced.
26+
27+
Unchanged on purpose: every value that already had a defensible answer keeps it,
28+
byte for byte — out-of-range numbers (`?limit=1000`, `?limit=-5`) still reach the
29+
engine untouched, because range is its declared business (`ListRunsRequestSchema`
30+
bounds it and the engine slices by it), absent/empty parameters still mean "no
31+
limit", any string cursor still passes through verbatim, and unknown query keys are
32+
still ignored rather than refused.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
"@objectstack/driver-sql": patch
3+
---
4+
5+
refactor(driver-sql): read the autonumber default from the contract instead of a hardcoded fallback (#7263)
6+
7+
Execution half 3/3 of the maintainer's route-3 ruling on #6555. `{0000}` is now a
8+
declared contract default (`DEFAULT_AUTONUMBER_FORMAT`, landed with
9+
`resolveAutonumberFormat` in `@objectstack/spec/data`), so this driver stops
10+
writing the default down for itself.
11+
12+
Two sites in `sql-driver.ts``initObjects` and the external-object
13+
registration path — each spelled the same four lines by hand:
14+
15+
```ts
16+
const rawFmt = (typeof field.autonumberFormat === 'string' && field.autonumberFormat)
17+
? field.autonumberFormat
18+
: (typeof field.format === 'string' && field.format ? field.format : '');
19+
const fmt = rawFmt || '{0000}';
20+
```
21+
22+
Both are now `const fmt = resolveAutonumberFormat(field);`. That is the whole
23+
change: one symbol added to an import this file already had, no new dependency,
24+
and the `#1603` comment about honouring both spellings retired to the resolver's
25+
own docstring, which carries it.
26+
27+
**Behaviour-neutral, by construction and by measurement.** `resolveAutonumberFormat`'s
28+
precedence — canonical `autonumberFormat`, then the `format` shorthand, then the
29+
declared default, with anything that is not a **non-empty string** counting as
30+
undeclared — was deliberately taken from these very lines, including their
31+
truthiness rule (not the engine's `??`). A differential check over 484 field
32+
documents, spanning both spellings across 22 value shapes (absent key,
33+
`undefined`, `null`, `''`, non-empty strings, numbers, booleans, `NaN`, arrays,
34+
objects, a boxed `String`, `Symbol`, function, `BigInt`), found the old
35+
expressions and the resolver returning the identical string in every case —
36+
`format: ''`, `autonumberFormat: ''` and the non-string values included, not just
37+
the happy path.
38+
39+
Compatibility note, per the ruling: choosing {0000} keeps stored driver-sql data
40+
undisturbed; engine-fallback deployments flip from bare 1 to 0001 for newly
41+
issued numbers. Counter continuity itself is unaffected (#6468 pinned it).
42+
43+
The engine half of the same ruling is #7262; #6555 stays open until it lands, so
44+
a format-less field still renders `0001` on SQL and a bare `1` on the engine's
45+
in-memory fallback until then. This half moves neither.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/metadata-core": minor
3+
"@objectstack/rest": patch
4+
"@objectstack/runtime": patch
5+
---
6+
7+
Metadata-plane FLS: the ADR-0106 D4 read exemption is now **derived** from the #6603 write-capability gate, so "whoever can write a schema can see all of it" is enforced by construction (#7020).
8+
9+
The two sets used to be maintained separately and were in fact different: the write gate demands `manage_metadata`, while the D4 exemption listed `studio.access` / `setup.access`. They met only on the shipped `admin_full_access` set, which carries all three — so the invariant #6603's ruling stated held by coincidence, not by construction. A caller holding `manage_metadata` alone passed every metadata write gate and still read a **masked** object schema, and its GET, edit and PUT round trip then deleted the fields it was never shown.
10+
11+
`OBJECT_SCHEMA_MASK_EXEMPT_CAPABILITIES` is now the union of two named halves — `OBJECT_SCHEMA_WRITE_CAPABILITIES` (the write gate's key, spelled once) and `OBJECT_SCHEMA_READ_ONLY_EXEMPT_CAPABILITIES` (`studio.access` / `setup.access`) — both newly exported from `@objectstack/metadata-core`.
12+
13+
**Behaviour change:** a caller holding `manage_metadata` now reads object schemas unmasked on every schema-serving exit. This widens read access for that cohort and is the ruled intent (maintainer, 2026-08-10). The derivation is one-directional: no principal loses read access, and the `/packages` read cohort (#7033 / #7023) keeps its own separately-ruled set.

0 commit comments

Comments
 (0)