Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
8de88ee
feat(objectql, plugin-email): email template names held per namespace…
claude Nov 10, 2026
cef0ef9
test(objectql, plugin-email): pin the email template narrowing, the p…
claude Nov 10, 2026
c88476f
test(dogfood): the flow-family boot pins read the email template's na…
claude Nov 10, 2026
4312637
fix(metadata-protocol): the exact qualifier reads a bundle addition a…
claude Nov 10, 2026
8bbcc56
test(core, cli): the resolver and the stored-references report read t…
claude Nov 10, 2026
e37cebe
docs(changeset): a code package's bundle addition is still judged aga…
claude Nov 10, 2026
f1189c7
test(plugin-email): the platform-requested list pin loads core at mod…
claude Nov 10, 2026
3ccfb7a
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
3465aaa
fix(metadata-protocol): the exact qualifier keeps a pair's item on it…
claude Nov 10, 2026
a52f89e
docs(changeset): the email template change declares the ?package= nar…
claude Nov 10, 2026
a4326c9
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
50e1ef9
fix(plugin-email, service-automation, service-messaging): an email te…
claude Nov 10, 2026
a4ca293
fix(metadata-protocol, cli): a pair member row two holders could own …
claude Nov 10, 2026
1484ab5
test(plugin-email): read the last protocol request without Array.prot…
claude Nov 10, 2026
1fabc5d
feat(metadata): the in-memory store keeps one slot per (package, name…
claude Nov 10, 2026
f6bbafc
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
4c176c5
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
249a6a4
fix(metadata-protocol, cli)!: a package-less row of a name two packag…
claude Nov 10, 2026
9102aca
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
14bbef3
fix(service-automation, metadata-protocol, objectql, cli): contract r…
claude Nov 10, 2026
376b80e
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
9ea252e
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
c79a4e6
test(metadata-protocol, rest, runtime): the getItem-reading doubles r…
claude Nov 10, 2026
c5d0e37
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
d808486
fix(cli): the stored-references hint names no tracker id in its print…
claude Nov 11, 2026
ee4bcab
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 11, 2026
7136e33
fix(metadata-protocol): a row hydrated into the registry with no pack…
claude Nov 11, 2026
4668d9e
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
460df4a
test(objectql): the package-less row is written as the store writes i…
claude Nov 11, 2026
248d471
fix(metadata): a bundled type's unscoped loader reads keep every memb…
claude Nov 11, 2026
26e2548
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
61dfabf
docs(changeset): declare the loader half — rollback and delete by nam…
claude Nov 11, 2026
f9f589d
fix(metadata): MetadataManager's plural reads keep a bundled type's m…
claude Nov 11, 2026
f779531
fix(metadata): a bundled type's save writes its own member's row, and…
claude Nov 11, 2026
12ee9e5
docs(changeset): the manager's plural reads per member, and the membe…
claude Nov 11, 2026
cfb8f30
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
0f93f8f
fix(metadata): the boot priming pass never writes an item back into t…
claude Nov 11, 2026
74a7e4c
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
e8c7447
docs(metadata): MetadataUnregisterOptions keeps its own doc comment (…
claude Nov 11, 2026
fefaa51
test(metadata): the R5-A control seeds the database loader from a rea…
claude Nov 11, 2026
2990c4c
docs(metadata): the boot write-back note is its own changeset line; o…
claude Nov 11, 2026
74d2e76
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
7594637
test(metadata): database-loader.test.ts pays the metadata-fs load at …
claude Nov 11, 2026
6ed3bea
docs(metadata): keep database-loader.ts out of round 7's diff
claude Nov 11, 2026
17cfbf8
fix(metadata): the boot pass binds a legacy row's empty package colum…
claude Nov 11, 2026
27abd45
Merge branch 'main' into claude/project-thread-3giljf
objectstack-fleet[bot] Nov 12, 2026
14132f0
wip(objectql): a pair row binds to the holder the exact read's owner …
claude Nov 12, 2026
97eff78
wip(objectql): pins for the pair row's binding on every overlay-slot …
claude Nov 12, 2026
4fc0ba8
wip(objectql): narrowing pin, declared non-holder pin and changeset (…
claude Nov 12, 2026
ee13867
wip(changeset): no FROM-TO heading in a no-migration changeset (#32408)
claude Nov 12, 2026
49dd454
wip(objectql): CONTROL pin, two independent holders still refuse the …
claude Nov 12, 2026
c768862
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 12, 2026
ed0f2ba
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 12, 2026
d9d3621
fix(objectql): a pair row's owners are counted as the exact read coun…
claude Nov 12, 2026
a8b63c2
fix(objectql): a pair row's owners are counted over every holder with…
claude Nov 12, 2026
7b74268
fix(objectql): an undeclared row of another App's pair counts where i…
claude Nov 12, 2026
075fb31
fix(objectql): a pair row whose count would read another App's undecl…
claude Nov 12, 2026
1c1342c
test(objectql): red pins — the exact read serves an undeclared pair r…
claude Nov 12, 2026
d9a7d64
fix(metadata-protocol)!: the exact read adopts the registry's binding…
claude Nov 12, 2026
65a7c73
docs(objectql): the registry's pair-row binding notes name the exact …
claude Nov 12, 2026
7bea2a2
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 12, 2026
6d317e6
docs(spec, changeset): register the pair-row locale narrowing as an A…
claude Nov 12, 2026
4fd9eeb
test(objectql): a pair row's binding over every holder meets the serv…
claude Nov 12, 2026
47c03c3
docs(objectql, changeset): the pair-row binding counts every holder, …
claude Nov 12, 2026
e94a8de
docs(changeset): the Clause-② line names the reads a disabled holder'…
claude Nov 12, 2026
669d498
docs(spec): the pair-row upgrade entry names only the remedies the sa…
claude Nov 12, 2026
4776430
fix(objectql): a door reads a holder's bundle members as if the arriv…
claude Nov 12, 2026
7509d92
test(objectql): pin a pair row's locale at both doors and at the cold…
claude Nov 12, 2026
bc22c1a
test(objectql): name the undeclared-row door pins for what they hold:…
claude Nov 12, 2026
c3cc324
docs(changeset): declare the pair row's mirror admission at the gate …
claude Nov 12, 2026
268e074
Merge claude/project-thread-iv3vz6 (PR #32815 head e94a8de8f) into th…
claude Nov 12, 2026
87b88bb
Merge origin/main (f770d664b) into the card branch
claude Nov 12, 2026
8887a5c
Merge claude/project-thread-iv3vz6 (PR #32815 head c3cc324b1) into th…
claude Nov 12, 2026
df0b5fa
fix(objectql): the exact read adopts the pair-row binding over the ho…
claude Nov 12, 2026
c233053
docs(spec, changeset): register the exact read's pair-row narrowing a…
claude Nov 12, 2026
72c8a9c
Merge origin/main (bf02d33bd) into the card branch
claude Nov 13, 2026
5a9d589
fix(objectql): the exact read's adoption settles the bundle-addition …
claude Nov 13, 2026
dd283fa
fix(objectql): the exact read's adoption counts a package holding the…
claude Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/32561-exact-read-adopts-pair-row-binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@objectstack/metadata-protocol': minor
'@objectstack/objectql': minor
---

fix(metadata-protocol)!: the exact read (`?package=X`) no longer serves an undeclared App-pair row the registry binds to no package (ADR-0150 D4, red line 2)

Clause-②: yes (narrowing) — two exact reads that served a row of an App's customization pair now answer the address's own shipped item: `?package=X` for an undeclared pair row of the very bundle member a bundle addition of another package of the App ships, and a package's address for an undeclared pair row while another App's shared pair stores an undeclared row of the name. In both the registry binds the row to no package. First-party packages in either position: 0. The widening is one public method: `SchemaRegistry#overlayRowBinding(type, name, item)`, the registry's binding of a stored row, read-only, which the exact read asks.
Metadata-protocol: narrowed

<!-- adr-0087: registered pair-row-exact-read-no-owner -->

**BREAKING** for two exact reads, both of an undeclared row of an App's customization pair (`<app>.custom`, a row with no `overrides.package`). An undeclared row that could customize either of two packages' members is not attributed to one of them (red line 2). To keep such a row on the package whose locale it customizes, save it with `overrides.package` naming that package, the package of the App that ships the locale: the save door accepts it (it already prescribes it, refusing `403 NOT_OVERRIDABLE` a new undeclared row of a locale an installed package ships), and `?package=<that package>` then serves the row. A row of a locale no package ships stays readable at `?package=<app>.custom`.

The exact read's owner check (`pairRowOnAddress`) counted the holders after the registry had bound the row, so a bundle addition was no holder there, and it never read another App's pair. The registry (`SchemaRegistry#pairRowBinding`, since #32408) counts an addition that ships the row's member as a second owner, and binds a row whose count would read another shared pair's undeclared row to no package. The exact read now adopts that binding: `SchemaRegistry#overlayRowBinding(type, name, item)`, a new read-only method, answers it over the holders the exact read serves (a disabled package holds no name, ADR-0150 D3), and `pairRowOnAddress` treats an undeclared pair row as no member of an address whose one owner it does not bind the row to. It adds no count of its own.

What moves (each line FROM → TO), with App A, X a package of A holding an email template's name, Z a package of A depending on X that ships only `fr-FR` of it (a bundle addition), and `A.custom` storing an undeclared `fr-FR` row:

- `?package=X&member=fr-FR`: FROM the row TO X's own `fr-FR` (none, `404`). X's list and execution list: FROM carrying the row TO leaving it out. Either order: Z installed before the row (reached by a hydrated row; the save door refuses that write `403 NOT_OVERRIDABLE`, prescribing the declaration) or after it (the row saved and published while X alone held the name, then Z installed). `?package=Z` keeps Z's own `fr-FR`; `?package=A.custom` keeps the row.
- `?package=Y` / `?package=P` when App A's pair and App B's pair each store an undeclared row of the name (the registry binds neither): FROM the pair's row TO the package's own item of that member (none). Each pair's own address keeps its row.

Unchanged: a row declaring its base (`overrides.package`) binds and serves as declared; the App's own address keeps an undeclared row of its own pair (#28390 ruling A); two holders that could own a row still refuse `409` naming both; a row whose one owner the registry binds it to is served there as before; a registry without `overlayRowBinding` keeps the exact read's own count. With one of two holders of the name disabled, the enabled holder's address, list and execution list keep an undeclared row of the pair (#32183, epic ruling R4-A): the adoption counts the served holders, as the exact read does, while the registry's own readers keep counting every holder, so a disable never moves their binding.
10 changes: 10 additions & 0 deletions .changeset/32561-pair-row-exact-read-semantic-entry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/spec': patch
---

docs(spec): the protocol upgrade guide names the two exact reads #32561 narrows: an undeclared row of an App's customization pair is no member of a package's address when the registry binds it to no package, because another package of the App ships its locale as a bundle addition or another App's shared pair stores an undeclared row of the name

Clause-②: no
Metadata-protocol: none

The semantic entry `pair-row-exact-read-no-owner` (`packages/spec/src/migrations/entries/semantic/18.pair-row-exact-read-no-owner.ts`) registers the narrowing `.changeset/32561-exact-read-adopts-pair-row-binding.md` declares (ADR-0087, `adr-0087: registered`). There is no automatic rewrite, because which package's locale a pair row customizes is a judgement about that row. The remedy it names is the one the save door accepts: declare the package that ships the locale (`overrides.package`). It adds no key and no export.
42 changes: 40 additions & 2 deletions packages/metadata-protocol/src/protocol-exact-qualifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ interface ExactQualifierRegistry {
getEnvironmentScopedItem(type: string, name: string, member?: string): unknown;
getPackageScopedItem(type: string, name: string, packageId: string, member?: string): unknown;
getPackage?(id: string): unknown;
/**
* [#32561] The package the registry binds a stored row of `(type, name)`
* to (objectql's `overlayRowBinding`, the one owner rule every registry
* reader asks, counted over the served holders as this read counts them):
* a package id, `null` for the environment's own item, or `undefined` for
* no package. The exact read adopts it for an undeclared pair row
* ({@link pairRowOnAddress}); a registry without it keeps the exact read's
* own count.
*/
overlayRowBinding?(type: string, name: string, item: unknown): string | null | undefined;
}

/**
Expand Down Expand Up @@ -422,7 +432,8 @@ export function exactQualifierItem(
const bases = [address.packageId, ...address.registryHolders];
if (bases.some((base) => isPairOf(served, base, installed))) {
const verdict = pairRowOnAddress(served, item, address.packageId, bases,
registry.packageScopedHoldersOf(request.type, request.name) ?? [], installed);
registry.packageScopedHoldersOf(request.type, request.name) ?? [], installed,
registryRowBinding(registry, request.type, request.name, item));
if (verdict === 'serve') return item;
if (verdict !== 'not-member') {
throw refusal(request.type, request.name, address.packageId, 'ambiguous-in-closure',
Expand Down Expand Up @@ -716,7 +727,22 @@ export function pairMemberWithheldAt(
const registry = exactQualifierRegistry(host);
if (!registry || (registry as { readsByPackage?(type: string): boolean }).readsByPackage?.(type) !== true) return false;
return pairRowOnAddress(pairId, item, base, [base], registry.packageScopedHoldersOf(type, name) ?? [],
installedManifestsOfEngine(host.engine)) !== 'serve';
installedManifestsOfEngine(host.engine), registryRowBinding(registry, type, name, item)) !== 'serve';
}

/**
* [#32561] The registry's binding of the stored row `item`, read lazily
* ({@link ExactQualifierRegistry.overlayRowBinding}), or `undefined` when the
* registry does not answer it (the exact read then keeps its own count).
*/
function registryRowBinding(
registry: ExactQualifierRegistry,
type: string,
name: string,
item: unknown,
): (() => string | null | undefined) | undefined {
const read = registry.overlayRowBinding;
return typeof read === 'function' ? () => read.call(registry, type, name, item) : undefined;
}

/**
Expand All @@ -735,6 +761,16 @@ export function pairMemberWithheldAt(
* - Else two or more holders could own it: refuse, naming every one (F2).
* - Else a sole owner that is not one of the address's bases owns it: not
* this address's member (S2).
* - [#32561] Else a sole owner the registry does not bind the row to
* (`registryBinding`, the registry's one owner rule: `pairRowBinding`
* counts a bundle addition that ships the row's member as an owner, and
* binds a row whose count would read another shared pair's undeclared row
* to no package) is no owner of it: not this address's member, so the
* address answers its own shipped item (epic ruling, red line 2). The
* exact read adopts the registry's count rather than counting again. Both
* count the served holders (a disabled package holds no name, ADR-0150
* D3): with one of two holders disabled, the row stays the enabled one's
* member (#32183, epic ruling R4-A).
* - Else (the sole owner is a base, or no holder pairs it) it is served.
*/
export function pairRowOnAddress(
Expand All @@ -744,13 +780,15 @@ export function pairRowOnAddress(
bases: readonly string[],
holders: readonly string[],
installed: InstalledManifestLookup,
registryBinding?: () => string | null | undefined,
): 'serve' | 'not-member' | { refuse: string[] } {
const declared = declaredOverrideBase(item);
if (declared !== undefined) return bases.includes(declared) ? 'serve' : 'not-member';
if (isAppPairOf(pairId, addressPackageId, installed)) return 'serve';
const owners = pairMemberOwners(pairId, holders, installed, item);
if (owners.length > 1) return { refuse: owners };
if (owners.length === 1 && !bases.includes(owners[0]!)) return 'not-member';
if (owners.length === 1 && registryBinding !== undefined && registryBinding() !== owners[0]) return 'not-member';
return 'serve';
}

Expand Down
Loading
Loading