Skip to content

Commit c7df09f

Browse files
committed
refactor(objectql): move the migration-flag and attestation bodies out of engine.ts into engine-migration-flags.ts
Eight ObjectQL method bodies of the "Migration flags and attestations" section move unedited into a new module behind one named host, MigrationFlagsHost: supplyTenantKeyCertifiedResolver, isOrganizationOwnershipCertified, readFileReferencesFlagRow, isValueShapesMigrationVerified, readMigrationFlagVerified, noteAdmittedValueShapeViolation, announceOpenMigrationGates and wasDatastoreCreatedFromEmpty. The class keeps each method's name, modifiers, signature, docblock and position; fields go as getters, the one field a moved body assigns (migrationGatesAnnounced) as a getter and setter pair over the instance, methods as late-bound closures. MigrationFlagRead gains `export` in place so the module can name it as a type. No behaviour changes. Two stale path readers of cut 12 are corrected: the header of engine-realtime-publish.ts (accessibleOrganizationsOf now lives in engine-driver-options.ts), and the engine-execute-transactions.ts ADR anchor entry, which drops the buildDriverOptions half of the transaction-seam paragraph that engine-driver-options.ts's entry now carries. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wzm9du2KfnvCibhjZZYgyq
1 parent 756220c commit c7df09f

4 files changed

Lines changed: 364 additions & 163 deletions

File tree

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25522 cut 13] The migration-flag and attestation bodies of `ObjectQL`,
5+
* moved out of `engine.ts`: `supplyTenantKeyCertifiedResolver` (hand a driver
6+
* the "is this database certified" question), `isOrganizationOwnershipCertified`
7+
* (that question's answer), `readFileReferencesFlagRow` and
8+
* `isValueShapesMigrationVerified` (the two memoized flag readers),
9+
* `readMigrationFlagVerified` (the one `sys_migration` row read behind them),
10+
* `noteAdmittedValueShapeViolation` (tally a violation this boot admitted),
11+
* `announceOpenMigrationGates` (the once-per-boot open-gate advisory) and
12+
* `wasDatastoreCreatedFromEmpty` (did this process create its store).
13+
*
14+
* A MOVE, not a redesign. Each exported function below is the body of the
15+
* `ObjectQL` method of the same name as it stood, re-indented one level and
16+
* otherwise unedited. Each method's docblock stays on the method in
17+
* `engine.ts`, which is still the contract. ADR-0104 and ADR-0131 govern what
18+
* these bodies decide; their decisions are cited on those docblocks.
19+
*
20+
* What stays in `engine.ts`: `isFileReferencesMigrationVerified` (ADR-0104
21+
* names it by path), `recordObservedDeviation` and `retractCreationAttestation`
22+
* (each writes through `this.update`), the bodies too short to pay for a host
23+
* (`haveFileColumnsMoved`, `readMigrationFlagMemoized`,
24+
* `readMigrationFlagRowMemoized`, `valueShapeViolationsAdmitted`,
25+
* `admittedViolationSink`, `invalidateDataMigrationFlags`), and every field
26+
* of the section, each declared where it was.
27+
*
28+
* ## How the moved bodies reach the class
29+
*
30+
* The shape #23050 set for `rest-server.ts`, applied per method as
31+
* `engine-driver-options.ts` applies it. The class keeps all eight methods
32+
* with name, modifiers, signature, docblock and position, and each body is
33+
* one literal of exactly the {@link MigrationFlagsHost} members that method
34+
* reads, plus a `.call`. The fields a body reads (`logger`, `_registry`,
35+
* `admittedValueShapeViolations`, `migrationGatesAnnounced`, `drivers`) go as
36+
* GETTERS, so each read happens when the body makes it, as it did. The one
37+
* field a body ASSIGNS, `migrationGatesAnnounced`, goes as a getter and
38+
* SETTER pair over the instance, so the write lands on the engine and the
39+
* next read, from either side, sees it. The methods they call go as
40+
* late-bound closures over the instance, so a double or spy set on the engine
41+
* is still the one a call reaches.
42+
*
43+
* This module imports the engine only as a TYPE, so there is no module cycle
44+
* at run time, and nothing re-exports it, so the published surface does not
45+
* move.
46+
*/
47+
48+
import {
49+
DATA_MIGRATION_FLAG_OBJECT,
50+
FILE_REFERENCES_MIGRATION_ID,
51+
VALUE_SHAPES_MIGRATION_ID,
52+
ORGANIZATION_OWNERSHIP_MIGRATION_ID,
53+
ORGANIZATION_OWNERSHIP_SHAPE_GENERATION,
54+
organizationOwnershipShapeGenerationOf,
55+
isDataMigrationFlagVerified,
56+
hasMovedFileColumns,
57+
} from '@objectstack/spec/system';
58+
import type { ExecutionContext } from '@objectstack/spec/kernel';
59+
import type { IDataDriver } from '@objectstack/core';
60+
import { isMissingTableError } from '@objectstack/metadata/errors';
61+
import { valueShapePostureSetByEnv, mediaPostureSetByEnv } from './validation/record-validator.js';
62+
import type { AdmittedValueShapeViolation } from './validation/record-validator.js';
63+
import type { MigrationFlagRead, ObjectQL } from './engine.js';
64+
65+
/**
66+
* The `ObjectQL` members the functions below read — every one of them, and no
67+
* other; each function's `this` is the `Pick` of the members IT reads. Each is
68+
* typed AS the class member (an indexed access on the class), so a change to a
69+
* member's type on the class is a change here too, and naming a member the
70+
* class does not have is a compile error. See the module header for which
71+
* members are getters, and which one also has a setter.
72+
*/
73+
export interface MigrationFlagsHost {
74+
readonly logger: ObjectQL['logger'];
75+
readonly _registry: ObjectQL['_registry'];
76+
readonly admittedValueShapeViolations: ObjectQL['admittedValueShapeViolations'];
77+
migrationGatesAnnounced: ObjectQL['migrationGatesAnnounced'];
78+
readonly drivers: ObjectQL['drivers'];
79+
isOrganizationOwnershipCertified: ObjectQL['isOrganizationOwnershipCertified'];
80+
findOne: ObjectQL['findOne'];
81+
readMigrationFlagRowMemoized: ObjectQL['readMigrationFlagRowMemoized'];
82+
readMigrationFlagMemoized: ObjectQL['readMigrationFlagMemoized'];
83+
recordObservedDeviation: ObjectQL['recordObservedDeviation'];
84+
retractCreationAttestation: ObjectQL['retractCreationAttestation'];
85+
objectHasMediaField: ObjectQL['objectHasMediaField'];
86+
objectHasCoveredValueField: ObjectQL['objectHasCoveredValueField'];
87+
readMigrationFlagVerified: ObjectQL['readMigrationFlagVerified'];
88+
}
89+
90+
/** The body of `ObjectQL#supplyTenantKeyCertifiedResolver`; the method's docblock there is its contract. */
91+
export function supplyTenantKeyCertifiedResolver(
92+
this: Pick<MigrationFlagsHost, 'isOrganizationOwnershipCertified' | 'logger'>,
93+
driver: IDataDriver,
94+
): void {
95+
const sink = driver as unknown as {
96+
setTenantKeyCertifiedResolver?: (resolve: () => Promise<boolean>) => void;
97+
};
98+
if (typeof sink.setTenantKeyCertifiedResolver !== 'function') return;
99+
try {
100+
sink.setTenantKeyCertifiedResolver(() => this.isOrganizationOwnershipCertified());
101+
} catch (e: any) {
102+
// A driver that did not take the resolver answers "not certified": it
103+
// keeps the two forms distinct and REPORTS a destructive recreate_index,
104+
// which is never applied unattended (safe-only boot reconcile, the
105+
// artifact gate, `os migrate apply --allow-destructive`).
106+
this.logger.debug('Driver declined the ADR-0131 certification resolver', {
107+
driverName: driver.name,
108+
error: e?.message ?? String(e),
109+
});
110+
}
111+
}
112+
113+
/** The body of `ObjectQL#isOrganizationOwnershipCertified`; the method's docblock there is its contract. */
114+
export async function isOrganizationOwnershipCertified(
115+
this: Pick<MigrationFlagsHost, '_registry' | 'findOne'>,
116+
): Promise<boolean> {
117+
if (!this._registry.getObject(DATA_MIGRATION_FLAG_OBJECT)) return false;
118+
try {
119+
const row: any = await this.findOne(DATA_MIGRATION_FLAG_OBJECT, {
120+
where: { id: ORGANIZATION_OWNERSHIP_MIGRATION_ID },
121+
context: { isSystem: true } as ExecutionContext,
122+
});
123+
if (!row || row.id !== ORGANIZATION_OWNERSHIP_MIGRATION_ID) return false;
124+
const verified = isDataMigrationFlagVerified({
125+
id: ORGANIZATION_OWNERSHIP_MIGRATION_ID,
126+
last_run_at: String(row.last_run_at ?? ''),
127+
verified_at: row.verified_at == null ? null : String(row.verified_at),
128+
blocking: typeof row.blocking === 'number' ? row.blocking : Number(row.blocking ?? Number.NaN),
129+
});
130+
return verified && organizationOwnershipShapeGenerationOf(row.details) >= ORGANIZATION_OWNERSHIP_SHAPE_GENERATION;
131+
} catch (error) {
132+
// A store whose ledger table does not exist yet has no certificate.
133+
if (isMissingTableError(error, DATA_MIGRATION_FLAG_OBJECT)) return false;
134+
throw error;
135+
}
136+
}
137+
138+
/** The body of `ObjectQL#readFileReferencesFlagRow`; the method's docblock there is its contract. */
139+
export function readFileReferencesFlagRow(
140+
this: Pick<MigrationFlagsHost, 'readMigrationFlagRowMemoized'>,
141+
): Promise<MigrationFlagRead> {
142+
return this.readMigrationFlagRowMemoized(
143+
'fileReferencesMigrationVerified',
144+
FILE_REFERENCES_MIGRATION_ID,
145+
'[value-shape] this deployment has verified the file-as-reference migration — ' +
146+
'media value shapes are enforced and released field files may be collected ' +
147+
'(ADR-0104)',
148+
);
149+
}
150+
151+
/** The body of `ObjectQL#isValueShapesMigrationVerified`; the method's docblock there is its contract. */
152+
export async function isValueShapesMigrationVerified(
153+
this: Pick<MigrationFlagsHost, 'readMigrationFlagMemoized'>,
154+
): Promise<boolean> {
155+
return this.readMigrationFlagMemoized(
156+
'valueShapesMigrationVerified',
157+
VALUE_SHAPES_MIGRATION_ID,
158+
'[value-shape] this deployment has verified the value-shape scan — reference and ' +
159+
'structured-JSON value shapes are enforced (ADR-0104)',
160+
);
161+
}
162+
163+
/** The body of `ObjectQL#readMigrationFlagVerified`; the method's docblock there is its contract. */
164+
export async function readMigrationFlagVerified(
165+
this: Pick<MigrationFlagsHost, '_registry' | 'findOne' | 'logger'>,
166+
migrationId: string,
167+
verifiedLog?: string,
168+
): Promise<MigrationFlagRead> {
169+
if (!this._registry.getObject(DATA_MIGRATION_FLAG_OBJECT)) {
170+
return { verified: false, conclusive: false, columnsMoved: false };
171+
}
172+
try {
173+
// [#20648] The row by primary key, through the single-row route. ⛔ Not
174+
// `find` with `limit: 1`: the SQL driver reads that as page one of a
175+
// walk, and this read runs at boot BEFORE `sys_migration` is registered
176+
// with the driver — so every boot of an existing deployment printed the
177+
// driver's "Paged read ... is NOT deterministic" warning for a lookup
178+
// that cannot return two rows. `findOne` is the route it exempts.
179+
const row: any = await this.findOne(DATA_MIGRATION_FLAG_OBJECT, {
180+
where: { id: migrationId },
181+
context: { isSystem: true } as ExecutionContext,
182+
});
183+
if (!row || row.id !== migrationId) {
184+
return { verified: false, conclusive: true, columnsMoved: false };
185+
}
186+
const flag = {
187+
id: migrationId,
188+
last_run_at: String(row.last_run_at ?? ''),
189+
verified_at: row.verified_at == null ? null : String(row.verified_at),
190+
// A non-numeric count must read as "not zero", not as 0 — a bad
191+
// coercion lands on NaN, which fails the === 0 test.
192+
blocking: typeof row.blocking === 'number' ? row.blocking : Number(row.blocking ?? Number.NaN),
193+
// [#15989] Read here so both questions come off ONE row: an absent or
194+
// empty stamp is the JSON encoding, which is what every row written
195+
// before the column step existed holds.
196+
columns_moved_at: row.columns_moved_at == null ? null : String(row.columns_moved_at),
197+
};
198+
const verified = isDataMigrationFlagVerified(flag);
199+
if (verified && verifiedLog) this.logger.info(verifiedLog);
200+
return { verified, conclusive: true, columnsMoved: hasMovedFileColumns(flag) };
201+
} catch {
202+
// unreadable evidence → stay lenient, keep asking; and the columns read
203+
// as NOT moved, which is the encoding every deployment already writes.
204+
return { verified: false, conclusive: false, columnsMoved: false };
205+
}
206+
}
207+
208+
/** The body of `ObjectQL#noteAdmittedValueShapeViolation`; the method's docblock there is its contract. */
209+
export function noteAdmittedValueShapeViolation(
210+
this: Pick<MigrationFlagsHost, 'admittedValueShapeViolations' | 'recordObservedDeviation' | 'retractCreationAttestation'>,
211+
object: string,
212+
violation: AdmittedValueShapeViolation,
213+
): void {
214+
const migrationId =
215+
violation.gate === 'media' ? FILE_REFERENCES_MIGRATION_ID : VALUE_SHAPES_MIGRATION_ID;
216+
const existing = this.admittedValueShapeViolations.get(migrationId);
217+
if (existing) {
218+
existing.count += 1;
219+
} else {
220+
this.admittedValueShapeViolations.set(migrationId, {
221+
count: 1,
222+
first: { object, field: violation.field, type: violation.type, detail: violation.detail },
223+
});
224+
}
225+
// Two consumers of the same counterexample, answering different questions.
226+
// The deviation marker goes first because it is the one that applies to
227+
// EVERY verified deployment (#4797); the retraction below applies only to
228+
// the narrow fresh-datastore case (#4769) and closes the gate outright
229+
// there. Each is the other's backstop if one write fails.
230+
this.recordObservedDeviation(migrationId);
231+
this.retractCreationAttestation(migrationId);
232+
}
233+
234+
/** The body of `ObjectQL#announceOpenMigrationGates`; the method's docblock there is its contract. */
235+
export async function announceOpenMigrationGates(
236+
this: Pick<MigrationFlagsHost, 'migrationGatesAnnounced' | '_registry' | 'objectHasMediaField' | 'objectHasCoveredValueField' | 'readMigrationFlagVerified' | 'logger'>,
237+
): Promise<void> {
238+
if (this.migrationGatesAnnounced) return;
239+
this.migrationGatesAnnounced = true;
240+
try {
241+
const mediaByEnv = mediaPostureSetByEnv();
242+
const coveredByEnv = valueShapePostureSetByEnv();
243+
if (mediaByEnv && coveredByEnv) return;
244+
245+
let media = false;
246+
let covered = false;
247+
for (const obj of this._registry.getAllObjects() as any[]) {
248+
if (!media && !mediaByEnv && this.objectHasMediaField(obj)) media = true;
249+
if (!covered && !coveredByEnv && this.objectHasCoveredValueField(obj)) covered = true;
250+
if ((media || mediaByEnv) && (covered || coveredByEnv)) break;
251+
}
252+
253+
// [#4769] Read the ledger, not the memo. This advisory runs once, at
254+
// `kernel:bootstrapped`, and the fresh-datastore attestation may have
255+
// written its rows moments earlier — after the boot's first write had
256+
// already memoized "not verified". Reporting from that memo tells a
257+
// brand-new deployment to run a migration whose gate is already closed.
258+
// One query per applicable gate, once per boot.
259+
if (media && !(await this.readMigrationFlagVerified(FILE_REFERENCES_MIGRATION_ID)).verified) {
260+
this.logger.info(
261+
'[value-shape] media values are checked but NOT enforced here, and released files are ' +
262+
'never collected — this deployment has not verified its file migration. Run ' +
263+
'`os migrate files-to-references` (dry run) to see what it would do, then `--apply` ' +
264+
'to close the gate (ADR-0104).',
265+
);
266+
}
267+
if (covered && !(await this.readMigrationFlagVerified(VALUE_SHAPES_MIGRATION_ID)).verified) {
268+
this.logger.info(
269+
'[value-shape] reference and structured-JSON values are checked but NOT enforced here — ' +
270+
'this deployment has not verified its value-shape scan. Run `os migrate value-shapes` ' +
271+
'(dry run) to see what it would report, then `--apply` to close the gate ' +
272+
'(ADR-0104).',
273+
);
274+
}
275+
} catch {
276+
// An advisory must never be the reason a boot fails.
277+
}
278+
}
279+
280+
/** The body of `ObjectQL#wasDatastoreCreatedFromEmpty`; the method's docblock there is its contract. */
281+
export function wasDatastoreCreatedFromEmpty(
282+
this: Pick<MigrationFlagsHost, 'drivers'>,
283+
): boolean {
284+
let created = 0;
285+
let existing = 0;
286+
let reporting = 0;
287+
for (const driver of this.drivers.values()) {
288+
const stats = (driver as any).getSchemaSyncStats?.();
289+
if (!stats) continue;
290+
reporting += 1;
291+
created += Number(stats.created) || 0;
292+
existing += Number(stats.existing) || 0;
293+
}
294+
return reporting > 0 && existing === 0 && created > 0;
295+
}

‎packages/objectql/src/engine-realtime-publish.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
* docblocks included; no other code in `engine.ts` called them.
1717
*
1818
* What stays in `engine.ts`: `setRealtimeService` (a two-line field setter),
19-
* the `realtimeService` field itself and the three write-path call sites, and
19+
* the `realtimeService` field itself and the three write-path call sites.
2020
* `accessibleOrganizationsOf`, which sat between the helpers but is read by the
21-
* tenant wall, not by an event.
21+
* tenant wall, not by an event, now lives in `engine-driver-options.ts`.
2222
*
2323
* ## How the moved bodies reach the class
2424
*

0 commit comments

Comments
 (0)