Skip to content

Commit 4dbdd32

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-ybjm3h
2 parents a967328 + 912076f commit 4dbdd32

69 files changed

Lines changed: 3757 additions & 404 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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-ui gains an eval for a report's `dateRange` and `exportOptions`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** The skill taught a report's `dateRange` in `rules/dashboards.md`, but no eval checked that an agent authoring a report reaches for `dateRange` on a `type: 'date'` dataset dimension and for `exportOptions` with `csv` / `xlsx`. An agent could hand-roll a `runtimeFilter` window on the date column, invent a report permission key for export, declare a second preset table, or place either key on a `joined` container, and nothing scored that wrong.
11+
- **Now.** A third eval file, `evals/report-period-export.json`, carries one report-authoring prompt whose assertions require `dateRange: { dimension, defaultRange }` and `exportOptions` with `'csv'` / `'xlsx'`, name the existing permission-set `allowExport` as the only export gate, and refuse a `joined` container, a `close_date` filter window, a `presets:` table and an invented permission key. `evals/README.md` lists the file.
12+
- No schema, key or behaviour changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
fix(lint)!: a field group's `visibleWhen` is judged like a field's own `visibleWhen` — in `os validate` / `os build` and at the object save door, refused as `expression-invalid` at the group's key
6+
7+
`fieldGroups[].visibleWhen` gates the whole group, header included, and the form renderer fails it closed: a predicate that cannot evaluate hides the group on every form and record page. No pass of the expression rule read the slot, so a group predicate naming a field the object does not declare (`record.no_such_field == 'x'`) saved with a 200 and the section vanished, while the same text on a field's `visibleWhen` or an action's `visible` was refused 422 at the same door. The object walk now judges every field group's `visibleWhen` with the field `visibleWhen` slot's rules: it must parse, read fields as `record.FIELD` (never bare), name fields the object declares, call only registered functions, and read no root beyond `record`, `previous` and `parent`. The finding is located at `object 'NAME' · field group 'KEY' visibleWhen`, and the root refusal is worded for the group's fail-closed gate.
8+
9+
Clause-②: no (narrowing) — the object save door, `os validate` and `os build` refuse a field group `visibleWhen` they accepted before. Nothing is widened.
10+
11+
Metadata-protocol: narrowed (an object's `fieldGroups[].visibleWhen` may no longer carry a predicate the field `visibleWhen` slot refuses)
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed or renamed: ObjectFieldGroupSchema.visibleWhen keeps its shape and meaning, and the build and the door now give it the verdict a field visibleWhen already gets. A refused predicate never evaluated (the group was hidden on every form), so there is no rewrite that keeps what the author meant; the author fixes the predicate, and the refusal names the field or root. -->
14+
15+
**BREAKING** accept-set narrowing at the object save door (`PUT /api/v1/meta/object/:name`, draft promotion, package draft publish) and in `os validate` / `os build`.
16+
17+
- **Before.** A field group `visibleWhen` with an unknown field, a bare field reference, an unregistered function or a syntax error saved, and the group was hidden on every form and detail page with nothing said. A `current_user` read saved too, and hid the group everywhere except in a form whose host binds the user.
18+
- **Now.** Each answers `422 INVALID_METADATA` at the door (and an `expression-invalid` error at build), located at the group's key.
19+
- **Unchanged.** A group predicate over the object's declared fields (`record.stage == 'won'`, `previous.stage != record.stage`) saves as before, and a group with no `visibleWhen` is untouched. No example, template or platform object in this repository declares a group `visibleWhen`.
20+
21+
**The remedy.** Fix the predicate the refusal names: write `record.FIELD` against a field the object declares. To hide fields by role, declare field-level security on a permission set; to gate on record state, write the predicate against `record`.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql): a file field reads the same whether or not the projection lists `id`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A reader who may not read `sys_file` (every member whose application grants no `sys_file` read) is served a file field by the verdict of the record that owns the file, matched on that record's `id`. A read whose projection left `id` out, such as `GET /api/v1/data/OBJECT?select=FILE_FIELD`, gave that verdict no record key, so a file the reader may read came back as `{ id, metadataRefused: true }`, while `select=id,FILE_FIELD` answered the full `{ id, name, size, mimeType, url }`.
11+
12+
`find` and `findOne` now read `id` for any projection that names a file field and cut it back off the answer, the same way a formula's widened columns are cut. So:
13+
14+
- A permitted reader gets the full file value with or without `id` in `select` / `fields`, on `GET /data/:object`, `POST /data/:object/query` and `GET /data/:object/:id`.
15+
- The answer still carries exactly the columns the caller named: no `id` is added to a row that did not ask for it.
16+
- A reader the owning record's verdict refuses is still answered `{ id, metadataRefused: true }`, with or without `id`.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
feat(plugin-detail,app-shell): the default record page is one page at two widths — resident columns on a wide page, the tabs in a drawer
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The synthesized default record page (no authored page, or a slotted page that keeps the default tab strip) used to be tabbed at every width: on a wide screen the details took the whole width while the timeline, the related lists and the comments each sat behind a tab click. It is now laid out by the width of the view it occupies — never the viewport, so the same page in a drawer stays narrow:
12+
13+
- **1280 px and up:** three resident columns — details · timeline (the discussion composer on top, then Activity and History) · related (each related list as a card naming its first rows, then the opt-in reference rail and `rightRail` slot, Approvals, Attachments and the created / updated line). No tab strip.
14+
- **1024 – 1279 px:** details · timeline; the related column folds into a rail opened from a **Related (N)** toggle above the columns. Closed, its lists read nothing.
15+
- **Below 1024 px, and whenever the view is embedded (drawer / split pane):** the tab strip, exactly as before.
16+
17+
One header (title, actions, highlights, stage path) serves every tier. No metadata key, page template or node type is added: the page description stays the tabbed one Studio seeds, and the columns are read off it by each tab's stable `value`. `?tab=` keeps selecting a tab in the tabbed tier and is ignored by the columns. An authored record page (`kind: 'full'`) and an authored `slots.tabs` strip are drawn as authored at every width. In a column tier History is resident, so its audit read goes out when the page opens instead of when its tab is clicked.
18+
19+
New `@object-ui/plugin-detail` exports for a host wiring the same layout: `useRecordPageTier(element, { embedded, enabled })`, `recordPageTierFor(width)`, `buildDefaultPageColumns(page)`, `recordPageColumnOfTab(value)`, `RecordPageColumns`, the `RECORD_PAGE_THREE_COLUMN_MIN_WIDTH` / `RECORD_PAGE_TWO_COLUMN_MIN_WIDTH` floors and their types. Additive: nothing existing changes shape.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the `allowExport` doc comment no longer says the built-in admin permission sets grant export
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The comment on `allowExport` in `permission.zod.ts` said the built-in admin sets carry the export grant. They do not: `admin_full_access` and `organization_admin` hold the super-user bits but no `allowExport`, so an admin holding only those sets sees no Export control and the export route answers `403 EXPORT_NOT_PERMITTED`. The comment, and the "Permission sets" and "Permissions matrix" doc pages, now say so and name the way to grant export: bind the admin to an app or environment permission set that names each object with `allowRead: true, allowExport: true`. No behaviour changes; the shipped sets are untouched.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-automation says a paused screen's Next / Submit and step count are engine-served, never authored
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** The `screen` node row described the fields an author declares and said nothing about the primary button or the step counter. An agent writing a wizard from the skill could hand-roll a "Submit" label or a "Step 2 of 4" into `fields`, or try to author `progress` on the screen node, and the skill did not say that the runtime answers both from the flow graph.
11+
- **Now.** The `screen` node row says that Next / Submit and "Step N of M" are not authored: when a screen pauses the engine reads the graph and serves `progress` (`nextScreen: 'none' | 'possible' | 'certain'`; `step` / `stepCount` only when every path agrees), and the runner labels the button and draws the counter from that alone. A `decision`, a `fault` edge, a loop back, a non-screen pause (`approval`, `wait`) or a `subflow` after a screen answers `possible`, which shows "Next" with no count. A `progress` key in the node `config` is refused; a hand-rolled label or counter in `fields` is wrong.
12+
- No schema, key or behaviour changes. No eval prompt: `evals/flows-triggers-approvals.json` sits at token-ratchet headroom 0.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-formula evals check that a record name built from a select field reaches for `label(record.kind)`, and that a predicate never calls `label()`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** `skills/objectstack-formula/SKILL.md` teaches `label(record.f)`, but no eval in `evals/cel-predicates-formulas.json` exercised it: an agent authoring a record name from a select field could hand-roll a ternary over the stored value with hard-coded English or Chinese strings, and nothing caught it; nor did anything catch `label()` written into a validation rule, where it faults and aborts every write.
11+
- **Now.** Eval 6 asks for a `contract_version` name built from its `kind` select and expects `label(record.kind)` in an `F` formula, refusing a `== 'clean' ?` ternary. Eval 7 asks for a validation rule and a `visibleWhen` on the same object and refuses `label(` in either, expecting predicates over the stored value. Four sentences in evals 1, 2, 3 and 5 that restated their own `must_not_contain` assertions are gone; every fact and assertion stays.
12+
- No schema, key or behaviour changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/types': patch
3+
'@objectstack/objectql': patch
4+
'@objectstack/core': patch
5+
---
6+
7+
fix(types): a unique refusal tells the user which field by its label, never by its API key
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: none
11+
12+
A create or update refused by a single-column unique index answered `409 UNIQUE_VIOLATION` with a user-facing `error` that named the field by its API key ("A record with this gsku already exists"), while `developerMessage` named it by its label. The split was inverted: the developer read the label and the end user read `gsku`.
13+
14+
- **The user-facing `error` names the field by its label.** For a field labelled `Global SKU`, the body now reads `"error": "A record with this Global SKU already exists"`. `field` still carries the API key (`gsku`) for programs, and `code`, the status and `developerMessage` are unchanged.
15+
- **In the writer's locale.** The label is read the same way the record validator and the `DELETE_RESTRICTED` refusal read theirs: the translation bundle for the request's locale, then the declared `label`. That one label feeds both the user-facing sentence and the engine's own `message` (the `developerMessage` on the wire). A request in `zh-CN` against a translated object reads its labels in `zh-CN` in both. The sentence around the label stays English.
16+
- **No label, no name.** When the field declares no label and the bundle holds none, the sentence is the generic `"A record with this value already exists"`, and `field` still names the key. The same applies to a raw driver unique error that no engine write door wrapped, since it carries no schema to read a label from, and to `sanitizeRowError` (`@objectstack/core`), the import runner's text cleaner. The composite-index answer is unchanged.
17+
- **`DuplicateRecordError` gains `fieldLabel`** (`@objectstack/objectql`): the conflicting field's display name in the writer's locale, set only when the field has one. `envelopeUniqueViolation` takes the write's message context as an optional fourth argument, and every engine write door passes it.
18+
19+
If a client matched the old sentence text to find the field, read `field` instead. It has carried the API key on this body all along.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql)!: a predicate delete (`multi: true` with a filter) honours `deleteBehavior` for every row it matches: a `restrict` refuses, a `master_detail` cascades, a `set_null` clears (#26871)
6+
7+
Clause-②: no (narrowing) — a predicate delete (`engine.delete(object, { where, multi: true })`, which a flow's `delete_record` node with `multi: true` and a filter reaches) refuses with 409 `DELETE_RESTRICTED` where it succeeded: when a row it matches is held by a `restrict` reference (authored, or a required lookup's `set_null`, which is refused as `restrict`). Nothing is widened.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: a declared deleteBehavior is now honoured on the predicate delete path too. The refusal is the existing DELETE_RESTRICTED, so there is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing at the predicate delete (`@objectstack/objectql`), served by every caller of `delete` with `multi: true` and a filter, the flow `delete_record` node among them.
13+
14+
- **Before.** A predicate delete went straight to the driver's bulk delete and ran no referential walk. No `restrict` refused it, the matched rows' `master_detail` children were left behind, and `set_null` lookups kept pointing at deleted rows. The same rows deleted one at a time by id (`DELETE /api/v1/data/:object/:id`, the bulk `deleteManyData` door) refused or cascaded.
15+
- **Now.** When any relation of the delete family (`master_detail`, `lookup`, `tree`) points at the object, the predicate delete reads the rows it matches under the caller's scope, runs the by-id delete's referential walk for each, and then deletes exactly those rows, all in one unit of work on a single datasource. A `restrict` on any matched row refuses the whole delete with the existing `409 DELETE_RESTRICTED`, and nothing is deleted; `master_detail` children (and every `cascade`) are deleted; `set_null` references are cleared. The answer is still the count of rows the delete removed.
16+
- **Unchanged.** An object no relation points at keeps the single bulk delete: no row is read and no transaction is opened. A cascade that reaches another datasource runs without a transaction and warns once, as the by-id delete does. A predicate delete of `sys_organization` is still refused.
17+
- **Who sees it.** A flow (or in-process caller) that bulk-deletes rows held by a `restrict` reference now fails instead of deleting them; one that bulk-deletes masters now deletes their details and clears their lookups instead of leaving them dangling.
18+
19+
**The remedy.** Delete or reassign the restricting rows first, then run the bulk delete. If the referencing rows are meant to go with the rows deleted, declare `deleteBehavior: 'cascade'` on that reference.

‎.claude/skills/pm-dispatch/references/contract-review.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
- 已发布面以包的 `exports` 映射为准:从入口类型图不可达、且 `exports` 不可寻址的 `.d.ts` 是
1919
出货字节,⛔ 不是已发布接受集,改它不触发 `Clause-②: yes`;legacy node10 深引用在契约之外。
2020
- 四面皆不碰 ⇒ CI 加席位自读 ⛔ 不起第二个 agent;谁跑 = 派发席,达档者席内审。
21+
- 派发席达档调用被配额拒(HTTP 429)⇒ ⛔ 不降档审;PR 落交接行、留等复核标记,交分诊席。
22+
- `Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (<HTTP status / error>, <UTC time>) · head <sha>`
2123
- 复核记录 = 一条评论落 PR 或卡,席内与子代理同形;散文、dev 自评、`os-dev-report` 恒不算。
2224
- 同形 = `## Contract review` 题头、head sha 独占码段、①②③ 逐项(申报面外三面加 ④)、
2325
独立性对、PASS/FAIL。
@@ -32,8 +34,6 @@
3234
被引代码读而不审;①–④ 合称完整复核,余面同形无 ④,⛔ 无第二条记录。
3335
- 实质 head 各欠一轮完整复核;只落发现的 commit 由同一复核者 delta 复核;无 must-fix 即止。
3436
- 另记作者:PR `user.login` 非 `objectstack-fleet[bot]` ⇒ ③ 注点名该 login(脱中继写),落地前读。
35-
- 复核形状:只读 diff 与卡片,check 结论取 head 的 check-runs,⛔ 永不本地重跑派生门禁族。
36-
- 只喂卡片、既有裁决与 PR 本体,⛔ 不喂派发令与派发席自己的结论;简报写成对抗性。
3737
- 简报从 `node scripts/pm/record-recognisers.mjs --brief-template` 起稿:
3838
只读条款与输入集内建,⛔ 不删 ⛔ 行。
3939
- 隔离复核子代理暂存全写按所审 PR 命名的 `<scratchpad>/pr-<n>/`,⛔ 不读非本轮自写的暂存。
@@ -51,5 +51,5 @@
5151
- 只作等复核标记,⛔ 不是闸门;单载体 = PR,卡上副本在规则外、不是要求。
5252
- 挂:ACCEPT 时条款②任一肢命中而现 head 无同形 PASS 在案 ⇒ 派发席同笔挂于 PR。
5353
- 摘:现 head 同形 PASS 在案 ⇒ 写记录的席同笔摘;PR 合并或关闭 ⇒ 派发席摘;FAIL 不摘。
54-
- 读者 = 维护者过滤 `is:pr is:open label:needs:contract-review` 与席位巡查、交接。
54+
- 只带标记无交接行者仍归本席,分诊 ⛔ 不碰;交接后 head 再动 ⇒ 新交接行或本席自审。
5555
- ⛔ 无 check、workflow、队列守卫或巡查脚本读它;入队只认同形记录,标记有无不改判。

0 commit comments

Comments
 (0)