Skip to content

Commit a04a2c7

Browse files
committed
refactor(metadata-protocol): move 21 private member bodies out of protocol.ts into seven themed modules
Move-only: each body moves whole into a protocol-*.ts module, re-indented one level and otherwise unedited; the class keeps each method with its docblock and signature, and its body hands a host of exactly the members it reads to the moved function. Two module-level helpers gain export in place, and the imports only the moved bodies read move with them. Claude-Session: https://claude.ai/code/session_01L6bhgVeV5GgzaH9Ny16aSx Co-authored-by: Claude <noreply@anthropic.com>
1 parent c5d4634 commit a04a2c7

8 files changed

Lines changed: 881 additions & 289 deletions
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 33] Two degradations said out loud, from
5+
* `ObjectStackProtocolImplementation`, moved out of `protocol.ts` whole:
6+
*
7+
* - `throwMetadataServiceUnavailable` — a degraded MetadataService read
8+
* turned into the same 503 the overlay half of the reads throws;
9+
* - `warnClosureReadUnavailable` — why the batch closure degraded, once per
10+
* process.
11+
*
12+
* A MOVE, not a redesign. Each function below is the body of the
13+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
14+
* re-indented one level and otherwise unedited.
15+
*
16+
* ## How the moved bodies reach 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 each 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 LoudDegradationsHost} members it reads, plus a `.call`; a body
23+
* that reads no member is called as a plain function. The warned-once flag is
24+
* handed over as a getter and a setter, so the read and the write reach the
25+
* class field as it stands at call time. Every caller keeps calling the class
26+
* methods.
27+
*
28+
* What the bodies read from elsewhere, they import from where `protocol.ts`
29+
* imports it; `metadataStoreUnavailableError` is `protocol.ts`'s own and is imported from there. Nothing re-exports this module, so the published surface does
30+
* not move.
31+
*/
32+
33+
import type { ObjectStackProtocolImplementation } from './protocol.js';
34+
import { metadataStoreUnavailableError } from './protocol.js';
35+
36+
/**
37+
* The `ObjectStackProtocolImplementation` members the functions below read —
38+
* every one of them, and no other; each function's `this` is the `Pick` of
39+
* the members it reads. Each is typed AS the class member (an indexed access
40+
* on the class), so a change to a member's signature on the class is a change
41+
* here too, and naming a member the class does not have is a compile error.
42+
*/
43+
export interface LoudDegradationsHost {
44+
closureReadWarned: ObjectStackProtocolImplementation['closureReadWarned'];
45+
}
46+
47+
/** The body of `ObjectStackProtocolImplementation#throwMetadataServiceUnavailable`; the method's docblock there is its contract. */
48+
export function throwMetadataServiceUnavailable(errors: string[]): never {
49+
throw metadataStoreUnavailableError(
50+
new Error(
51+
`The metadata service could not read every loader: ${
52+
errors.length > 0 ? errors.join('; ') : 'no loader detail reported'
53+
}`,
54+
),
55+
);
56+
}
57+
58+
/** The body of `ObjectStackProtocolImplementation#warnClosureReadUnavailable`; the method's docblock there is its contract. */
59+
export function warnClosureReadUnavailable(this: Pick<LoudDegradationsHost, 'closureReadWarned'>, type: string, name: string): void {
60+
if (this.closureReadWarned) return;
61+
this.closureReadWarned = true;
62+
console.warn(
63+
`[Protocol] publishPackageDrafts: this overlay repository declares no 'get', so the batch's own `
64+
+ `pending drafts cannot be read (first reached at ${type}/${name}). Author-time validation falls `
65+
+ `back to the LIVE declarations only, without this batch's own drafts in the closure, so a draft that references a `
66+
+ `sibling drafted in the SAME batch may be refused as unresolved. Nothing is published unchecked: `
67+
+ `the gate still runs, with strictly less resolution context.`,
68+
);
69+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 33] `sys_metadata`'s overlay-uniqueness indexes and the
5+
* bundle-member backfill they need first, each run once per process, from
6+
* `ObjectStackProtocolImplementation`, moved out of `protocol.ts` whole:
7+
* `ensureOverlayIndex` and `ensureOverlayMembersBackfilled`.
8+
*
9+
* A MOVE, not a redesign. Each function below is the body of the
10+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
11+
* re-indented one level and otherwise unedited.
12+
*
13+
* ## How the moved bodies reach the class
14+
*
15+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
16+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
17+
* The class keeps each method — the published `dist/*.d.ts` lists its name,
18+
* docblock and declared signature — and its body is one literal of exactly
19+
* the {@link OverlayIndexHost} members it reads, plus a `.call`; a body that
20+
* reads no member is called as a plain function. The `engine` field and the
21+
* two run-once flags are handed over as getters (and the flags as setters),
22+
* so each read and write reaches the class field as it stands at call time;
23+
* the call to `ensureOverlayMembersBackfilled` is a closure, so it still
24+
* reaches the class method. Every caller keeps calling the class methods.
25+
*
26+
* What the bodies read from elsewhere, they import from where `protocol.ts`
27+
* imports it. Nothing re-exports this module, so the published surface does
28+
* not move.
29+
*/
30+
31+
import { driverCanRunSql, resolveDriverExec } from './migrations/driver-exec.js';
32+
import { backfillOverlayBundleMembers, ensureMetadataOverlayIndexes } from './migrations/overlay-index.js';
33+
import type { ObjectStackProtocolImplementation } from './protocol.js';
34+
35+
/**
36+
* The `ObjectStackProtocolImplementation` members the functions below read —
37+
* every one of them, and no other; each function's `this` is the `Pick` of
38+
* the members it reads. Each is typed AS the class member (an indexed access
39+
* on 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 OverlayIndexHost {
43+
readonly engine: ObjectStackProtocolImplementation['engine'];
44+
readonly ensureOverlayMembersBackfilled: ObjectStackProtocolImplementation['ensureOverlayMembersBackfilled'];
45+
overlayIndexEnsured: ObjectStackProtocolImplementation['overlayIndexEnsured'];
46+
overlayMembersBackfilled: ObjectStackProtocolImplementation['overlayMembersBackfilled'];
47+
}
48+
49+
/** The body of `ObjectStackProtocolImplementation#ensureOverlayIndex`. */
50+
export async function ensureOverlayIndex(this: Pick<OverlayIndexHost, 'engine' | 'ensureOverlayMembersBackfilled' | 'overlayIndexEnsured'>): Promise<void> {
51+
if (this.overlayIndexEnsured) return;
52+
this.overlayIndexEnsured = true;
53+
// [#22877] Rows stored before the member column existed get their
54+
// body's member before the first write can address them.
55+
await this.ensureOverlayMembersBackfilled();
56+
try {
57+
const engineAny = this.engine as any;
58+
let driver: any = engineAny?.driver ?? engineAny?.getDriver?.();
59+
if (!driver && engineAny?.drivers instanceof Map) {
60+
for (const candidate of engineAny.drivers.values()) {
61+
if (driverCanRunSql(candidate)) {
62+
driver = candidate;
63+
break;
64+
}
65+
}
66+
}
67+
if (!driver) return;
68+
const resolved = resolveDriverExec(driver);
69+
// The refusal stays INSIDE the seam rather than becoming an early
70+
// return: a driver taken from `engineAny?.driver` is not
71+
// capability-checked above, and when it offers neither surface it is
72+
// the migration that classifies and reports the failure — an early
73+
// return here would put that back into the silent `catch` below.
74+
const exec = async (sql: string): Promise<void> => {
75+
if (!resolved) throw new Error('driver has neither raw nor execute');
76+
await resolved(sql);
77+
};
78+
// `console` satisfies the logger surface structurally; this class
79+
// carries no injected logger, and its own diagnostics go to
80+
// `console.warn` throughout (see `emitMetadataMutation`).
81+
await ensureMetadataOverlayIndexes(exec, console);
82+
} catch {
83+
// A boot must never fail over an index. Note this arm is now only
84+
// reachable for driver RESOLUTION failures: every DDL failure past
85+
// this point is classified and reported by the migration itself,
86+
// instead of vanishing into an empty catch (#6418).
87+
}
88+
}
89+
90+
/** The body of `ObjectStackProtocolImplementation#ensureOverlayMembersBackfilled`. */
91+
export async function ensureOverlayMembersBackfilled(this: Pick<OverlayIndexHost, 'engine' | 'overlayMembersBackfilled'>): Promise<void> {
92+
if (this.overlayMembersBackfilled) return;
93+
this.overlayMembersBackfilled = true;
94+
await backfillOverlayBundleMembers(this.engine as any, console);
95+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 33] The ADR-0067 commit row that groups a turn's published
5+
* artifacts, from `ObjectStackProtocolImplementation`, moved out of
6+
* `protocol.ts` whole: `recordPackageCommit` and the named INSERT it calls,
7+
* `persistPackageCommitRow` (named so the durability log-level rule can
8+
* declare it).
9+
*
10+
* A MOVE, not a redesign. Each 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.
13+
*
14+
* ## How the moved bodies reach the class
15+
*
16+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
17+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
18+
* The class keeps each method — the published `dist/*.d.ts` lists its name,
19+
* docblock and declared signature — and its body is one literal of exactly
20+
* the {@link PackageCommitRecordHost} members it reads, plus a `.call`; a
21+
* body that reads no member is called as a plain function. The `engine` field
22+
* and the noted-once flag are handed over as getters (and the flag as a
23+
* setter), so each read and write reaches the class field as it stands at
24+
* call time; the call to `persistPackageCommitRow` is a closure, so it still
25+
* reaches the class method. Every caller keeps calling the class methods.
26+
*
27+
* What the bodies read from elsewhere, they import from where `protocol.ts`
28+
* imports it. Nothing re-exports this module, so the published surface does
29+
* not move.
30+
*/
31+
32+
import { isMissingTableError } from '@objectstack/metadata/errors';
33+
import type { CommitPlanItem, ObjectStackProtocolImplementation } from './protocol.js';
34+
35+
/**
36+
* The `ObjectStackProtocolImplementation` members the functions below read —
37+
* every one of them, and no other; each function's `this` is the `Pick` of
38+
* the members it reads. Each is typed AS the class member (an indexed access
39+
* on 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 PackageCommitRecordHost {
43+
commitStoreUnprovisionedNoted: ObjectStackProtocolImplementation['commitStoreUnprovisionedNoted'];
44+
readonly engine: ObjectStackProtocolImplementation['engine'];
45+
readonly persistPackageCommitRow: ObjectStackProtocolImplementation['persistPackageCommitRow'];
46+
}
47+
48+
/** The body of `ObjectStackProtocolImplementation#persistPackageCommitRow`; the method's docblock there is its contract. */
49+
export async function persistPackageCommitRow(this: Pick<PackageCommitRecordHost, 'engine'>, row: Record<string, unknown>): Promise<void> {
50+
// [#21911] The explicit system opt-in — see recordMetadataAudit.
51+
await this.engine.insert('sys_metadata_commit', row, { context: { isSystem: true } });
52+
}
53+
54+
/** The body of `ObjectStackProtocolImplementation#recordPackageCommit`; the method's docblock there is its contract. */
55+
export async function recordPackageCommit(this: Pick<PackageCommitRecordHost, 'commitStoreUnprovisionedNoted' | 'persistPackageCommitRow'>, args: {
56+
packageId: string;
57+
operation: 'apply' | 'revert';
58+
message?: string;
59+
actor?: string;
60+
aiModel?: string;
61+
parentCommitId?: string;
62+
items: CommitPlanItem[];
63+
eventSeqStart?: number;
64+
eventSeqEnd?: number;
65+
}): Promise<{ commitId: string } | null> {
66+
try {
67+
const commitId = 'cmt_' + (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
68+
? crypto.randomUUID()
69+
: `${args.eventSeqEnd ?? 0}-${args.items.length}-${args.packageId}`);
70+
await this.persistPackageCommitRow({
71+
id: commitId,
72+
package_id: args.packageId,
73+
operation: args.operation,
74+
...(args.message ? { message: args.message } : {}),
75+
...(args.actor ? { actor: args.actor } : {}),
76+
...(args.aiModel ? { ai_model: args.aiModel } : {}),
77+
...(args.parentCommitId ? { parent_commit_id: args.parentCommitId } : {}),
78+
...(args.eventSeqStart !== undefined ? { event_seq_start: args.eventSeqStart } : {}),
79+
...(args.eventSeqEnd !== undefined ? { event_seq_end: args.eventSeqEnd } : {}),
80+
items: JSON.stringify(args.items),
81+
item_count: args.items.length,
82+
created_at: new Date().toISOString(),
83+
});
84+
return { commitId };
85+
} catch (error) {
86+
// [#9066] The publish keeps its outcome — `null` still comes back
87+
// and the caller still reports success — but the silence is over.
88+
//
89+
// BENIGN: `sys_metadata_commit` was never provisioned. That is a
90+
// real deployment state (a first boot, or an env kernel composed
91+
// without the commit log — the exact case
92+
// `@objectstack/metadata`'s plugin note records), and it is a
93+
// CONFIGURATION fact, not a store that broke: it is the same on
94+
// every publish and it is fixed in one place. Stated once per
95+
// instance, at `info`, per this card's ruling that an unprovisioned
96+
// commit store stays silent or informational.
97+
//
98+
// EVERYTHING ELSE — a connection drop, a timeout, a permission
99+
// denial, schema drift on that one table — is a write that was
100+
// supposed to land and did not, while the publish it describes DID.
101+
// `error`, per AGENTS.md "Degradation log levels", and the line
102+
// owes the two things that rule requires: the CONSEQUENCE (this
103+
// turn is not revertible, and the system will keep looking healthy)
104+
// and the FIX (repair write access to the commit store).
105+
//
106+
// NOT rethrown, and not surfaced to the caller: the artifacts are
107+
// already live and unwinding them over a missing history row would
108+
// be strictly worse than losing the row. Telling the CALLER that
109+
// the turn is unrevertible is a separate question (a response-field
110+
// change the #8896 ruling forbids for this family) and deliberately
111+
// NOT decided here.
112+
if (isMissingTableError(error, 'sys_metadata_commit')) {
113+
if (!this.commitStoreUnprovisionedNoted) {
114+
this.commitStoreUnprovisionedNoted = true;
115+
console.info(
116+
'[Protocol] sys_metadata_commit is not provisioned — publishes and reverts '
117+
+ 'succeed but record no ADR-0067 commit row, so no turn can be reverted '
118+
+ '(the revert plan of every turn is dropped). Fix: provision the commit '
119+
+ 'store — it is registered alongside sys_metadata_history by '
120+
+ "@objectstack/metadata's plugin; run schema sync. Said once per protocol instance.",
121+
);
122+
}
123+
return null;
124+
}
125+
const reason = (error as { message?: string } | undefined)?.message ?? String(error);
126+
console.error(
127+
`[Protocol] sys_metadata_commit write FAILED for package '${args.packageId}' `
128+
+ `(${args.operation}, ${args.items.length} item(s)): ${reason}. `
129+
+ 'The publish itself SUCCEEDED and reports success, so nothing looks broken — but '
130+
+ "this turn's revert plan was not persisted: the turn is absent from the package "
131+
+ 'commit timeline and can never be reverted, and every further publish loses its '
132+
+ 'own the same way until this is repaired. Fix: restore write access to '
133+
+ 'sys_metadata_commit (connectivity, permissions, or schema drift on that table).',
134+
);
135+
return null;
136+
}
137+
}

0 commit comments

Comments
 (0)