Skip to content

Commit 2d8b92f

Browse files
os-zhuangos-project-managerclaude
authored
fix(metadata-protocol): the destructive gate's reachable type set is object alone (#11014) (#11170)
`saveMetaItem`'s Phase 3a-destructive gate opened on `(singularType === 'object' || singularType === 'field')` and the `field` limb could not produce a finding, so the condition made the gate's coverage read wider than it is — #10886's face inventory had to chase a `field` face population that does not exist. Both reasons the limb was inert were re-measured through the real `saveMetaItem` before the deletion: 1. a `field` body has no `fields` map to diff (`FieldSchema` is a `strictObject` declaring no such key, so both sides of the detector fold to `{}`); measured as a `text` -> `number` change saving with no 409 while the same-shaped change on an `object` refused in the same harness; 2. `field` is code-only, so the #5086 refusal one gate up answers first — `NOT_CREATABLE` for a runtime-only parent, `NOT_OVERRIDABLE` for an artifact-backed one — before persistence and before the diff. Neither reason is absolute and the trim is right because of where they stop: reason 2 stops at the documented `OS_METADATA_WRITABLE` hatch (reason 1 then holds the gate inert on its own), and reason 1 stops at schema-valid bodies — a stored `field` row carrying a `fields` map did fire the gate. That double fault is the one behaviour delta, and the refusal it removes was a false alarm: a `field` write mints a standalone `sys_metadata` row nothing composes into its parent object (#7893), so no driver ever materialised the named columns. `object` behaviour is untouched. The reachable type set is now pinned by `protocol.destructive-gate-reachable-types.test.ts`, which pairs every "no 409" assertion with a live `object` control. Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y Co-authored-by: Claude <pm@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent be7262e commit 2d8b92f

5 files changed

Lines changed: 591 additions & 4 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
Trim the dead `field` limb from `saveMetaItem`'s Phase 3a-destructive gate: the
6+
reachable type set is `object` alone (#11014). The condition read
7+
`(singularType === 'object' || singularType === 'field')` and the `field` half
8+
could not produce a finding, so the gate's coverage *read* wider than it is —
9+
#10886's face inventory had to establish per face which types reach this gate,
10+
and the `field` spelling is the one thing that made the answer look bigger.
11+
12+
Both reasons the limb was inert were re-measured through the real `saveMetaItem`
13+
before the deletion, and both hold:
14+
15+
1. **A `field` body has no `fields` map to diff.** The detector reads
16+
`prev.fields` / `next.fields`; a `field` item's body IS one field definition
17+
(`FieldSchema`, a `strictObject` declaring no `fields` key), so both sides
18+
fold to `{}`. Measured: a `text``number` change on a stored `field` row —
19+
the exact edit that raises `field_type_change` inside an object body — saved
20+
with no 409, while the same-shaped change on an `object` refused in the same
21+
harness.
22+
2. **`field` is code-only** (`allowRuntimeCreate: false` and
23+
`allowOrgOverride: false` in the kernel registry), so the #5086 refusal one
24+
gate up answers first — `NOT_CREATABLE` for a runtime-only parent,
25+
`NOT_OVERRIDABLE` for an artifact-backed one — before persistence and before
26+
the diff.
27+
28+
Neither reason is absolute, and the trim is right because of where they stop.
29+
Reason 2 stops at the documented operator hatch (`OS_METADATA_WRITABLE=field`),
30+
which does carry a `field` write into the gate — reason 1 then holds it inert on
31+
its own. Reason 1 stops at schema-valid bodies: the detector is type-agnostic, so
32+
a *stored* `field` row carrying a `fields` map did fire the gate. That is the one
33+
behaviour delta: such a save now succeeds instead of answering `409
34+
DESTRUCTIVE_CHANGE`. It needed two faults at once (hatch open, plus a body
35+
`FieldSchema` rejects with `unrecognized_keys: ['fields']`), and the refusal it
36+
removed was a false alarm — a `field` write mints a standalone `sys_metadata` row
37+
nothing composes into its parent object (#7893), so no driver ever materialised
38+
the columns the finding named.
39+
40+
`object` behaviour is untouched: same predicate, same findings, same `409`
41+
envelope, same remedy clause. The reachable type set is now pinned by
42+
`protocol.destructive-gate-reachable-types.test.ts`, which carries the
43+
measurements above and pairs every "no 409" assertion with a live `object`
44+
control, so the limb cannot grow back on the reading it was deleted for.

packages/metadata-protocol/src/protocol.destructive-409-face-inventory.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,15 @@
3030
* gate at all, and if so what does its catch emit.
3131
*
3232
* The gate fires only when ALL of: `!request.force`, the folded type is
33-
* `object` or `field`, an item already exists under the target name, and the
34-
* diff is non-empty. That predicate is what eliminates four of the seven.
33+
* `object`, an item already exists under the target name, and the diff is
34+
* non-empty. That predicate is what eliminates four of the seven.
35+
*
36+
* ⚠️ [#11014] That type list read `object` or `field` while this inventory was
37+
* being built, and the `field` half could not produce a finding — so the
38+
* enumeration above had to chase a `field` face population that does not
39+
* exist. The limb is now trimmed; the reachable type set is `object` alone and
40+
* is pinned by `protocol.destructive-gate-reachable-types.test.ts`, which also
41+
* carries the measurements for both reasons the limb was inert.
3542
*
3643
* | # | caller | type | `force` | reaches gate | face | `issues` structurally |
3744
* |:--|:--|:--|:--|:--|:--|:--|

0 commit comments

Comments
 (0)