Skip to content

Commit 88fb255

Browse files
refactor(metadata-protocol): move saveConflict and listLiveMetadataTypes out of protocol.ts, each into its own module (#25467 cut 24) (#26704)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X7JKgiM29GUcitRvoGGj5BU)_ Part of #25467 Clause-②: no Metadata-protocol: none Ruling-ref: 6270097840 Cut 24 of the `protocol.ts` split (#24586 ruling A, recorded in `6270097840`). Cuts 1–23 landed as #25494, #25518, #25538, #25589, #25601, #25623, #25642, #25719, #25731, #25740, #25797, #25939, #25972, #26014, #26104, #26168, #26264, #26317, #26350, #26366, #26404, #26449 and #26654. The claim is `6441745164`. It takes the cut 24 proposal of the cut 23 report (`6440919239`). As the claim orders, the two members are not one theme, so each moves into its own module. Both passed the AST screen and the read-seam census, and both moved unedited. Two `ObjectStackProtocolImplementation` members move unedited, each into a new module (#23050 shape B): - `saveConflict` → `packages/metadata-protocol/src/protocol-save-conflict.ts`: the optimistic-save conflict answer. It builds the save door's 409 for a `ConflictError` through the one conflict builder and writes its decision-audit row (#21207, #23406). - `listLiveMetadataTypes` → `packages/metadata-protocol/src/protocol-live-metadata-types.ts`: the live metadata-type listing. It returns the engine SchemaRegistry's types plus the `metadata` service's, de-duplicated (#6992). `protocol.ts` loses 13 net lines (29,165 to 29,152). The cut is move-only, and the card stays open for later cuts. Branch `claude/project-thread-ybjm3h` is the harness-pinned branch for this thread. It was restarted from `main` `fe53769a8e` (cut 23's merge). It names no issue, so the claim comment is the identity record. It holds the move commit `34b0e4a3c9` and one merge of `main`, `f430c6a947` (8 commits). The merge touches no file of `packages/metadata-protocol/`. The package's `src/` tree is `917e56be` at the move commit and at the merge. ## Serial with the file's in-flight PRs Measured just before this PR was opened, against the merged head `f430c6a947`: of the 16 open PRs, **none edits `protocol.ts`**, and none names either member or either new module. Three touch `packages/metadata-protocol/src/` test files: **#26337** (3 files, among them `protocol.recovery-doors-emit-mutation.test.ts`), **#26628** (`protocol.meta-types-degenerate-derivation.test.ts`) and **#26701** (`protocol.metadata-store-outage.test.ts`). Each one's `git merge-tree` is clean against this head. #26628's test drives `getMetaTypes`, so `listLiveMetadataTypes` runs through the class method there. It stubs `getRegisteredTypes` and does not read `protocol.ts` by path. `main` moved one commit after the merge: #26682 (`4373103165`, auth sign-out). It touches no file of this package, `git merge-tree` with this head is clean, and the derivation found nothing stale, so `main` was not merged again. ## The order's screen (private statics, `new Error(`, the read-seam census) Both members passed: - **No private-static read.** The AST walk found no access on the `ObjectStackProtocolImplementation` identifier and none on `this.constructor`. - **No error constructed.** Neither body has `new Error(`. `saveConflict` returns the refusal that `metadataConflictRefusal` builds. No `PRODUCERS` row is owed. - **Read-seam census.** `check-durability-degradation-log-level --list` was taken on the base and again on the cut: `82 read seam(s)` and an identical list once `[@:][0-9]+` is stripped. The full readings are under "The durability `--list`". ## What moves | Member | Base line | Visibility | Body lines | AST tokens (params + return + body) | New module | |---|---|---|---|---|---| | `listLiveMetadataTypes` | `:8494` | private | 15 | 105 | `protocol-live-metadata-types.ts` (66 lines) | | `saveConflict` | `:21469` | private | 16 | 163 | `protocol-save-conflict.ts` (68 lines) | ## The shape (#23050 shape B, as cuts 1–23 apply it) - **The class keeps both members.** Each keeps its name, docblock, signature and position. Each body now delegates, marked `[#25467 cut 24]`. - **`saveConflict` reads two members, both methods.** `SaveConflictHost` types each as the class member itself (`ObjectStackProtocolImplementation['…']`). The function types its `this` as their `Pick`, and the stub annotates its literal as `ThisParameterType<typeof saveConflict>`. `metadataConflictRefusal` and `recordOptimisticConflictAudit` are handed over as closures, so the body's calls reach the class methods exactly as before. - **`listLiveMetadataTypes` reads two members, both fields.** `LiveMetadataTypesHost` types them the same way. `engine` (a `private` field) and `getServicesRegistry` (the constructor-supplied optional function field) are handed over as getters. This is the hand-over cut 20 used for `getServicesRegistry`. So the body reads the protocol's own values at call time, and the `?.()` on an absent registry behaves as before. - **No member changes visibility.** - **Every caller still calls the class method.** `getMetaTypes`' `this.listLiveMetadataTypes()` (`:8513`, base) does not change, and neither do the save door's two `throw await this.saveConflict(err, request, writeSource)` sites (`:21243`, `:21454`). ## Module-level declarations, by reader Measured by an AST walk over each body's identifiers, against `protocol.ts`'s top-level declarations and import bindings (property names excluded): | Name | Where `protocol.ts` has it | Other readers in `protocol.ts` | What this cut does | |---|---|---|---| | `ConflictError` (type position, `saveConflict`'s parameter) | the `@objectstack/metadata-core` import | yes (7: five `instanceof` sites, among them the save door's two, `metadataConflictRefusal`'s parameter and the stub's signature) | stays; the new module imports it as a type from there too | `listLiveMetadataTypes`' body reads no module-level declaration. Its module imports nothing but the class's type. No import of `protocol.ts` loses its last reader, and no declaration gains `export`. **The `:172` comment is not touched.** The claim allowed correcting the stale `[commit ece4dad]` emitted-specifier comment (disclosed in cut 23) if this cut touched that import block. This cut adds its two imports after cut 23's import and does not touch the `@objectstack/spec` block, so the comment stays for a later edit. ## The durability `--list` `check-durability-degradation-log-level` exits 0 at both ends with identical output: `82 read seam(s)` at both ends. Its `--list` is **identical once `[@:][0-9]+` is stripped** (158 lines, and no entry changes file). - `listLiveMetadataTypes` has a `try` around the `metadata` service's `getRegisteredTypes()`. That callee is outside the gate's read vocabulary (`find`/`findOne`/`count`), so the catch was not a read seam before the move and is not one after it. - `saveConflict` has no `try`. - On the merged tree the verdict still reads `82 read seam(s)`. ## The published `.d.ts` The series bar is ruling `6348891160`: sorted-union identity, measured first, with raw identity reported when it holds. **Both hold for this cut.** Base `fe53769a8e` and head `34b0e4a3c9` were built with `pnpm --filter @objectstack/metadata-protocol build` against one dependency tree. The closure was built at head by a full `pnpm build` (116/116). For the base build, the package's `src/` was swapped tree-only to base (both new modules removed) and restored from `HEAD` under a trap. Afterwards `git diff HEAD` was empty, `git status` was clean, and all three touched blobs equalled `HEAD`'s. - **Normalised sha256** (the ruling's bar): `310d157f1455a7b76c3d9a60e905ac5e07debb23ec4a6a615b510870341bdbec` at both ends, on `dist/index.d.ts` and `dist/index.d.cts`. The normaliser sorted 80 string-literal union runs. **Sorted-union identity holds.** - **Raw sha256**: `724eaa784128c37a7e4b59d85e21212ba6378a5b07279853c97828cd8368e932` at both ends, 777,143 bytes. **Raw identity holds.** - **Again on the merged tree** `f430c6a947` (closure rebuilt by a full `pnpm build`, 116/116): the same raw and normalised sha256, and the same `index.js`/`index.cjs` hashes, as the pre-merge head. - **The two builds differ where they should.** - `index.js` hashes `8cc705b2d0a6a59b…` at base and `0cb9bc1754636851…` at head (`index.cjs` `8af96d570232787c…` → `40c568bf03d0672b…`). - `listLiveMetadataTypes.call(host` and `saveConflict.call(host` each occur 0 times in the base bundles and once in the head bundles (ESM and CJS alike). - `function listLiveMetadataTypes(` and `function saveConflict(` each go 0 → 1. - **Positive control** on the head tree. `private` was dropped from `saveConflict` on disk (restored from `HEAD` under a trap) and the package rebuilt. Both `.d.ts` files move to `980c4b207280c4e8…` (normalised `f5a4a75faae310fd…`, which differs from the bar's `310d157f…`). So the bar can see a change on these members. - **Export tables.** The ESM and CJS bundles, loaded at runtime from the package, each export the same 78 names at base and head. The sorted list hashes to `9f18a0177ac736ed…` on all of them. Neither moved function nor either `…Host` interface appears. No changeset: `skip-changeset`, per ruling `6348891160`. ## Proof of move-only **Bodies, by AST token.** The compare reads the TypeScript compiler's AST leaf tokens, with JSDoc skipped, over the parameters (less the added `this`), the return type and the body. It compares base `protocol.ts` at `fe53769a8e` against the two head modules: ``` listLiveMetadataTypes: base tokens 105, head tokens 105, differ 0, text-equal(ws-collapsed, comments incl.) true saveConflict: base tokens 163, head tokens 163, differ 0, text-equal(ws-collapsed, comments incl.) true TOTAL tokens=268 differ=0 members=2 CLASS members base=301 head=301 order-same=true byte-identical=299 stubs-with-identical-docblock+signature=2 other=[] TOP base=252 head=254 byte-identical=252 ``` **Negative controls**, one per member. Each changed one token in an in-memory copy of a head module: ``` MUTATED packages/metadata-protocol/src/protocol-live-metadata-types.ts "getRegisteredTypes()" -> "getRegisteredTypez()" listLiveMetadataTypes: base tokens 105, head tokens 105, differ 1, text-equal(ws-collapsed, comments incl.) false saveConflict: base tokens 163, head tokens 163, differ 0, text-equal(ws-collapsed, comments incl.) true MUTATED packages/metadata-protocol/src/protocol-save-conflict.ts "return conflict;" -> "return err;" listLiveMetadataTypes: base tokens 105, head tokens 105, differ 0, text-equal(ws-collapsed, comments incl.) true saveConflict: base tokens 163, head tokens 163, differ 1, text-equal(ws-collapsed, comments incl.) false ``` - **Class side.** There are 301 members at base and at head, in the same order. The 299 that did not move are byte-identical to base, leading comments included. Both stubs sit at their base positions and keep their docblocks and signatures byte-identical. - **Top level of `protocol.ts`.** All 252 base statements are byte-identical, leading comments included. Head adds two statements, the new modules' imports. - **Size.** +152 / −31 over 3 files, 183 changed lines. ## Readers of the moved lines The tests and scripts that read `protocol.ts` by path (the files that name `protocol.ts` and call `readFileSync`) were grepped for both member names and for the bodies' text: `metadataConflictRefusal`, `recordOptimisticConflictAudit`, `has been modified since you loaded it`, `schemaTypes`, `runtimeTypes`, `MetadataService not available`. So were the test files in `packages/`, `docs/qa/` and the `@objectstack/spec` liveness ledgers. - **By path: none reads the moved bodies.** Of the 26 such files, the one textual hit is a `// MetadataService not available` comment inside an inline fixture of `scripts/check-durability-degradation-log-level.mjs`. That fixture is its own code string and does not read `protocol.ts`. - **By name, outside the package:** `packages/spec/src/meta-spelling/metadata-url-spelling.ts` names `listLiveMetadataTypes()` in a comment only. No edit is owed. - **The liveness ledgers.** No row points at either member. Carried forward: `manifest.json`'s `dependencies` row → `protocol.ts#resolveWritePackageScope` (cut 19), and `seed.json` → `protocol.ts#applySeedBodies` (cut 14). Both still quote lines that moved to a seam module. `check:liveness` checks only that the `#symbol` is named in the file, and both stubs keep it. Re-pointing them is a `packages/spec/**` edit, outside this card. - **ADR anchors.** `check-adr-anchors` gives identical output at both ends: 1,871 decision-letter citations, and 59,671 citations across 8,363 files. Neither moved body cites an ADR. `listLiveMetadataTypes`' docblock (ADR-0131 D6) stays on the stub, and neither new module's header cites one. - **Engine-double contract** (`check-engine-double-contract`): exits 0 at both ends. - The `updateData`/`deleteData` seams keep their entries at new line numbers (13866 → 13863, 13891 → 13888). - `productionFiles` is 3,777 at base and 3,780 at head. The +3 are the two new files and the gitignored `packages/spec/src/migrations/registry.ts` that the spec build writes in the head tree, as in cuts 20–23. - **Identical output at both ends:** `check-adr-anchors`, `check-adr-symbol-anchors`, `check-system-context-census`, `check-tenant-audit-census`, `check-query-options-erasure-ratchet`, `check-filter-alias-parity` and `check-durability-degradation-log-level`. `check-nul-bytes` differs only in the scanned file count, +2. The base-end readings were taken in a clean detached worktree at `fe53769a8e`. ## Verification - `tsc --noEmit` in `packages/metadata-protocol` exits 0 at head and on the merged tree. - `pnpm exec eslint --no-inline-config packages/metadata-protocol` exits 0 with no output (the whole touched package, with the root `lint` script's flags). - **`packages/metadata-protocol` suite**, base and head, compared per test name and status. Each leg ran vitest `--maxWorkers=2` with the JSON reporter under the shared verify lock. - Base `fe53769a8e`: 21,599 tests (21,580 passed, 0 failed, 19 skipped), 272 files. - Head `34b0e4a3c9`: 21,599 tests (21,580 passed, 0 failed, 19 skipped), 272 files. - None lost, none added, none changed status. The base leg is the package `src/` swapped tree-only from `fe53769a8e` (both new modules removed) and restored from `HEAD` under a trap. - The merged head `f430c6a947` gives the same 21,599 tests, per test identical to the pre-merge head (0 lost, 0 added, 0 changed). - `check-platform-checklist --changed-from origin/main` (merged tree): OK. 1 anchored file is in the change set, 29 anchors were judged, and 0 were lost. - **Governed surface.** `check-governed-merges --branch claude/project-thread-ybjm3h` finds 0 of 3 paths in the register (13 surfaces), so the PR is NOT governed. It has 183 changed lines, under the 3,000-line human-merge bar. - **Derived gates.** `dispatch-gates --commands --repo objectstack-ai/objectstack` derives 46 gates on 3 changed paths from `f430c6a947`, with no stale-tree finding, and all 46 exit 0 after the full `pnpm build` (116/116). `dispatch-gates --ran` reports 46 derived, 46 run, 0 NOT-MEASURED and 0 UNRUN, and exits 0. - `check-durability-degradation-log-level --list` on `f430c6a947`: `82 read seam(s)`. ## Carry-forward These stay in `protocol.ts`: - **The read-seam members** (cut 21). A member named in a `protocol.ts` read-seam entry of `check-durability-degradation-log-level --list`, as the guarded wrapper or the guarding member, stays until the gate follows imported wrappers or a ruling accepts the loss. The census-negative set is the one cuts 22–23 measured (this cut re-trialled only its own members and the cut 25 candidates): `findEnvironmentRows`, `packagelessAddress`, `findServedOverlayRow`, `foldStoredCollection`, `applyObjectRegistryMutation`, `unmanagedImportCollisions`, `rollBackUnmanagedImport`, `readRevertableCommit` and `storedFlowBindingAgrees`. `rethrowUnlessMetadataStoreUnprovisioned` cannot move at all. - Public members whose return type is inferred. - **The bodies kept by a by-path reader:** - `probeRecord`, `getMetaItemCached`, `findReferencesToMeta`, `refuseUnmintableMetaType` and `metaTypeNamespaceExists`. - `viewContainerNameCollisionRefusal` with `overlaidShippedContainerViewNames`, and `reportPageRequiresAbsentAtLoad`. - The 7 `applyRegistryWriteThrough` callers (among them `applyRemoteMetadataMutation`), `readCodeLayerForCarryForward` and `ensureObjectStorage`. - `emitMetadataMutation`: its body is one of the two `this.notifyMutationListenersLocal(` sites that `protocol.recovery-doors-emit-mutation.test.ts` counts in `protocol.ts`. - The private-static readers: `refusePackagedBaseRemoval`, `packagedBaseRefusal`, `runPublishSideEffects`, `promoteDraftForPublish`, `publishPackageDrafts` and `discardPackageDrafts`. - The field writers: `warnClosureReadUnavailable`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent dc9d1ff commit 88fb255

3 files changed

Lines changed: 152 additions & 31 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 24] The live metadata-type listing of
5+
* `ObjectStackProtocolImplementation` (`listLiveMetadataTypes`), moved out
6+
* of `protocol.ts` whole: the types the engine's SchemaRegistry holds, plus
7+
* the types the `metadata` service reports, de-duplicated — the one answer to
8+
* "which types exist here" that `getMetaTypes` and every other caller share.
9+
*
10+
* A MOVE, not a redesign. The function below is the body of the
11+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
12+
* re-indented one level and otherwise unedited: the same two sources in the
13+
* same order, the same tolerance of an absent `metadata` service, and the
14+
* same de-duplication.
15+
*
16+
* ## How the moved body reaches the class
17+
*
18+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
19+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
20+
* The class keeps the method — the published `dist/*.d.ts` lists its name,
21+
* docblock and declared signature — and its body is one literal of exactly
22+
* the {@link LiveMetadataTypesHost} members it reads, plus a `.call`. Both
23+
* are handed over as getters, so the body reads the protocol's own `engine`
24+
* and its constructor-supplied `getServicesRegistry` field at call time, as
25+
* `protocol-metadata-service-read.ts` hands that field over. Every other
26+
* reader of the method keeps calling the class method.
27+
*
28+
* The body reads none of `protocol.ts`'s module-level declarations and
29+
* imports nothing but the class's type. Nothing re-exports this module, so
30+
* the published surface does not move.
31+
*/
32+
33+
import type { ObjectStackProtocolImplementation } from './protocol.js';
34+
35+
/**
36+
* The `ObjectStackProtocolImplementation` members the function below reads —
37+
* every one of them, and no other; the function's `this` is the `Pick` of the
38+
* members it reads. Each is typed AS the class member (an indexed access on
39+
* the class), so a change to a member's signature on the class is a change
40+
* here too, and naming a member the class does not have is a compile error.
41+
*/
42+
export interface LiveMetadataTypesHost {
43+
readonly engine: ObjectStackProtocolImplementation['engine'];
44+
readonly getServicesRegistry: ObjectStackProtocolImplementation['getServicesRegistry'];
45+
}
46+
47+
/** The body of `ObjectStackProtocolImplementation#listLiveMetadataTypes`; the method's docblock there is its contract. */
48+
export async function listLiveMetadataTypes(
49+
this: Pick<LiveMetadataTypesHost, 'engine' | 'getServicesRegistry'>,
50+
): Promise<string[]> {
51+
const schemaTypes = this.engine.registry.getRegisteredTypes();
52+
53+
// Also include types from MetadataService (runtime-registered: agent, tool, etc.)
54+
let runtimeTypes: string[] = [];
55+
try {
56+
const services = this.getServicesRegistry?.();
57+
const metadataService = services?.get('metadata');
58+
if (metadataService && typeof metadataService.getRegisteredTypes === 'function') {
59+
runtimeTypes = await metadataService.getRegisteredTypes();
60+
}
61+
} catch {
62+
// MetadataService not available
63+
}
64+
65+
return Array.from(new Set([...schemaTypes, ...runtimeTypes]));
66+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 24] The save door's answer to an optimistic-save conflict, from
5+
* `ObjectStackProtocolImplementation` (`saveConflict`), moved out of
6+
* `protocol.ts` whole: the 409 for a `ConflictError` — the repository's race,
7+
* or a caller's token the keyed comparison refused — built by the one
8+
* conflict builder, plus its decision-audit row (#21207, #23406).
9+
*
10+
* A MOVE, not a redesign. The function below is the body of the
11+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
12+
* re-indented one level and otherwise unedited: the same refusal text and
13+
* first-write pin, the same audit row, in the same order.
14+
*
15+
* ## How the moved body reaches the class
16+
*
17+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
18+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
19+
* The class keeps the method — the published `dist/*.d.ts` lists its name,
20+
* docblock and declared signature — and its body is one literal of exactly
21+
* the {@link SaveConflictHost} members it reads, each handed over as a
22+
* closure, plus a `.call`. Every other reader of the method keeps calling
23+
* the class method.
24+
*
25+
* What the body reads from elsewhere, it imports from where `protocol.ts`
26+
* imported it. Nothing re-exports this module, so the published surface does
27+
* not move.
28+
*/
29+
30+
import type { ConflictError } from '@objectstack/metadata-core';
31+
import type { ObjectStackProtocolImplementation } from './protocol.js';
32+
33+
/**
34+
* The `ObjectStackProtocolImplementation` members the function below reads —
35+
* every one of them, and no other; the function's `this` is the `Pick` of the
36+
* members it reads. Each is typed AS the class member (an indexed access on
37+
* the class), so a change to a member's signature on the class is a change
38+
* here too, and naming a member the class does not have is a compile error.
39+
*/
40+
export interface SaveConflictHost {
41+
readonly metadataConflictRefusal: ObjectStackProtocolImplementation['metadataConflictRefusal'];
42+
readonly recordOptimisticConflictAudit: ObjectStackProtocolImplementation['recordOptimisticConflictAudit'];
43+
}
44+
45+
/** The body of `ObjectStackProtocolImplementation#saveConflict`; the method's docblock there is its contract. */
46+
export async function saveConflict(
47+
this: Pick<SaveConflictHost, 'metadataConflictRefusal' | 'recordOptimisticConflictAudit'>,
48+
err: ConflictError,
49+
request: { type: string; name: string; actor?: string; parentVersion?: string | null; mode?: 'draft' | 'publish' },
50+
writeSource: string,
51+
): Promise<Error> {
52+
const conflict = await this.metadataConflictRefusal(
53+
err,
54+
`${request.type}/${request.name}`,
55+
`${request.type}/${request.name} has been modified since you loaded it.`,
56+
request.parentVersion === null ? (request.mode === 'draft' ? 'draft' : 'active') : undefined,
57+
);
58+
await this.recordOptimisticConflictAudit({
59+
type: request.type,
60+
name: request.name,
61+
operation: 'save',
62+
...(request.actor ? { actor: request.actor } : {}),
63+
source: writeSource,
64+
expectedParent: err.expectedParent,
65+
actualHead: err.actualHead,
66+
});
67+
return conflict;
68+
}

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,10 @@ import { reportSealedOverlayRows } from './protocol-sealed-overlay-report.js';
413413
import { expandUnderOwnName, resolveRowlessExpandedView } from './protocol-view-expansion-names.js';
414414
// [#25467 cut 23] The read-side conversion of a stored item: the ADR-0087 chain with its notices, and the warned-once read with the page `type` default.
415415
import { convertStoredItem, convertStoredItemDetailed } from './protocol-stored-item-conversion.js';
416+
// [#25467 cut 24] The live metadata-type listing: the SchemaRegistry's types and the `metadata` service's, de-duplicated.
417+
import { listLiveMetadataTypes } from './protocol-live-metadata-types.js';
418+
// [#25467 cut 24] The save door's 409 for a `ConflictError`, with its decision-audit row.
419+
import { saveConflict } from './protocol-save-conflict.js';
416420

417421
/**
418422
* Canonical Zod schema per metadata type lives in
@@ -8492,21 +8496,14 @@ export class ObjectStackProtocolImplementation implements
84928496
* One accessor, no second vocabulary.
84938497
*/
84948498
private async listLiveMetadataTypes(): Promise<string[]> {
8495-
const schemaTypes = this.engine.registry.getRegisteredTypes();
8496-
8497-
// Also include types from MetadataService (runtime-registered: agent, tool, etc.)
8498-
let runtimeTypes: string[] = [];
8499-
try {
8500-
const services = this.getServicesRegistry?.();
8501-
const metadataService = services?.get('metadata');
8502-
if (metadataService && typeof metadataService.getRegisteredTypes === 'function') {
8503-
runtimeTypes = await metadataService.getRegisteredTypes();
8504-
}
8505-
} catch {
8506-
// MetadataService not available
8507-
}
8508-
8509-
return Array.from(new Set([...schemaTypes, ...runtimeTypes]));
8499+
// [#25467 cut 24] The body moved to `protocol-live-metadata-types.ts` (see `LiveMetadataTypesHost`).
8500+
const readEngine = () => this.engine;
8501+
const readGetServicesRegistry = () => this.getServicesRegistry;
8502+
const host: ThisParameterType<typeof listLiveMetadataTypes> = {
8503+
get engine() { return readEngine(); },
8504+
get getServicesRegistry() { return readGetServicesRegistry(); },
8505+
};
8506+
return listLiveMetadataTypes.call(host);
85108507
}
85118508

85128509
async getMetaTypes() {
@@ -21471,22 +21468,12 @@ export class ObjectStackProtocolImplementation implements
2147121468
request: { type: string; name: string; actor?: string; parentVersion?: string | null; mode?: 'draft' | 'publish' },
2147221469
writeSource: string,
2147321470
): Promise<Error> {
21474-
const conflict = await this.metadataConflictRefusal(
21475-
err,
21476-
`${request.type}/${request.name}`,
21477-
`${request.type}/${request.name} has been modified since you loaded it.`,
21478-
request.parentVersion === null ? (request.mode === 'draft' ? 'draft' : 'active') : undefined,
21479-
);
21480-
await this.recordOptimisticConflictAudit({
21481-
type: request.type,
21482-
name: request.name,
21483-
operation: 'save',
21484-
...(request.actor ? { actor: request.actor } : {}),
21485-
source: writeSource,
21486-
expectedParent: err.expectedParent,
21487-
actualHead: err.actualHead,
21488-
});
21489-
return conflict;
21471+
// [#25467 cut 24] The body moved to `protocol-save-conflict.ts` (see `SaveConflictHost`).
21472+
const host: ThisParameterType<typeof saveConflict> = {
21473+
metadataConflictRefusal: (...a) => this.metadataConflictRefusal(...a),
21474+
recordOptimisticConflictAudit: (...a) => this.recordOptimisticConflictAudit(...a),
21475+
};
21476+
return saveConflict.call(host, err, request, writeSource);
2149021477
}
2149121478

2149221479
/**

0 commit comments

Comments
 (0)