Skip to content

Commit 85abb2e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22957-tree-delete-behavior
2 parents 5ce34e0 + 38d98d8 commit 85abb2e

4 files changed

Lines changed: 35 additions & 18 deletions

‎docs/adr/0005-metadata-customization-overlay.md‎

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
> **Protocol 18 update (2026-10-12, ADR-0131 C5, #22340):** The per-type overlay opt-in on `MetadataTypeRegistryEntry` is spelled `allowEnvironmentOverlay` from protocol 18 (`packages/spec/src/kernel/metadata-plugin.zod.ts#allowEnvironmentOverlay`). [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6 retired the per-organization overlay axis, so the key now names the one scope it governs — whether an environment may overlay an item of this type that a managed package ships. The old key `allowOrgOverride` stays on the registry entry schema only as a tombstone that refuses at parse with the new name in its prescription: no second key, no alias, no conversion. The lines below that name `allowOrgOverride` describe the field as it was spelled when they were written and are preserved verbatim.
66
77

8-
**Status**: Accepted (2026-05-16) · **Amended** (2026-05-22, see "Amendment: post-ADR-0006 v4 scope") · **Amended** (2026-04-13, branch concept removed — see [ADR-0008 §0](./0008-metadata-repository-and-change-log.md#0-2026-04-13-amendment--drop-project-and-branch-from-metaref)) · **Amended** (2026-08-09, #6825 — the Phase-1 overlay-index migration is deleted; see "Amendment (2026-08-09, #6825): overlay-index delivery after the Phase-1 migration was deleted") · **Amended** (2026-09-04, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6 — the per-organization overlay axis is **retired for now**: `sys_metadata` carries no organization column, org-scoped writes of the five tier-A types are refused, and the environment layer is the whole ledger; the environment → code layered resolution and the "no overlay row = the registry" reading are unchanged) · **Amended** (2026-09-30, #20051 ruling 甲 — in "Addendum — 2026-05-16 (c)", which code cites as appendix (c), the persisted document is the **parsed body** once every round-trip key is declared, reversing the verbatim `request.item` save; a stored `view` row's round-trip keys are declared, and the storage switch itself, the ruling's last stage, is in force for `view` since PR #20868 (`9905e61ca2`) — see the amendment notes under that addendum's persisted-document bullet) · **Amended** (2026-09-30, #20051 ruling B — the parsed body a `view` stores is the parsed value of every key its request body carried, not the whole parse output: undeclared keys dropped, schema defaults not materialised, a moved key stored under its canonical spelling; every other type still stores its request body — see the second amendment note under that bullet)
8+
**Status**: Accepted (2026-05-16) · **Amended** (2026-05-22, see "Amendment: post-ADR-0006 v4 scope") · **Amended** (2026-04-13, branch concept removed — see [ADR-0008 §0](./0008-metadata-repository-and-change-log.md#0-2026-04-13-amendment--drop-project-and-branch-from-metaref)) · **Amended** (2026-08-09, #6825 — the Phase-1 overlay-index migration is deleted; see "Amendment (2026-08-09, #6825): overlay-index delivery after the Phase-1 migration was deleted") · **Amended** (2026-09-04, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6 — the per-organization overlay axis is **retired for now**: `sys_metadata` carries no organization column, org-scoped writes of the five tier-A types are refused, and the environment layer is the whole ledger; the environment → code layered resolution and the "no overlay row = the registry" reading are unchanged) · **Amended** (2026-09-30, #20051 ruling 甲 — in "Addendum — 2026-05-16 (c)", which code cites as appendix (c), the persisted document is the **parsed body** once every round-trip key is declared, reversing the verbatim `request.item` save; a stored `view` row's round-trip keys are declared, and the storage switch itself, the ruling's last stage, is in force for `view` since PR #20868 (`9905e61ca2`) — see the amendment notes under that addendum's persisted-document bullet) · **Amended** (2026-09-30, #20051 ruling B — the parsed body a `view` stores is the parsed value of every key its request body carried, not the whole parse output: undeclared keys dropped, schema defaults not materialised, a moved key stored under its canonical spelling; every other type still stores its request body — see the second amendment note under that bullet) · **Amended** (2026-10-12, #22877 ruling A — the overlay uniqueness key gains a fifth part, `bundle_member`: the member of a bundled type, such as an `email_template`'s locale, so two members of one item are two overlay rows; NULL is the canonical member, so every row of a type whose identity is `name` alone is unchanged. In force since `5677add93f` (PR #22954) — see "What delivers overlay uniqueness today")
99
**Deciders**: ObjectStack Protocol Architects
1010
**Builds on**: [ADR-0003](./0003-package-as-first-class-citizen.md) (Package as first-class citizen), [ADR-0004](./0004-cloud-multi-kernel.md) (Cloud + per-project kernels)
1111
**Amended by**: [ADR-0006 v4](./0006-project-environment-split.v4.md) (drops `sys_project` entirely), [ADR-0008](./0008-metadata-repository-and-change-log.md) (re-expresses overlay as `LayeredRepository`; subsequently drops `project`/`branch` from `MetaRef`), [ADR-0029 D9](./0029-kernel-object-ownership-and-platform-objects-decomposition.md#amendment-2026-08-09-6853-a-tenant-overlay-of-an-object-is-its-own-contributor-layer-not-a-second-own) (for `object` only: the overlay is a registry contributor LAYER over the packaged owner, resolved as `base = overlay ?? own`, instead of a destructive in-place overwrite), [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) C5 / D6 (the per-organization overlay axis is retired and the per-type opt-in is spelled `allowEnvironmentOverlay` from protocol 18 — the note at the top of this record)
@@ -838,6 +838,10 @@ migration. The deleted producer could therefore only ever no-op (while reporting
838838
`status: 'created'`) or, in its one non-no-op window, install the retired key
839839
that nothing afterwards repaired.
840840

841+
> **2026-10-12 note** — "the current key" above is the four-part key #6771 measured.
842+
> Since `5677add93f` (#22877 ruling A) the declared index and the runtime key carry a
843+
> fifth part, `bundle_member` — see the status line and the two owners below.
844+
841845
### Two staleness items in the same block that PREDATE #6771
842846

843847
The YAML example there is stale for reasons of its own; attributing them to
@@ -864,22 +868,28 @@ The YAML example there is stale for reasons of its own; attributing them to
864868
(`packages/metadata-protocol/src/migrations/overlay-index.ts`, run at most once
865869
per protocol instance through its own `ensureOverlayIndex`) — the tier this ADR
866870
actually wants. Raw SQL, partial and NULL-safe:
867-
`(type, name, organization_id, COALESCE(package_id, ''))` `WHERE state = 'active'`,
871+
`(type, name, organization_id, COALESCE(package_id, ''), COALESCE(bundle_member, ''))` `WHERE state = 'active'`,
868872
with a sibling `idx_sys_metadata_overlay_draft` so an active row and a draft row
869-
for one key can coexist. `COALESCE` is what constrains package-less (global)
870-
overlays, which a plain UNIQUE leaves unconstrained because SQL treats NULLs as
871-
distinct. On a dialect that cannot build the partial form it degrades to a
873+
for one key can coexist. The fifth part (since `5677add93f`, #22877 ruling A) is a
874+
bundled type's member — an `email_template`'s locale — so two members of one item
875+
are two rows; it is NULL for the canonical member and for every row of a name-only
876+
type. `COALESCE` is what constrains package-less (global) overlays and canonical
877+
members, which a plain UNIQUE leaves unconstrained because SQL treats NULLs as
878+
distinct; folded to `''`, those rows are constrained exactly as under the four-part
879+
key. On a dialect that cannot build the partial form it degrades to a
872880
**non-unique** composite index — deliberately, since an unrestricted UNIQUE
873881
would reject the active/draft coexistence — and reports that uniqueness is not
874882
enforced instead of claiming success. It builds under a throwaway name first and
875883
only then replaces the real one, so a failure keeps the previous index; every
876884
failure is reported in the ADR-0120 D4 shape and none blocks the boot.
877885
2. **The declaration in `metadata-core`'s `sys-metadata.object.ts`** —
878-
`{ name: 'idx_sys_metadata_overlay_active', fields: ['type', 'name', 'organization_id', 'package_id'], unique: true }`,
886+
`{ name: 'idx_sys_metadata_overlay_active', fields: ['type', 'name', 'organization_id', 'package_id', 'bundle_member'], unique: 'global' }`
887+
(`'global'` is protocol 18's spelling of the bare `true` it retired, ADR-0120 D2),
879888
materialized by `SqlDriver.syncDeclaredIndexes`. This is the coarse fallback a
880-
stack assembled without the runtime migration gets: it still prevents duplicate
881-
active overlays, at the cost of also colliding with archived and reset rows, and
882-
it stays NULL-distinct on `package_id`. It cannot express either refinement —
889+
stack assembled without the runtime migration gets: unrestricted, so it also
890+
collides with archived and reset rows, and NULL-distinct on `package_id` and on
891+
`bundle_member` — NULL for every canonical member, so it holds no canonical row
892+
unique. It cannot express either refinement —
883893
knex's `table.unique()` has no `WHERE` and no `COALESCE` — which is why the
884894
runtime tier exists rather than a richer declaration.
885895

‎docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
# ADR-0139: One engine-internal semi-join leaf — "the ids of object X this caller may read" — pushed down by ObjectQL, never authorable; ADR-0055 row (a) and ADR-0056's non-goal revisited for this one leaf
22

3-
**Status**: Proposed (2026-10-10). **This record declares; it implements nothing.** It becomes **Accepted** on the
4-
maintainer's approval (`docs/adr/**` is Tier H, Prime Directive #14); the execution cards under
5-
[Execution plan](#execution-plan-after-acceptance) are cut after acceptance and ⛔ never before it. Until then the
6-
platform stays in the ruling's letter D state, and letter C (a located 400 at the bound, after objectui#12081 item 8)
7-
is the `domain:services` lane's interim on [#22590](https://github.com/objectstack-ai/objectstack/issues/22590).
3+
**Status**: Accepted (2026-10-11), accepted by the merge that landed it on `main`
4+
([#22707](https://github.com/objectstack-ai/objectstack/pull/22707)); proposed 2026-10-10. **Amended** (2026-10-12):
5+
D2's position sentence names the executor boundary — after the middleware chain and the before-hooks, immediately
6+
before each driver call — in place of the seam where `lowerRelationConditions` runs, which precedes the chain D4's
7+
emitters write into (ruling [`6121741984`](https://github.com/objectstack-ai/objectstack/issues/22861#issuecomment-6121741984)
8+
on [#22861](https://github.com/objectstack-ai/objectstack/issues/22861), letter A). **This record declares; it
9+
implements nothing.** It became **Accepted** on the maintainer's approval (`docs/adr/**` is Tier H, Prime
10+
Directive #14); the execution cards under [Execution plan](#execution-plan-after-acceptance) are cut after acceptance
11+
and ⛔ never before it. Until then the platform stayed in the ruling's letter D state, and letter C (a located 400 at
12+
the bound, after objectui#12081 item 8) is the `domain:services` lane's interim on
13+
[#22590](https://github.com/objectstack-ai/objectstack/issues/22590).
814
**Decided by**: the maintainer, 「同意」 to the director seat's batch #312 item 2 (ruling
915
[`6097072172`](https://github.com/objectstack-ai/objectstack/issues/22590#issuecomment-6097072172), 2026-10-10T11:34Z):
1016
**A is the end state, its first deliverable an ADR**; B (an unbounded `$in`) and a raised cap are ⛔ not taken. The
@@ -124,8 +130,9 @@ any position but a `$in` comparand, the engine refuses it loudly. ⛔ No second
124130

125131
### D2 — ObjectQL composes the scope, once per parent object per request, fail closed
126132

127-
At the engine's own filter seam — where `packages/objectql/src/engine.ts#lowerRelationConditions` runs today, after
128-
ingress parsing and the comparand doors — every `ReadableIds` leaf is replaced by its composed form, still branded,
133+
At the engine's executor boundary — on the final AST, after ingress parsing, the comparand doors, the middleware chain
134+
(`packages/objectql/src/engine.ts#executeWithMiddleware`) and the before-hooks, immediately before each driver call of
135+
`find`, `findOne`, `count` and `aggregate` — every `ReadableIds` leaf is replaced by its composed form, still branded,
129136
before any driver sees it:
130137

131138
1. `canReadObject(P, caller)` false, a throw, or a service that exposes neither it nor `explain` ⇒ `$in: []`, FALSE on

‎scripts/adr-anchors/packages__metadata-core__src__objects__sys-metadata.object.ts.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"adrs": [
44
"ADR-0005"
55
],
6-
"invariant": "ADR-0005 overlay uniqueness (amended 2026-08-09, #6825) — the declared `idx_sys_metadata_overlay_active` on `(type, name, organization_id, package_id)` is the COARSE fallback a stack assembled without `metadata-protocol`'s runtime migration gets: unrestricted and NULL-distinct, so it also collides with archived/reset rows. That is deliberate, not an oversight to fix here — knex's `table.unique()`, which `SqlDriver.syncDeclaredIndexes` builds through, can express neither `WHERE state = 'active'` nor `COALESCE(package_id, '')`, which is why the runtime tier exists. Do NOT re-add a `partial:` key (retired at protocol 17, #5248 / #4943, ADR-0049 — no driver ever emitted the predicate) and do NOT put `environment_id` or `scope` back in the discriminator: `environment_id` is retired and NULL on new rows, `package_id` is in the key under ADR-0048 so two packages shipping the same (type, name) each keep their own overlay row."
6+
"invariant": "ADR-0005 overlay uniqueness (amended 2026-08-09, #6825) — the declared `idx_sys_metadata_overlay_active` on `(type, name, organization_id, package_id, bundle_member)` is the COARSE fallback a stack assembled without `metadata-protocol`'s runtime migration gets: unrestricted and NULL-distinct, so it also collides with archived/reset rows. That is deliberate, not an oversight to fix here — knex's `table.unique()`, which `SqlDriver.syncDeclaredIndexes` builds through, can express neither `WHERE state = 'active'` nor `COALESCE(package_id, '')`, which is why the runtime tier exists. Do NOT re-add a `partial:` key (retired at protocol 17, #5248 / #4943, ADR-0049 — no driver ever emitted the predicate) and do NOT put `environment_id` or `scope` back in the discriminator: `environment_id` is retired and NULL on new rows, `package_id` is in the key under ADR-0048 so two packages shipping the same (type, name) each keep their own overlay row."
77
}

‎scripts/adr-anchors/packages__metadata-protocol__src__migrations__overlay-index.ts.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"adrs": [
44
"ADR-0005"
55
],
6-
"invariant": "ADR-0005 overlay uniqueness (amended 2026-08-09, #6825) — `ensureMetadataOverlayIndexes` is the FINE tier that actually delivers the decision: `(type, name, organization_id, COALESCE(package_id, ''))` `WHERE state = 'active'`, plus the `_draft` sibling. Three properties look improvable from inside this file alone and are each a recorded decision: (a) the fallback on dialects without partial indexes stays NON-unique, because an unrestricted UNIQUE would reject the active+draft coexistence the two indexes exist to allow; (b) `idx_sys_metadata_overlay_active` deliberately reuses the name `metadata-core`'s `sys-metadata.object.ts` declares, since `SqlDriver.syncDeclaredIndexes` skips by name and that is what stops the coarser declared form from being re-imposed; (c) it builds under a probe name before replacing the real one, and every failure keeps the previous index and is reported per ADR-0120 D4 without blocking the boot. #6771 deleted `@objectstack/metadata`'s second, stale-keyed producer of this same index name — do not grow a third."
6+
"invariant": "ADR-0005 overlay uniqueness (amended 2026-08-09, #6825) — `ensureMetadataOverlayIndexes` is the FINE tier that actually delivers the decision: `(type, name, organization_id, COALESCE(package_id, ''), COALESCE(bundle_member, ''))` `WHERE state = 'active'`, plus the `_draft` sibling. Three properties look improvable from inside this file alone and are each a recorded decision: (a) the fallback on dialects without partial indexes stays NON-unique, because an unrestricted UNIQUE would reject the active+draft coexistence the two indexes exist to allow; (b) `idx_sys_metadata_overlay_active` deliberately reuses the name `metadata-core`'s `sys-metadata.object.ts` declares, since `SqlDriver.syncDeclaredIndexes` skips by name and that is what stops the coarser declared form from being re-imposed; (c) it builds under a probe name before replacing the real one, and every failure keeps the previous index and is reported per ADR-0120 D4 without blocking the boot. #6771 deleted `@objectstack/metadata`'s second, stale-keyed producer of this same index name — do not grow a third."
77
}

0 commit comments

Comments
 (0)