Skip to content

Commit 77dc706

Browse files
refactor(metadata-protocol): move the flow save's retired-spelling replay verdict out of protocol.ts into protocol-flow-save-replay.ts (#25467 cut 18) (#26317)
<!-- 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 18 of the `protocol.ts` split (#24586 ruling A, recorded in `6270097840`). Cuts 1–17 landed as #25494, #25518, #25538, #25589, #25601, #25623, #25642, #25719, #25731, #25740, #25797, #25939, #25972, #26014, #26104, #26168 and #26264. The claim is `6425711631`. The scope is the cut 18 proposal of the cut 17 report (`6423385238`), as the server seat approved it in its dispatch order. The member passed the AST screen and moved unedited. One `ObjectStackProtocolImplementation` member moves unedited into a new module, `packages/metadata-protocol/src/protocol-flow-save-replay.ts` (#23050 shape B): - `flowSaveReplaysRetired`: the flow save's retired-spelling replay verdict (#23903 ruling B′). It answers whether a flow save may keep the full conversion chain, because every retired spelling its body carries is one that a stored row already carries. `protocol.ts` loses 46 net lines (29,290 to 29,244). 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` `0a1061a61c` after cut 17 merged. The branch names no issue, so the claim comment is the identity record. It holds two commits: - the move commit `e6ddd60ba6`; - the merge of `main` `8faba89e69` (`5a7ad6a7b2`). That merge touches no file of `packages/metadata-protocol/`: the package's `src/` tree is `23f9f0cc` at both ends. It does change the root `tsconfig.json` and two other packages' `package.json`, so the workspace was rebuilt in full and the package re-measured after it. ## Serial with the file's in-flight PRs Measured just before this PR was opened: of the 23 open PRs, three touch `packages/metadata-protocol/src/`. Each was checked with `git merge-tree` against this branch's head (`5a7ad6a7b2`). **#26138** edits seven `protocol.*.test.ts` files, none of which names the moved member, `FLOW_FALLBACK_VERDICT_NOT_REPLAYED` or the new module. Its merge-tree reports one conflict, in `packages/spec/src/ui/visibility-alias-keys-retirement.test.ts`, which #26138 has with `main` itself (a merge-tree against `origin/main` alone reports it too). **#26305** (head `1267e3cb2b`) adds 26 lines to `protocol.ts`, at `:27` (imports), `:7145` and `:7540`. The merge-tree is **clean** (exit 0). **#26244** (draft, head `c33b95d89f`) edits `protocol.ts` (+5 / −2). Its hunks are the `@objectstack/spec/contracts` import lines at `:166–167` and a host literal at `:23178`. The merge-tree **conflicts in `protocol.ts`**, at the import block only. #26244 rewrites lines 166–167. This cut rewrites the adjacent line 168, dropping `ALL_CONVERSIONS` from the `@objectstack/spec` import. Git refuses adjacent edits as one conflict. The resolution is the union of both sides: ```ts import type { IFieldFileService, IPubSub } from '@objectstack/spec/contracts'; import { FIELD_FILE_SERVICE, RLS_MEMBERSHIP_RESOLVER_SERVICE } from '@objectstack/spec/contracts'; import { applyConversionsToFlow, applyConversionsToStoredItem, type ConversionNotice, type ConversionTodoNotice } from '@objectstack/spec'; ``` The `:23178` hunk merges clean. The same merge-tree also conflicts in `packages/spec/src/type-alias-convention.pin.test.ts`. That conflict is between #26244 and `main` itself: a merge-tree of #26244 with `origin/main` alone shows the same conflict. It is not this cut's. Whichever PR lands second takes the three-line resolution above. The `ALL_CONVERSIONS` drop is owed, not optional: the moved body was its last reader in `protocol.ts`, so keeping it would leave an unused import. ## The order's screen (private statics, `new Error(`) The member passed both checks: - **No private-static read.** The AST walk found no access on the `ObjectStackProtocolImplementation` identifier and none on `this.constructor`. - **No error constructed.** The body has no `new Error(` and no other error constructor, so no `PRODUCERS` row is owed. It has no `try`, no `catch` and no log line. ## What moves | Member | Base line | Visibility | Body lines | AST tokens (params + return + body) | |---|---|---|---|---| | `flowSaveReplaysRetired` | `:6599` | private async | 55 | 642 | The proposal said 55 body lines. ## The shape (#23050 shape B, as cuts 1–17 apply it) - **The class keeps the member** with its name, docblock, signature and position. Its body now delegates, marked `[#25467 cut 18]`. - **Typing.** `FlowSaveReplayHost` types its one member as the class member itself, `ObjectStackProtocolImplementation['getOverlayRepo']`. The function types its `this` as `Pick<FlowSaveReplayHost, 'getOverlayRepo'>`. The stub annotates its literal as `ThisParameterType<typeof flowSaveReplaysRetired>`. No member changes visibility. - **Members read**, measured by an AST walk over property and element accesses on `this`, with `as` casts, parentheses and `!` stripped, and bare `this` checked too. The body reads `getOverlayRepo` only. It is handed over as a closure that calls the method on the protocol at call time. A test that sets `protocol.getOverlayRepo = ...` on the instance (`protocol.driver-text-disclosure.test.ts` does) still reaches the body. - **Every other caller still calls the class method.** ## Module-level declarations, by reader Measured by an AST walk over the body's identifiers, against `protocol.ts`'s top-level declarations and import bindings. Property names are excluded. | Name | Where `protocol.ts` has it | Other readers in `protocol.ts` | What this cut does | |---|---|---|---| | `FLOW_FALLBACK_VERDICT_NOT_REPLAYED` | `const` at `:3358` | 1 (the save door's flow fallback in `saveMetaItem`, base `:20930`) | gains `export` in place; the new module imports it at runtime | | `ALL_CONVERSIONS` | the `@objectstack/spec` import, `:168` | **0**, so the moved body was its last reader | leaves the import, and the new module imports it from `@objectstack/spec` | | `applyConversionsToFlow`, `ConversionNotice` | the same `@objectstack/spec` import | yes | stay; the new module imports them from there too | | `itemDiscriminator` | the `@objectstack/metadata-core` import | yes | stays; imported from there too | | `MetadataWriteFace` | exported type alias | yes | already exported; imported as a type | `FLOW_FALLBACK_VERDICT_NOT_REPLAYED` is not re-exported from `index.ts`, so the package's export table and `.d.ts` do not change (see below). The new module's runtime import of it from `./protocol.js` follows the cycle that earlier modules already use (`protocol-publish-batch.ts`, `protocol-stored-overlay-reads.ts`). It reads the constant only at call time. The one new statement in `protocol.ts` is the import of the new module. ## The durability `--list` `check-durability-degradation-log-level` exits 0 at both ends with identical output. Its `--list` is **identical once `[@:][0-9]+` is stripped**: 159 lines, and no entry changes file. The body has no `catch` and makes no engine call. Its two reads are `repo.headAt(...)` on the overlay repository. ## The published `.d.ts` The series bar is ruling `6348891160`, sorted-union identity, measured first. Base `0a1061a61c` and head `e6ddd60ba6` were built with `pnpm --filter @objectstack/metadata-protocol build` against one dependency tree. The closure was built at head by a full `pnpm build` (119/119). For the base build, the package's `src/` was swapped tree-only to base and restored from `HEAD` under a trap. Afterwards `git diff HEAD` was empty, `git status` was clean, and both touched blobs equalled `HEAD`'s. The readings were taken on the pre-merge tree and again after merging `main` (head `5a7ad6a7b2`, closure rebuilt by a full `pnpm build`, 119/119). They hold for both: - **Raw sha256**: `08914e65f936a2b031c3f3f215481a97f53d132eefd869e4f42d928947ebe173` at base and at head, on both `dist/index.d.ts` and `dist/index.d.cts`, 775,874 bytes each. **Raw identity holds**, so sorted-union identity holds too. The member stays a `private NAME;` line, and the newly exported `FLOW_FALLBACK_VERDICT_NOT_REPLAYED` does not appear. - **Normalised sha256**: `46ccd7e5c4db673652513d1801ec9c9f06bf34a0a7fb6ba97fb2531ef0cc544b` at both ends (80 string-literal union runs sorted, using the cut 12 normaliser). - **The two builds differ where they should.** `index.js` hashes `8732ec7d7e0d5cd5…` at base and `74a6c78db7e9bcc6…` at head. The `.call(host` site of `flowSaveReplaysRetired` occurs 0 times in the base bundles and once in the head bundles, ESM and CJS alike. `function flowSaveReplaysRetired(` appears 0 times at base and once at head. - **Positive control** on the head tree. `private` was dropped from `flowSaveReplaysRetired` on disk (restored from `HEAD` under a trap) and the package rebuilt. Both `.d.ts` files move to `bd3b47c8b435f536…` (normalised `164c324a9516551e…`) and print `flowSaveReplaysRetired(request: { name: string; item?: unknown; packageId?: string | null; …`. So the bar can see a change on this member. - **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 four. Neither the moved function, `FlowSaveReplayHost` nor `FLOW_FALLBACK_VERDICT_NOT_REPLAYED` appears. No changeset: `skip-changeset`, per ruling `6348891160`. ## Proof of move-only **Body, by AST token.** The compare reads the TypeScript compiler's AST leaf tokens, skipping JSDoc, over the parameters (less the added `this`), the return type and the body. It runs base `protocol.ts` at `0a1061a61c` against the head module: ``` flowSaveReplaysRetired: base tokens 642, head tokens 642, differ 0, text-equal(ws-collapsed, comments incl.) true TOTAL tokens=642 differ=0 members=1 CLASS members base=300 head=300 order-same=true byte-identical=299 stubs-with-identical-docblock+signature=1 other=[] TOP base=246 head=247 byte-identical=244 ``` **Negative control.** One token was changed in an in-memory copy of the head module (`mode === 'draft' ? 'draft' : 'active'` to `mode !== 'draft' ? …`, one occurrence): ``` MUTATED packages/metadata-protocol/src/protocol-flow-save-replay.ts "mode === 'draft' ? 'draft' : 'active'" -> "mode !== 'draft' ? 'draft' : 'active'" flowSaveReplaysRetired: base tokens 642, head tokens 642, differ 1, text-equal(ws-collapsed, comments incl.) false TOTAL tokens=642 differ=1 members=1 ``` - **Class side.** There are 300 members at base and at head, in the same order. The 299 that did not move are byte-identical to base, leading comments included. The stub sits at its base position and keeps its docblock and signature byte-identical (everything before the body's opening brace). The base signature spans lines (`(request: {` … `}, mode: …): Promise<boolean> {`). The module splits the two parameters onto their own lines. The token compare shows that this changes whitespace only. - **Top level of `protocol.ts`.** 244 of the 246 base statements are byte-identical, leading comments included. The other two are the `@objectstack/spec` import, which loses `ALL_CONVERSIONS`, and `FLOW_FALLBACK_VERDICT_NOT_REPLAYED`, which gains `export`. Head adds one statement, the import of the new module. - **Size.** +123 / −54 over 2 files, 177 changed lines. `protocol-flow-save-replay.ts` has 115 lines. ## Readers of the moved lines Every test and script that reads `protocol.ts` by path (70 files) was grepped for the member name and for the identifiers its body uses: `ALL_CONVERSIONS`, `applyConversionsToFlow`, `FLOW_FALLBACK_VERDICT_NOT_REPLAYED`, `getOverlayRepo`, `headAt`, `excludeConversionIds`, `includeRetired`, `retiredFromLoadPath`, and the local names. So were all test files in `packages/` and `ui/`, and `docs/qa/`. - **`protocol.view-expansion-reconcile-call-sites.test.ts`** reads `protocol.ts` by path. It holds a census of stored-row WRITES (`repo.put/delete/promoteDraft/restoreVersion(`, raw `sys_metadata` engine writes). It also pins that a repository is bound only as `const repo = this.getOverlayRepo();`. The moved body binds `repo` that way and calls only `repo.headAt(`, a read. So the census loses no write, and no pin edit is owed. Six earlier seam modules already bind `this.getOverlayRepo()` outside `protocol.ts` on the same footing (`protocol-drafts.ts`, `protocol-publish-batch.ts`, `protocol-save-door-refusals.ts` and others). The pin passes at both ends, in the package suite. - **No other by-path reader reads a moved body.** `protocol.driver-text-disclosure.test.ts` and `protocol.object-registry-write-through-spelling.test.ts` match on `getOverlayRepo`, but as an instance override or call, not the moved text. - **Behaviour through the class method, no edit owed.** `packages/rest/src/meta-flow-save-retired-spelling.test.ts` names `flowSaveReplaysRetired` in its header and drives it through the save door. It was run at head after the full build: 1 file, 10 tests passed. - No hit in `docs/qa/`. - **ADR anchors.** `check-adr-anchors` exits 0 with identical output at both ends. The body cites `[#23903, ruling B′]`, an issue citation that moves with it, and no ADR. - **Engine-double contract** (`check-engine-double-contract`). It exits 0 at both ends. The `updateData`/`deleteData` seams keep their entries at new line numbers (13976 → 13930, 14001 → 13955). `productionFiles` is 3,739 at base and 3,741 at head. Of the +2, one is the new file and one is the gitignored `packages/spec/src/migrations/registry.ts` that the spec build writes in the head tree. - **Identical output at both ends:** `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, +1. The base-end readings were taken in a clean detached worktree at `0a1061a61c`. - **Governed surface.** `check-governed-merges --branch claude/project-thread-ybjm3h` finds 0 of 2 paths in the register, so the change is NOT governed. At 177 changed lines it is under the 3,000-line human-merge bar. ## Verification - `tsc --noEmit` in `packages/metadata-protocol` exits 0 before and after the merge. `pnpm exec eslint --no-inline-config packages/metadata-protocol` exits 0 with no output. It covers the whole touched package, which has no `lint` script; these are the root `lint` script's flags. - **`packages/metadata-protocol` suite**, base and head, compared per test name and status. Each run used vitest `--maxWorkers=2` with the JSON reporter under the shared verify lock, and both legs gave `VERDICT command-exit 0`. The runs are on the pre-merge tree; the merge changed no file of the package. - Base `0a1061a61c`: 21,558 tests (21,539 passed, 0 failed, 19 skipped), 269 files. - Head `e6ddd60ba6`: 21,558 tests (21,539 passed, 0 failed, 19 skipped), 269 files. - None lost, none added, none changed status. The base leg is the package `src/` swapped tree-only from `0a1061a61c` (the new module removed) and restored from `HEAD` under a trap. - `check-platform-checklist --changed-from origin/main` on the merged tree: OK, with 1 anchored file in the change set, 29 anchors judged and 0 lost. - **Derived gates.** `dispatch-gates --commands --repo objectstack-ai/objectstack` derives 51 gates on 2 changed paths from the merged tree `5a7ad6a7b2`; all 51 exit 0 after the full `pnpm build` (119/119). `dispatch-gates --ran`: 51 derived, 51 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The derivation flags the tree as behind `main`, but only for `scripts/check-cli-test-child-env.mjs` and `scripts/sdui-manifest.sources/i18n.json`; neither feeds one of these 51 families. Re-deriving against the newer `main` gives the same 51-line list, so `main` was not merged again. ## Carry-forward Unchanged from cut 17. These stay in `protocol.ts`: - Public members whose return type is inferred. - The bodies kept by a reader: `probeRecord`, `getMetaItemCached`, `findReferencesToMeta`, `packagelessAddress`, `refuseUnmintableMetaType`, `metaTypeNamespaceExists`, `viewContainerNameCollisionRefusal` with `overlaidShippedContainerViewNames`, `reportPageRequiresAbsentAtLoad`, the 7 `applyRegistryWriteThrough` callers, `readCodeLayerForCarryForward` and `ensureObjectStorage`. - 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 46255b1 commit 77dc706

2 files changed

Lines changed: 123 additions & 54 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 18] The flow save's retired-spelling replay verdict of
5+
* `ObjectStackProtocolImplementation` (`flowSaveReplaysRetired`, #23903
6+
* ruling B′): whether a flow save may keep the full conversion chain, because
7+
* every retired spelling its body carries is one a stored row already carries,
8+
* moved out of `protocol.ts` whole.
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 retired set, the same
13+
* conversion pass and site naming, and the same stored-row reads in the same
14+
* order.
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 FlowSaveReplayHost} members it reads, plus a `.call`. The overlay
23+
* repository is handed over as a closure that calls the method on the protocol
24+
* at call time, so a test double or spy set on the instance is still the one
25+
* the body reaches. Every other reader of the method keeps calling the class
26+
* method.
27+
*
28+
* What the body reads of `protocol.ts`'s own module-level declarations it
29+
* imports from it; what it imports from elsewhere, it imports from where
30+
* `protocol.ts` imported it. Nothing re-exports this module, so the published
31+
* surface does not move.
32+
*/
33+
34+
import { ALL_CONVERSIONS, applyConversionsToFlow, type ConversionNotice } from '@objectstack/spec';
35+
import { itemDiscriminator } from '@objectstack/metadata-core';
36+
import type { MetadataWriteFace, ObjectStackProtocolImplementation } from './protocol.js';
37+
import { FLOW_FALLBACK_VERDICT_NOT_REPLAYED } from './protocol.js';
38+
39+
/**
40+
* The `ObjectStackProtocolImplementation` members the function below reads —
41+
* every one of them, and no other; the function's `this` is the `Pick` of the
42+
* members it reads. Each is typed AS the class member (an indexed access on
43+
* the class), so a change to a member's signature on the class is a change
44+
* here too, and naming a member the class does not have is a compile error.
45+
*/
46+
export interface FlowSaveReplayHost {
47+
getOverlayRepo: ObjectStackProtocolImplementation['getOverlayRepo'];
48+
}
49+
50+
/** The body of `ObjectStackProtocolImplementation#flowSaveReplaysRetired`; the method's docblock there is its contract. */
51+
export async function flowSaveReplaysRetired(
52+
this: Pick<FlowSaveReplayHost, 'getOverlayRepo'>,
53+
request: {
54+
name: string;
55+
item?: unknown;
56+
packageId?: string | null;
57+
source?: string;
58+
writeFace?: MetadataWriteFace;
59+
},
60+
mode: 'draft' | 'publish'
61+
): Promise<boolean> {
62+
if (request.source !== undefined) return true;
63+
if (request.writeFace === 'package-duplicate' || request.writeFace === 'package-unmanaged-import') return true;
64+
const retired = new Set(ALL_CONVERSIONS.filter((c) => c.retiredFromLoadPath).map((c) => c.id));
65+
// The site of a notice's path, ids standing in for the indexes of the
66+
// array elements that carry one, read on the converted body it names.
67+
const siteOf = (root: unknown, path: string): string => {
68+
let at: unknown = root;
69+
let site = '';
70+
for (const [, key, index] of path.matchAll(/([^.[\]]+)|\[(\d+)\]/g)) {
71+
if (key !== undefined) {
72+
site += site === '' ? key : `.${key}`;
73+
at = at !== null && typeof at === 'object' ? (at as Record<string, unknown>)[key] : undefined;
74+
continue;
75+
}
76+
const element: unknown = Array.isArray(at) ? at[Number(index)] : undefined;
77+
const id = element !== null && typeof element === 'object' ? (element as { id?: unknown }).id : undefined;
78+
site += typeof id === 'string' && id !== '' ? `[#${id}]` : '[' + index + ']';
79+
at = element;
80+
}
81+
return site;
82+
};
83+
const retiredHits = (body: unknown): Set<string> => {
84+
const fired: ConversionNotice[] = [];
85+
const converted = applyConversionsToFlow(body, {
86+
includeRetired: true,
87+
excludeConversionIds: FLOW_FALLBACK_VERDICT_NOT_REPLAYED,
88+
onNotice: (n) => { if (retired.has(n.conversionId)) fired.push(n); },
89+
});
90+
const root = { flows: [converted] };
91+
return new Set(fired.map((n) => `${n.conversionId}|${n.from}|${siteOf(root, n.path)}`));
92+
};
93+
// The request's retired hits; each one a stored row carries is struck off.
94+
const uncarried = retiredHits(request.item);
95+
const strikeCarried = (body: unknown): void => {
96+
if (uncarried.size === 0) return;
97+
for (const hit of retiredHits(body)) uncarried.delete(hit);
98+
};
99+
const repo = this.getOverlayRepo();
100+
const ref = { type: 'flow', name: request.name, org: 'env' } as Parameters<typeof repo.headAt>[0];
101+
const member = itemDiscriminator('flow', request.item);
102+
const state: 'active' | 'draft' = mode === 'draft' ? 'draft' : 'active';
103+
const target = await repo.headAt(ref, { state, packageId: request.packageId ?? null, member });
104+
if (target.head) {
105+
strikeCarried(target.head.body);
106+
if (uncarried.size === 0) return true;
107+
if (state !== 'draft') return false;
108+
} else if (state !== 'draft') {
109+
return false;
110+
}
111+
const active = (await repo.headAt(ref, { state: 'active', packageId: target.packageId, member })).head;
112+
if (!active) return false;
113+
strikeCarried(active.body);
114+
return uncarried.size === 0;
115+
}

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

Lines changed: 8 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL, canonicalMetaUrlType, metaUrlSp
166166
// live transport in through `attachMetadataMutationPubSub`.
167167
import type { IPubSub } from '@objectstack/spec/contracts';
168168
import { RLS_MEMBERSHIP_RESOLVER_SERVICE } from '@objectstack/spec/contracts';
169-
import { ALL_CONVERSIONS, applyConversionsToFlow, applyConversionsToStoredItem, type ConversionNotice, type ConversionTodoNotice } from '@objectstack/spec';
169+
import { applyConversionsToFlow, applyConversionsToStoredItem, type ConversionNotice, type ConversionTodoNotice } from '@objectstack/spec';
170170
import { type FormView, type I18nLabel, isAggregatedViewContainer, expandViewContainer } from '@objectstack/spec/ui';
171171
// [commit ece4dad31] Emitted-specifier pin. This module's inferred public declarations
172172
// structurally mention `FormFieldInput` (FormView `sections[].fields`), and
@@ -400,6 +400,8 @@ import { artifactLockLayerAt, lookupArtifactItem, shippedArtifactsOf, shippingPa
400400
import { namesWithOwnStoredRow, readActiveOverlayRows, readItemActiveRows, storedOverlayEntries } from './protocol-stored-overlay-reads.js';
401401
// [#25467 cut 17] The object-owner verdicts: which contributor kind a stored object row registers as, and handing an adopted object's name to its rebound row.
402402
import { classifyObjectContribution, releaseAdoptedObjectOwner } from './protocol-object-owner.js';
403+
// [#25467 cut 18] The flow save's retired-spelling replay verdict (#23903 ruling B′): does a save newly introduce a retired spelling no stored row carries.
404+
import { flowSaveReplaysRetired } from './protocol-flow-save-replay.js';
403405

404406
/**
405407
* Canonical Zod schema per metadata type lives in
@@ -3356,7 +3358,7 @@ function isoFromValidDate(value: unknown): unknown {
33563358
* predates the flip" is a fact; a save never knows that, exactly as the
33573359
* engine's canonicalizer does not.
33583360
*/
3359-
const FLOW_FALLBACK_VERDICT_NOT_REPLAYED: readonly string[] = [
3361+
export const FLOW_FALLBACK_VERDICT_NOT_REPLAYED: readonly string[] = [
33603362
'flow-decision-mode-inclusive-explicit',
33613363
];
33623364

@@ -6604,59 +6606,11 @@ export class ObjectStackProtocolImplementation implements
66046606
source?: string;
66056607
writeFace?: MetadataWriteFace;
66066608
}, mode: 'draft' | 'publish'): Promise<boolean> {
6607-
if (request.source !== undefined) return true;
6608-
if (request.writeFace === 'package-duplicate' || request.writeFace === 'package-unmanaged-import') return true;
6609-
const retired = new Set(ALL_CONVERSIONS.filter((c) => c.retiredFromLoadPath).map((c) => c.id));
6610-
// The site of a notice's path, ids standing in for the indexes of the
6611-
// array elements that carry one, read on the converted body it names.
6612-
const siteOf = (root: unknown, path: string): string => {
6613-
let at: unknown = root;
6614-
let site = '';
6615-
for (const [, key, index] of path.matchAll(/([^.[\]]+)|\[(\d+)\]/g)) {
6616-
if (key !== undefined) {
6617-
site += site === '' ? key : `.${key}`;
6618-
at = at !== null && typeof at === 'object' ? (at as Record<string, unknown>)[key] : undefined;
6619-
continue;
6620-
}
6621-
const element: unknown = Array.isArray(at) ? at[Number(index)] : undefined;
6622-
const id = element !== null && typeof element === 'object' ? (element as { id?: unknown }).id : undefined;
6623-
site += typeof id === 'string' && id !== '' ? `[#${id}]` : '[' + index + ']';
6624-
at = element;
6625-
}
6626-
return site;
6627-
};
6628-
const retiredHits = (body: unknown): Set<string> => {
6629-
const fired: ConversionNotice[] = [];
6630-
const converted = applyConversionsToFlow(body, {
6631-
includeRetired: true,
6632-
excludeConversionIds: FLOW_FALLBACK_VERDICT_NOT_REPLAYED,
6633-
onNotice: (n) => { if (retired.has(n.conversionId)) fired.push(n); },
6634-
});
6635-
const root = { flows: [converted] };
6636-
return new Set(fired.map((n) => `${n.conversionId}|${n.from}|${siteOf(root, n.path)}`));
6637-
};
6638-
// The request's retired hits; each one a stored row carries is struck off.
6639-
const uncarried = retiredHits(request.item);
6640-
const strikeCarried = (body: unknown): void => {
6641-
if (uncarried.size === 0) return;
6642-
for (const hit of retiredHits(body)) uncarried.delete(hit);
6609+
// [#25467 cut 18] The body moved to `protocol-flow-save-replay.ts` (see `FlowSaveReplayHost`).
6610+
const host: ThisParameterType<typeof flowSaveReplaysRetired> = {
6611+
getOverlayRepo: (...a) => this.getOverlayRepo(...a),
66436612
};
6644-
const repo = this.getOverlayRepo();
6645-
const ref = { type: 'flow', name: request.name, org: 'env' } as Parameters<typeof repo.headAt>[0];
6646-
const member = itemDiscriminator('flow', request.item);
6647-
const state: 'active' | 'draft' = mode === 'draft' ? 'draft' : 'active';
6648-
const target = await repo.headAt(ref, { state, packageId: request.packageId ?? null, member });
6649-
if (target.head) {
6650-
strikeCarried(target.head.body);
6651-
if (uncarried.size === 0) return true;
6652-
if (state !== 'draft') return false;
6653-
} else if (state !== 'draft') {
6654-
return false;
6655-
}
6656-
const active = (await repo.headAt(ref, { state: 'active', packageId: target.packageId, member })).head;
6657-
if (!active) return false;
6658-
strikeCarried(active.body);
6659-
return uncarried.size === 0;
6613+
return flowSaveReplaysRetired.call(host, request, mode);
66606614
}
66616615

66626616
/**

0 commit comments

Comments
 (0)