Skip to content

Commit 15402d9

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21689-hook-no-body-save-door
2 parents 623b4a0 + 8843505 commit 15402d9

26 files changed

Lines changed: 1805 additions & 77 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
A flow `create_record`, `update_record` or `delete_record` node whose `objectName` is `sys_metadata` or `sys_metadata_history` is refused at parse, with the runtime's prescription: change metadata through the metadata API.
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered flow-write-node-stored-metadata-target-refused -->
10+
11+
**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings.
12+
13+
**Why.** App-authored work may not write the two stored-metadata tables: the metadata protocol is their only writer, where a change is validated and its provenance is recorded, and a flow is app-authored automation. The runtime already enforces that at the node: the three write nodes refuse such a target before they resolve a filter, compute a field or call the data engine, under every run identity. But `FlowSchema` still accepted the flow, so `objectstack validate` passed it, the metadata save door answered 200 for it and `registerFlow` registered it, and the author learned otherwise only at its first run.
14+
15+
**What is refused.** A `create_record`, `update_record` or `delete_record` node, at any depth including an ADR-0031 region body, whose `config.objectName` is a string naming `sys_metadata` or `sys_metadata_history`. The issue's `code` is `custom`, at `nodes.N.config.objectName`, and its message names the node type and the table and ends with the runtime's prescription. The judge is `flowNodeConfigRefusals`, the one `FlowSchema.parse`, `AutomationEngine.registerFlow` (which parses first) and `objectstack validate` share, and its membership test is the kernel's own `isStoredMetadataBodyObject`, the predicate the runtime judges by. That covers `FlowSchema`, `defineFlow()`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `flows.N.nodes.M.config.objectName`), `os validate`, an artifact's parse, `registerFlow` and the metadata save door (`422 INVALID_METADATA`). The refusal joins the closed flow slot refusal set as `write-node-stored-metadata-target`, with `params: { nodeType, objectName }`.
16+
17+
**What stays accepted, byte for byte.** A `get_record` node on those tables (a read is not a write; the runtime judges its reach at the run), a write node whose `objectName` is dynamic (a `{token}` template or an expression envelope: the parse cannot read it as a name, and the runtime judges the name it hands the data engine), and every write node on any other object.
18+
19+
**One prescription sentence.** `@objectstack/spec/kernel` now exports `STORED_METADATA_BODY_PRESCRIPTION`, the sentence the hook refusal and this flow refusal both end on. It was the hook refusal's private constant, moved unchanged.
20+
21+
## FROM → TO
22+
23+
| you wrote | write instead |
24+
|:--|:--|
25+
| a `create_record` / `update_record` / `delete_record` node with `objectName: 'sys_metadata'` or `objectName: 'sys_metadata_history'` | change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`) instead, and delete the node |
26+
| a write node on any other object, a `get_record` node, or a dynamic `objectName` | unchanged |
27+
28+
**The one-line fix: delete the node, or point its `objectName` at the object the flow really means to write, and make the metadata change through the metadata API.** The runtime never ran such a write, so removing it changes nothing a flow does.
29+
30+
**Who is affected, measured.** No authored flow writes either table in this repository's `packages/**`, `examples/**`, `skills/**`, `content/docs/**` or `docs/**` at `417443eb27` (229 write-node declarations); the only hits are the runtime's own tests of its node refusal. Deployed metadata was not measured. Where such a node already sits in a stored flow, the whole flow is refused at registration: at boot it is skipped with a warn naming it, its trigger not armed, while the flows beside it register.
31+
32+
### The kit
33+
34+
- **The refusal.** A third arm of `flowNodeConfigRefusals` (`automation/flow-node-config-refusals.ts`), beside the executor-contract arm and the decision arm.
35+
- **The ledger.** The D3 semantic entry `flow-write-node-stored-metadata-target-refused` (protocol 18). No key is removed, so there is no tombstone, and there is no D2 conversion: a refused node carries no intent a rewrite could keep.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
'@objectstack/objectql': minor
3+
---
4+
5+
fix(objectql)!: a system write's readonly value is judged for its shape — a seed's `'yesterday'` on a readonly datetime is refused with the sentence any other field gets, never stored (#21663)
6+
7+
**BREAKING** — a write that keeps a readonly value now has that value's SHAPE
8+
checked. The static readonly strip still exempts a system write (seed replay,
9+
migration, `isSystem` plugin code, a `before*` hook's stamp) and still drops a
10+
non-system caller's readonly value; what changed is that the value the
11+
exemption keeps is no longer stored unjudged. Before, the record validator
12+
skipped every readonly field, so under `isSystem` a malformed readonly value
13+
reached the driver verbatim — a seed's `run_at: 'yesterday'` on a readonly
14+
`datetime`, an unresolved `cel` envelope from a seeder that skips its
15+
resolution, an authored `created_at` the seed now keeps — while the same value
16+
on a non-readonly field was refused.
17+
18+
Now it is refused the same way: `VALIDATION_FAILED` (400 at an HTTP boundary),
19+
the same field code and the same sentence a non-readonly field gets
20+
(`Run At must be a valid datetime (ISO-8601)`), and a seed counts the row as a
21+
seed error. This holds on insert, on the dry run (`ObjectQL.validate`), and on
22+
both update paths, where the readonly values left after the strip are judged.
23+
24+
Which checks a readonly value reaches — its type's shape, never a constraint:
25+
26+
- refused: a `date` / `datetime` / `time` the platform does not read, a
27+
non-number on a number-typed field, a non-boolean on a boolean, a non-array on
28+
a multi-value field, a filter-operator object, and an ADR-0104 reference /
29+
media / structured-JSON shape under the object's own posture (warn-first, as
30+
on any other field, until the deployment's evidence enforces it);
31+
- NOT checked, exactly as before: option membership, `maxLength` /
32+
`minLength`, `valueDomain`, `min` / `max` / `scale` / `precision`, the email /
33+
url / phone formats, and `required`. Option membership stays out on purpose:
34+
`sys_activity.type` is a readonly `select` whose options are the built-in set
35+
of an open vocabulary, and an author-contributed value there is stored.
36+
37+
A numeric string on a readonly number field is now written as its number, and a
38+
lone scalar on a readonly multi-value field as a one-member list, as on any
39+
other field — the door reads the value the same way it judges it.
40+
41+
**What moves for consumers.** A seed, migration or `isSystem` write that puts a
42+
malformed value in a readonly field — or a hook that stamps one — is refused
43+
where it was stored. Fix the value at its producer: write an ISO-8601 instant
44+
(or a `Date`) into a readonly `datetime`, resolve a `cel` value before the
45+
write, and stamp numbers and booleans as such. Rows already stored are never
46+
re-read or rewritten. `validateRecord`, as exported, is unchanged: the readonly
47+
scope is the engine write path's own.
48+
49+
Clause-②: no (narrowing)
50+
51+
<!-- adr-0087: not-required (no-migration-prescription) a write-time refusal of a malformed value in a readonly field, judged by the same per-type shape checks a non-readonly field already gets. No authorable key, spelling, export or stored shape moves: the field schema is unchanged, the published validateRecord signature is unchanged, no stored row is read or rewritten, and which value a producer meant to write 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 write-path verdict, not a declaration (not runtime-interface-only or type-surface-only). -->
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
The metadata reads' `lock` / `editable` / `deletable` now say what the write doors do with a packaged item
7+
8+
Clause-②: no
9+
10+
Both metadata reads publish the ADR-0010 protection envelope beside the item: `GET /api/v1/meta/:type/:name/layers` (and its deprecated `?layers=true` spelling), and the by-name read `GET /api/v1/meta/:type/:name` where it resolves the envelope. The envelope was resolved from the item's own `_lock` alone, so it ignored the other refusal the write doors apply: an item a code package ships, on a type with no per-org overlay channel, is locked against in-place edits.
11+
12+
**Before.** A packaged flow, action, object, hook, seed, mapping, datasource, external catalog, doc, picklist, field, job, api, capability or agent with no `_lock` read `lock: 'none'`, `editable: true` and `deletable: true`. A packaged page, app, dataset, book, permission set, position, tool or skill read the same. Yet `PUT` refused each of them with `403 NOT_OVERRIDABLE` (or `403 ITEM_LOCKED` when the write names the read-only package), and the removal of the first group was refused too.
13+
14+
**After.** Each read reports what its doors answer:
15+
16+
- The first group reads `lock: 'full'`, `editable: false` and `deletable: false`.
17+
- The second group reads `lock: 'no-overlay'`, `editable: false` and `deletable: true`. Removing a leftover overlay row of these types is allowed: that is the repair path for overlays written before their per-org channel was withdrawn.
18+
- Items of the overlay types (`view`, `dashboard`, `report`, `translation`, `email_template`) are unchanged. So are items no package ships, such as an organization's own flows and actions, and every item while the `OS_METADATA_WRITABLE` operator hatch opens its type.
19+
20+
An item's own `_lock` still applies on top: the two refusals join, and neither replaces the other. `lockReason`, `lockSource` and `lockDocsUrl` are still present only when the item declares them. `provenance` and `packageId` already name the package.
21+
22+
The verdict is the one the write doors already share, read rather than re-derived, so the read moves whenever a door moves. The `lock` field's description in `@objectstack/spec` now names both refusals it reports. No key, type or accepted value changes.
23+
24+
**What to do.** Nothing, unless a client gated an edit or delete affordance on `editable` / `deletable`: it now hides that affordance for packaged items the server refuses, instead of offering a write that answers 403. The refusal itself names the sanctioned route for each type: for a packaged flow, clone it under a new name or switch it off; for a packaged action, switch it off.

‎content/docs/references/api/protocol.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1344,7 +1344,7 @@ Enable package response
13441344
| **name** | `string` | ✅ | Item name |
13451345
| **item** | `any` | ✅ | Metadata item definition |
13461346
| **sortability** | `{ fields: Record<string, object> }` | optional | Per-column sortability projection — present exactly when `type` is `object`, on every serving branch. Computed at serve time from the served document via the spec's own storage predicates; consumers render sort affordances from this signal and never re-derive it from field `type`. See `ObjectSortabilitySchema` for the closed category set. |
1347-
| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Resolved lock verdict for this item (ADR-0010 §3.3). `none` means unlocked; `no-overlay` / `no-delete` / `full` refuse the corresponding write with 403 `ITEM_LOCKED`. Resolved from the document's `_lock`, with the packaged artifact winning over any org overlay. |
1347+
| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Resolved lock verdict for this item (ADR-0010 §3.3). `none` means unlocked; `no-overlay` / `no-delete` / `full` mean the write doors refuse the corresponding write with 403. Joins two refusals: the document's own `_lock` (`ITEM_LOCKED`; the packaged artifact wins over any org overlay), and the locked packaged base — an item a code package ships, on a type with no per-org overlay channel (`NOT_OVERRIDABLE`, or `ITEM_LOCKED` when the write names the read-only package). |
13481348
| **lockReason** | `string` | optional | Human-readable explanation shown next to a refused write. Present only when the resolved item declares `_lockReason`. |
13491349
| **lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Which layer asserted the lock. Present only when the resolved item declares `_lockSource`. |
13501350
| **lockDocsUrl** | `string` | optional | Documentation link surfaced beside `lockReason`. Present only when the resolved item declares `_lockDocsUrl`. |

0 commit comments

Comments
 (0)