Repository navigation
Commit 5677add
Part of #22877
Clause-②: no
Stage 1 of 2 of ruling 6116165299 (letter A): the overlay store keys a
bundled type's row by its member, so two locales of one `email_template`
saved through the metadata door are two `sys_metadata` rows and both
survive a restart. Stage 2 (the by-name member selector on the `/meta`
doors, `Clause-②: yes`) is a separate PR; #22877 remains open for it.
The ruling, quoted:
> **A: the overlay store carries the bundled type's member as a key
column; one table, one hydration path; each member addressable by
name.**
>
> `sys_metadata` gains one nullable, type-neutral discriminator column.
NULL means the canonical member, so every existing row and every type
whose identity is `name` alone stays byte-identical. The value is taken
from the body through `itemDiscriminator` (`ITEM_KEY_DISCRIMINATORS`),
the one place the identity is declared.
## What changed
- **Column** (`metadata-core` `sys-metadata.object.ts`):
`bundle_member`, nullable text (`maxLength: 100`). NULL is the canonical
member and every row of a name-only type. The declared
`idx_sys_metadata_overlay_active` gains it.
- **Index** (`metadata-protocol` `migrations/overlay-index.ts`): the
runtime key is `(type, name, organization_id, COALESCE(package_id, ''),
COALESCE(bundle_member, ''))`, active and draft. The change rides the
existing ADR-0120 D4 probe-then-replace order unchanged: it drops and
rebuilds the real name on every first write, so the next boot replaces
the four-part index. A key part only relaxes uniqueness, so no rows a
store already holds can block the rebuild. The duplicate-listing query,
the dialect fallback and the `os migrate duplicates` probes all read the
same builders.
- **Backfill** (same module, `backfillOverlayBundleMembers`): through
the engine, each `email_template` row with a NULL member whose body
names a non-canonical locale gets that locale. It is run by boot
hydration (`loadMetaFromDb`, after its own read succeeded) and by the
first write. It is idempotent, never throws, and reports a failure at
`error` with the consequence and the fix. A missing table counts as
benign (`isMissingTableError`).
- **Repository** (`sys-metadata-repository.ts`): `put` takes the member
from the body (`overlayMemberOfBody` → `itemDiscriminator`), never from
the caller. The write head (inherited draft base and orphan adoption
included), the row payload, `get`, `headAt`, `delete`, `promoteDraft`
(and its drain), `restoreVersion` and `history` all address one member.
An omitted `member` means the canonical one. For a name-only type, no
`where` and no row payload ever carries the column.
- **Protocol** (`protocol.ts`): the save door addresses its stored head
and its credential carry-forward by the body's member. The by-name reads
that address rows directly (`findServedOverlayRow`, the publish door's
draft read, the diff's active read and history rows) address the
canonical member. A bare-name `getMetaItem` with no canonical stored row
answers the code layer's canonical member, ahead of the registry's
tier-first bare read.
## Pins (the ruling's, on real code)
`packages/objectql/src/environment-overlay-email-locale-restart.test.ts`
(real `ObjectQL` + real protocol, `sys_metadata` registered from
`SysMetadataObject`, a restart is a new engine and protocol over the
same stores):
- two locales saved, then restart → each serves its own wording (the
list the send path reads), and a re-save updates its own row;
- CONTROL: a single-locale save is unchanged (one row, member NULL; the
other locale keeps the package wording);
- CONTROL: a package-shipped bundle is unchanged across a restart;
- a bare-name read answers the canonical member, also when only `zh-CN`
is customized;
- a by-name reset removes the canonical member's overlay only;
- a row stored before the column existed is backfilled at boot from its
body, and that locale's next save updates it.
`packages/metadata-protocol/src/migrations/overlay-index.test.ts` (real
SQLite, `node:sqlite`):
- the four-part index refuses a second locale (the defect at the index
level);
- after the run the key is replaced, so two members coexist and a
duplicate `(type, name, member)` is refused `UNIQUE constraint failed`;
- the backfill writes exactly the non-canonical bodies, and every other
type's rows are byte-identical (all columns) before and after the
backfill and the index rebuild.
`packages/metadata-protocol/src/sys-metadata-repository.bundle-member.test.ts`:
the repository by member. It covers two rows, canonical defaults for
read, delete, history and promote, a revert refused across members, and
a CONTROL that no name-only payload or `where` carries the column.
`packages/qa/dogfood/test/email-template-two-locales-survive-boot.dogfood.test.ts`:
the showcase on a real SQLite file through `PUT /meta`, real
`sendTemplate` per locale, and a cold boot.
## Evidence
Every heavy run went through `scripts/pm/os-verify-lock.sh` (`VERDICT
command-exit 0` unless noted). Each exit code was captured before any
pipe.
- **Patch round 1, final head `f21b6f9d24`** (three commits on
`288ef6fb69`; no rebase and no force-push; os-dev-report 6119103318 on
#22877):
- the `operatorFacingErrorText` caller census (`Test Core (1/6)` red):
`overlay-index.ts` keeps its call. It gains a census row and a sentinel
pin in `raw-exec-operator-detail-16657.test.ts` (read and write legs,
and the sentinel reaches neither `result.detail` nor the error report)
(`a9f79a360c`);
- `check:i18n` drift (`Type Check · consumer gates` red): the 7
`platform-objects` translation bundles regenerated with `node
scripts/check-i18n-bundles.mjs --write`, committed alone (`10f3b686f4`);
- the `os migrate duplicates` pre-flight on a pre-column store (`Test
Core (3/6)` red on `10f3b686f4`), as the residual bullet below describes
(`f21b6f9d24`);
- gates at `f21b6f9d24`: 96 commands, all exit 0 (81 derived, reconciled
`--ran` 81 of 81, plus the declared wide families and the i18n and
census checks). `cli` duplicates files, both tiers: 6 files, 37 passed.
`types`: `test:repo` 11 passed, `local` 754 passed. `platform-objects`:
1083 passed. `metadata-protocol` `src/migrations/`: 232 passed. Full
packages build: exit 0.
- CI on `f21b6f9d24`: every job is green except the two timing gates
this PR does not cause: `Test Core (6/6)`'s drift step (#22966) and the
`Test Core` aggregator's suite-duration ceilings (#22949:
`cloud-connection`, `client`, `service-analytics`, `service-package`,
`knowledge-ragflow`).
- **Head `288ef6fb69`**, before patch round 1 (this branch merged with
`origin/main` `e357a441c6`; the incoming commits overlap none of the
files here, and in `metadata-core` they add only
`meta-write-capability.ts`):
- `turbo run build --filter=...@objectstack/objectql`: exit 0.
- `metadata-core` full suite: `17 passed`, `394 passed`.
- `metadata-protocol` typecheck: exit 0.
- `metadata-protocol` targeted (`overlay-index`,
`sys-metadata-repository.bundle-member`, `runtime-index-preflight`,
`lookup-artifact-item-call-sites`, `i18n-bundle-list-merge`,
`item-name-every-door`): `6 passed`, `72 passed`.
- `objectql` targeted (`environment-overlay-email-locale-restart`,
`environment-overlay-five-types`,
`engine-security-catalog-package-door`, `registry-i18n-bundle-key`): `4
passed`, `65 passed`.
- The derived gate union, `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `288ef6fb69`: **79 commands, 79
exit 0**. Reconciled with `--ran` (exit codes recorded): `79 derived, 79
run, 0 NOT-MEASURED, 0 UNRUN`.
- **Before the merge:**
- `metadata-protocol` full suite (`c7f399ba7f`): `Test Files 1 failed |
226 passed | 3 skipped (230)`, `Tests 2 failed | 28183 passed`. The one
red file was the `lookupArtifactItem` call-site census, which needed the
new site recorded (`59841e1570`); after that it is `4 passed`.
- `objectql` full suite, both projects (`59841e1570`): `Test Files 404
passed (404)`, `Tests 7864 passed`.
- At `139a2cbc9d`:
- `metadata` full suite: `58 passed`, `871 passed`.
- Typecheck of `metadata-core`, `metadata-protocol`, `objectql` (with
`check:test-typecheck`) and `dogfood`: all exit 0.
- The dogfood closure build (`turbo`, 63 tasks): exit 0.
- `email-template-two-locales-survive-boot.dogfood.test.ts`: `3 passed
(3)`.
- `packages/cli` is not touched. The repo-wide eslint sweep is CI's.
## Ablation
The member was dropped from the repository's write head: `const member =
overlayMemberOfBody(ref.type, body);` became an expression that is
`undefined` at runtime, carrying the marker
`__ABLATION_22877_MEMBER_DROPPED`. The change was made through
`scripts/ablation-replace.mjs` on committed HEAD. The anchor count went
1 → 0, the marker count 0 → 1, and the blob `314afe10d8` → `a28c0832a6`.
`metadata-protocol` was rebuilt (objectql resolves it through `dist/`),
and `ablation-dist-preflight` confirmed the marker in `dist`.
- Mutated: the restart pin file goes `3 failed | 3 passed (6)`. The
two-locale restart pin, the bare-name canonical pin and the by-name
reset pin are red (the second save updates the first locale's row). Both
CONTROLs (single locale, package bundle) and the backfill case stay
green, as predicted.
- Restored: blob back to `314afe10d8`, `git diff HEAD` empty, `dist`
rebuilt, `ablation-dist-preflight --absent` exit 0, and the file goes `6
passed (6)`.
## Deviations and residuals, stated
- **Lineage counter stays per `(org, type, name)`**, not per member.
`sys_metadata_history` declares `unique (organization_id, type, name,
version)`, and a per-member counter would mint colliding versions under
it, which would need a second index ceremony on a second table. With one
counter per name, a version names exactly one snapshot. `history` and
`restoreVersion` carry the member: history filters to one member, and a
revert to another member's version answers `404 VERSION_NOT_FOUND`.
- **History rows carry no member column.** A history row's member is
read off its own body (and a tombstone's off the row it removed, by
`previous_checksum`) through the same `overlayMemberOfBody`. No second
table and no history migration.
- **Registry heal on a by-name reset** (`removeRuntimeShadow`) still
sweeps the bundle's whole hydrated overlay group in the registry: the
other member's REGISTRY entry drops until the next boot. Its row stays
and is still served: the list and the send path read the store, as the
reset pin shows. Member-aware registry heal belongs with stage 2's
member-addressed DELETE.
- **Package-batch doors** (`publishPackageDrafts`,
`discardPackageDrafts`, `revertCommit`, the cluster mutation fan-out)
address the canonical member. Before this PR two members' drafts could
not coexist, so nothing they did before changes. A non-canonical
member's draft is published by a save naming its locale, or by stage 2's
selector.
- **`os migrate duplicates` on a store this version has not booted yet**
(a `sys_metadata` table without the member column): the overlay
pre-flight first reads whether the column is present. When it is not, it
lists duplicates with every row read as the empty member, so a live
duplicate is still reported `blocked` and is never reported
`unreadable`. Both statements live in `overlay-index.ts`. The choice
between them is made in `runtime-index-preflight.ts`, which reports the
statement that ran as the entry's `duplicateSql`. The report's keys are
unchanged. On such a store, an `email_template` pair whose bodies name
different locales can be reported and then not block once the serving
boot's backfill has run. That errs in the strict direction. (Patch round
1, `f21b6f9d24`. This fixes the `packages/cli` duplicates integration
red on `10f3b686f4`.)
- **A driver without partial indexes** (the ceremony's fallback arm,
MySQL): the rebuild is refused there, as before, and is reported in the
server log at the first metadata write. A store that booted before keeps
its four-column declared UNIQUE under its name, because
`syncDeclaredIndexes` skips by name. That index has never constrained an
environment-wide overlay row: the metadata door stores no organization
(ADR-0131 D6), and a UNIQUE key does not collide NULLs, in either the
direct index or driver-sql's hash-shadow form. So two languages of one
template coexist there too, and no save is refused. This was measured on
SQLite (two members with `organization_id` NULL are both accepted;
CONTROL: a non-NULL organization refuses the second member). It is not
measured on a live MySQL. `os migrate plan` lists the declaration change
as a destructive `recreate_index`, applied with `os migrate apply
--allow-destructive`. A fresh store gets the five-column declaration.
(Corrected after patch round 2: the earlier wording here said such a
save is refused, which the dev's measurement 6119963033 falsified.)
## Acceptance notes
- **i18n bundles:** the regenerated `zh-CN`, `ja-JP` and `es-ES` bundles
carry the English source text for the new `bundle_member` keys, which
still need translating. `check:i18n-coverage` stays green.
- **Caller census:**
`packages/types/src/driver-error-classification.callers.test.ts` (a
`domain:cli` file) gains one row for `overlay-index.ts`, the edit class
the seat named. The sentinel pin sits in `metadata-protocol`.
- **Surface:**
`metadata-protocol/src/migrations/runtime-index-preflight.ts` (and its
test) is outside the claim's listed files but inside its package and
directory. It is the module that reads `overlay-index.ts`'s statements.
The edit is one function and a private table, and the seat accepted it
as an in-lane deviation.
-
`packages/drivers/driver-sql/src/sql-driver-overlay-index-drift.test.ts`
keeps its own four-part fixture. It tests the drift detector's reading
of a `COALESCE` key part and imports nothing changed here, so it stays a
valid detector fixture.
- Two `objectql` harnesses (`environment-overlay-five-types.test.ts`,
`engine-security-catalog-package-door.test.ts`) hand-declare
`sys_metadata`. They gain the `bundle_member` field the platform object
declares, because the engine refuses an undeclared field in a `where`.
- `protocol.lookup-artifact-item-call-sites.test.ts` records the new
`lookupArtifactItem` site with its disposition.
`scripts/engine-double-contract.pinned.json` records the new test file's
pinned doubles (`--write`, additions only).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6a7eca6 commit 5677add
24 files changed
Lines changed: 1755 additions & 70 deletions
File tree
- .changeset
- packages
- metadata-core/src/objects
- metadata-protocol/src
- migrations
- objectql/src
- platform-objects/src/apps/translations
- qa/dogfood/test
- types/src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
96 | 117 | | |
97 | 118 | | |
98 | 119 | | |
| |||
200 | 221 | | |
201 | 222 | | |
202 | 223 | | |
203 | | - | |
204 | | - | |
205 | | - | |
206 | | - | |
207 | | - | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
208 | 231 | | |
209 | 232 | | |
210 | 233 | | |
| |||
217 | 240 | | |
218 | 241 | | |
219 | 242 | | |
220 | | - | |
221 | | - | |
222 | | - | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
223 | 247 | | |
224 | 248 | | |
225 | 249 | | |
226 | 250 | | |
227 | 251 | | |
228 | | - | |
| 252 | + | |
229 | 253 | | |
230 | 254 | | |
231 | 255 | | |
| |||
0 commit comments