@objectstack/core 17.0.0-rc.1 → 17.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { Logger, LifecycleEventName, IServiceRegistry } from '@objectstack/spec/contracts';
1
+ import { Logger, LifecycleEventName, IServiceRegistry, IObjectQLEngine } from '@objectstack/spec/contracts';
2
2
  export { EngineSchemaRegistryView, IDataDriver, IDataEngine, IHttpRequest, IHttpResponse, IHttpServer, IObjectQLEngine, Logger, Middleware, RouteHandler } from '@objectstack/spec/contracts';
3
3
  import { z } from 'zod';
4
- import { LoggerConfig } from '@objectstack/spec/system';
4
+ import { LoggerConfig, MigrationOnCrashPolicy, MigrationJournalEvent } from '@objectstack/spec/system';
5
5
  import { ObjectLogger } from './logger.cjs';
6
6
  export { createLogger } from './logger.cjs';
7
7
  import { ConflictResolutionStrategy, ApiRegistryEntryInput, ApiRegistryEntry, ApiDiscoveryQuery, ApiDiscoveryResponse, ApiEndpointRegistration, ApiRegistry as ApiRegistry$1 } from '@objectstack/spec/api';
@@ -297,6 +297,30 @@ declare class ObjectKernel {
297
297
  */
298
298
  getState(): string;
299
299
  private initPluginWithTimeout;
300
+ /**
301
+ * Race a plugin lifecycle hook against its startup-timeout guard, and
302
+ * reclaim the guard the moment the race settles (#4813).
303
+ *
304
+ * The guard used to be armed and then abandoned: when the plugin won the
305
+ * race, its `setTimeout` stayed ref'd in the event loop for the full
306
+ * `startupTimeout`, so every process idled that long after its work was
307
+ * done. One `os migrate` finished in 3s and then sat for 120s
308
+ * (`ObjectQLPlugin.startupTimeout`), held open by 8 orphaned guards — one
309
+ * per init plus one per start.
310
+ *
311
+ * Clearing on settle rather than `unref()`-ing at arm time is deliberate.
312
+ * An unref'd guard also stops pinning the loop, but it stops being a guard
313
+ * as well: if the hook never settles and nothing else keeps the loop alive,
314
+ * Node exits before the timer can fire and the timeout is never reported.
315
+ * The guard has to stay ref'd exactly as long as the race is undecided,
316
+ * which is what `clearTimeout` in a `finally` expresses.
317
+ *
318
+ * `operation` is widened to `T | PromiseLike<T>` because the Plugin
319
+ * contract permits a synchronous hook (`init`/`start` return
320
+ * `void | Promise<void>`); such a hook wins the race immediately and the
321
+ * guard is reclaimed on the same turn.
322
+ */
323
+ private raceStartupTimeout;
300
324
  /**
301
325
  * Whether a service is resolvable on this kernel right now — direct
302
326
  * registration or a loader-registered factory. Backs the init-service
@@ -605,6 +629,16 @@ declare abstract class ObjectKernelBase {
605
629
  * Declare only unconditional registrations: a conditional service (e.g.
606
630
  * one gated behind an option) would indict this plugin for orderings it
607
631
  * cannot actually satisfy.
632
+ *
633
+ * Declaring is NOT voluntary (#4471). Everything above can only enforce what
634
+ * a plugin declares — a plugin that resolves `getService('X')` during init()
635
+ * and declares nothing was invisible to all of it, failing only under
636
+ * unlucky composition orders (#4085, and #4420 at data-consistency cost).
637
+ * `scripts/check-init-service-contract.mjs` (CI: `check:init-service-contract`)
638
+ * closes that gap: it walks every plugin's init() call graph and errors on
639
+ * any init-reachable getService of a workspace-provided service that no
640
+ * declaration covers. Best-effort tolerance is declared IN the plugin via
641
+ * `optionalDependencies`, never exempted in the checker.
608
642
  */
609
643
  /**
610
644
  * The ordering-relevant surface of a kernel plugin. Structural on purpose:
@@ -2431,6 +2465,203 @@ declare function withTransientRetry<T>(fn: (attempt: number) => Promise<T>, opts
2431
2465
  */
2432
2466
  declare function bulkWrite<TRow, TRecord = any>(rows: TRow[], opts: BulkWriteOptions<TRow, TRecord>): Promise<BulkWriteRowResult<TRecord>[]>;
2433
2467
 
2468
+ /**
2469
+ * Can this runtime actually roll back? — the ADR-0119 D4 gate, shared.
2470
+ *
2471
+ * Exported from `@objectstack/core` and consumed by
2472
+ * `@objectstack/metadata-protocol`'s `batchData` (which depends on core, so
2473
+ * the direction is legal) so the two cannot drift. They were the same two-line
2474
+ * condition written twice, which is precisely the shape that drifts by one
2475
+ * clause and leaves one caller believing it has atomicity it does not have.
2476
+ *
2477
+ * TWO levels, both necessary. `engine.transaction()` exists but runs the
2478
+ * callback with NO transaction and NO rollback when the default driver lacks
2479
+ * `beginTransaction` — a declared caveat of the contract member (ADR-0119 D1),
2480
+ * and one that turns "atomic" back into a lie precisely where it matters. So
2481
+ * where the driver registry is inspectable the driver is checked too; where it
2482
+ * is not (test doubles), the engine-level probe is all there is.
2483
+ *
2484
+ * A type predicate, not a bare boolean: every caller's next move is to CALL
2485
+ * `transaction`, and on the host surfaces that declare it optionally
2486
+ * (`MetadataHostEngine`) a boolean would leave each one re-narrowing by hand —
2487
+ * which is the same restatement this helper exists to remove.
2488
+ */
2489
+ declare function engineCanRollBack<T>(engine: T): engine is T & EngineWithTransaction;
2490
+ /** What {@link engineCanRollBack} proves is present. Mirrors `IObjectQLEngine['transaction']`. */
2491
+ interface EngineWithTransaction {
2492
+ transaction<R>(callback: (trxCtx: any) => Promise<R>, baseContext?: any): Promise<R>;
2493
+ }
2494
+ /** What a forward/compensate callback is told about the chunk it is running. */
2495
+ interface MigrationChunkContext {
2496
+ readonly runId: string;
2497
+ /** Run-global chunk index — the LIFO ordering key, stable across a resume. */
2498
+ readonly chunkIndex: number;
2499
+ /**
2500
+ * 1 on the first try. `> 1` means a previous attempt's outcome is UNKNOWN:
2501
+ * recheck by natural key before re-writing (see this file's header).
2502
+ */
2503
+ readonly attempt: number;
2504
+ /**
2505
+ * The transaction-bound execution context. Thread it to every engine call
2506
+ * this callback makes — `engine.insert(obj, row, { context })` — so the
2507
+ * write joins the chunk's transaction instead of committing beside it.
2508
+ */
2509
+ readonly context: unknown;
2510
+ }
2511
+ /** One step of a plan. Steps run in declaration order; each is chunked. */
2512
+ interface MigrationPlanStep<TRow = unknown> {
2513
+ readonly name: string;
2514
+ /**
2515
+ * Read-only preflight. Throw to refuse the run. Runs for EVERY step before
2516
+ * any step writes — a plan that would fail at step 3 must not have written
2517
+ * step 1 (ADR-0117 D8's fail-closed enable gate, generalized).
2518
+ */
2519
+ preflight?(engine: IObjectQLEngine): Promise<void>;
2520
+ /** The rows this step processes. Called once, before chunking. */
2521
+ load(engine: IObjectQLEngine): Promise<TRow[]>;
2522
+ /** Forward work for one chunk. Runs INSIDE the chunk's transaction. */
2523
+ forward(rows: TRow[], ctx: MigrationChunkContext, engine: IObjectQLEngine): Promise<void>;
2524
+ /**
2525
+ * Undo one previously-committed chunk. Runs in its OWN transaction.
2526
+ * A step without one makes the plan non-compensable — which the runner
2527
+ * refuses up front rather than discovering at the worst possible moment
2528
+ * (see {@link runMigrationJournal}'s preflight).
2529
+ */
2530
+ compensate?(rows: TRow[], ctx: MigrationChunkContext, engine: IObjectQLEngine): Promise<void>;
2531
+ }
2532
+ interface MigrationPlan {
2533
+ /** Stable plan id. Part of the plan hash; identifies the plan across runs. */
2534
+ readonly id: string;
2535
+ /** Optional join to `sys_migration.id` when this plan implements a named migration. */
2536
+ readonly migrationId?: string;
2537
+ readonly steps: ReadonlyArray<MigrationPlanStep<any>>;
2538
+ readonly chunkSize?: number;
2539
+ /**
2540
+ * What a REDISCOVERED (crashed) run should do. Note this governs restart
2541
+ * only — an in-run failure always compensates, because the runner is still
2542
+ * alive to do it and a half-applied plan is nobody's intent.
2543
+ */
2544
+ readonly onCrash?: MigrationOnCrashPolicy;
2545
+ }
2546
+ /** One chunk in the run-global chunk plan. */
2547
+ interface MigrationChunk {
2548
+ /** Run-global index, 0-based, stable for a given plan hash. */
2549
+ readonly index: number;
2550
+ readonly stepIndex: number;
2551
+ readonly stepName: string;
2552
+ readonly offset: number;
2553
+ readonly length: number;
2554
+ }
2555
+ interface MigrationRunResult {
2556
+ readonly runId: string;
2557
+ /**
2558
+ * `completed` — every chunk committed.
2559
+ * `compensated` — a chunk failed and every committed chunk was undone.
2560
+ * `failed` — a chunk failed AND compensation could not finish. The database
2561
+ * is in a partial state that needs a human; the journal says exactly where.
2562
+ */
2563
+ readonly status: 'completed' | 'compensated' | 'failed';
2564
+ readonly chunksTotal: number;
2565
+ readonly chunksCommitted: number;
2566
+ readonly chunksCompensated: number;
2567
+ readonly planHash: string;
2568
+ /** The failure that ended a non-`completed` run. */
2569
+ readonly error?: unknown;
2570
+ }
2571
+ /**
2572
+ * Where a resume finds the plan it has to re-run (#4617).
2573
+ *
2574
+ * A journal cannot hold a plan. `forward` and `compensate` are FUNCTIONS, and
2575
+ * the rows a chunk covers are produced by `load()` against the live database —
2576
+ * none of it survives a process boundary, which is why the journal records the
2577
+ * plan HASH rather than the plan. So recovery needs the plan handed back to it
2578
+ * by whoever owns the code, and that is what this registry is: the seam between
2579
+ * "the journal knows a run stopped at chunk 7" and "something in this process
2580
+ * knows what chunk 7 was supposed to do".
2581
+ *
2582
+ * Registered as the `migration-plans` kernel service. An interrupted run whose
2583
+ * plan no loaded plugin registers is REPORTED, never silently skipped — the
2584
+ * operator is told which plan id is missing, because "nothing to resume" and
2585
+ * "the code that owns this run is not loaded" are different facts and only one
2586
+ * of them is safe to ignore.
2587
+ */
2588
+ interface MigrationPlanProvider {
2589
+ register(plan: MigrationPlan): void;
2590
+ get(planId: string): MigrationPlan | undefined;
2591
+ list(): MigrationPlan[];
2592
+ }
2593
+ /** The default {@link MigrationPlanProvider}. Last registration for an id wins. */
2594
+ declare class MigrationPlanRegistry implements MigrationPlanProvider {
2595
+ private readonly plans;
2596
+ register(plan: MigrationPlan): void;
2597
+ get(planId: string): MigrationPlan | undefined;
2598
+ list(): MigrationPlan[];
2599
+ }
2600
+ /** A run found by {@link findInterruptedRuns} — started, never concluded. */
2601
+ interface InterruptedRun {
2602
+ readonly runId: string;
2603
+ readonly planId: string;
2604
+ readonly planHash: string;
2605
+ readonly migrationId?: string;
2606
+ readonly startedAt?: string;
2607
+ /** Chunks whose `chunk_done` is present — known committed. */
2608
+ readonly committedChunks: number[];
2609
+ /** Chunks with `chunk_started` and no `chunk_done` — outcome UNKNOWN. */
2610
+ readonly unknownChunks: number[];
2611
+ readonly compensatedChunks: number[];
2612
+ }
2613
+ /** Raised when the runner refuses to start or to resume. Never a partial run. */
2614
+ declare class MigrationJournalRefusal extends Error {
2615
+ readonly code: string;
2616
+ constructor(code: string, message: string);
2617
+ }
2618
+ /** Flatten steps × rows into the run-global chunk list. */
2619
+ declare function planChunks(plan: MigrationPlan, rowCounts: readonly number[], chunkSize?: number): MigrationChunk[];
2620
+ /**
2621
+ * Hash the plan SHAPE — id, step names, and the chunk boundaries.
2622
+ *
2623
+ * Resuming a changed plan against an old journal would apply chunk boundaries
2624
+ * the journal never described: "chunk 7 done" would name a different range of
2625
+ * different rows, and the resume would skip work it never did. So the hash
2626
+ * covers exactly what a chunk index means, and a mismatch REFUSES.
2627
+ */
2628
+ declare function hashMigrationPlan(plan: MigrationPlan, chunks: readonly MigrationChunk[]): string;
2629
+ /**
2630
+ * Every event for a run, ordered by `seq`.
2631
+ *
2632
+ * Sorted in memory, deliberately. `seq` is the ordering authority (wall-clock
2633
+ * stamps tie at coarse resolution and skew), and a run's journal is bounded by
2634
+ * its chunk count, so this costs nothing and removes recovery's dependence on
2635
+ * driver-side sort behaviour — which is not something a recovery path should
2636
+ * be discovering the edges of.
2637
+ */
2638
+ declare function readRunJournal(engine: IObjectQLEngine, runId: string): Promise<MigrationJournalEvent[]>;
2639
+ /**
2640
+ * Runs that started and never concluded — the boot scanner's input.
2641
+ *
2642
+ * "Concluded" means `run_done` (finished forward) or `run_failed` with every
2643
+ * committed chunk compensated (finished backward). Anything else is a run that
2644
+ * stopped mid-flight and still owes the operator an answer.
2645
+ */
2646
+ declare function findInterruptedRuns(engine: IObjectQLEngine): Promise<InterruptedRun[]>;
2647
+ interface RunMigrationJournalOptions {
2648
+ /** Supply to resume an existing run; omit to start a new one. */
2649
+ readonly runId?: string;
2650
+ readonly chunkSize?: number;
2651
+ /** Injectable for deterministic tests. */
2652
+ readonly now?: () => string;
2653
+ }
2654
+ /**
2655
+ * Run `plan` under the journal, or resume a run left behind by a crash.
2656
+ *
2657
+ * Refuses (never partially runs) when: the runtime cannot roll back; any
2658
+ * step's preflight fails; the plan declares `onCrash: 'compensate'` but some
2659
+ * step cannot compensate; or a resume's plan hash disagrees with the journal.
2660
+ */
2661
+ declare function runMigrationJournal(engine: IObjectQLEngine, plan: MigrationPlan, options?: RunMigrationJournalOptions): Promise<MigrationRunResult>;
2662
+ /** Resume a run the journal says was interrupted. Thin alias for intent at call sites. */
2663
+ declare function resumeMigrationJournal(engine: IObjectQLEngine, plan: MigrationPlan, runId: string, options?: Omit<RunMigrationJournalOptions, 'runId'>): Promise<MigrationRunResult>;
2664
+
2434
2665
  /**
2435
2666
  * The slice of an execution context the resolver reads. Structural on purpose —
2436
2667
  * see {@link filterTokenContextFrom}.
@@ -2991,4 +3222,4 @@ declare class NamespaceResolver {
2991
3222
  private suggestAlternative;
2992
3223
  }
2993
3224
 
2994
- export { ANONYMOUS_DENY_BODY, ANONYMOUS_DENY_CODE, ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, API_KEY_PREFIX, type AnonymousDenyInput, type ApiKeyPrincipal, ApiRegistry, type ApiRegistryPluginConfig, type AuthGate, type BucketGranularity, type BulkWriteOptions, type BulkWriteRowResult, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type ExecutionContextLike, type FilterTokenResolutionContext, type GeneratedApiKey, type GrantValidityWindow, HotReloadManager, type KernelState, type KeyInput, type LadderPrincipal, type LadderRow, LiteKernel, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, type OrderablePlugin, POSTURE_INJECTION_RULE, POSTURE_LADDER, POSTURE_RANK, type ParsedSignature, type PermissionCheckResult$1 as PermissionCheckResult, type PermissionGrant, type Plugin, type PluginArtifactVerifyResult, PluginConfigValidator, type PluginContext, PluginHealthMonitor, type PluginHealthStatus, type PluginLoadResult, PluginLoader, type PluginMetadata, type PermissionCheckResult as PluginPermissionCheckResult, PluginPermissionEnforcer, PluginPermissionManager, type PluginPermissions, PluginSandboxRuntime, PluginSecurityScanner, type PluginSignatureConfig, PluginSignatureVerifier, type PluginStartupResult, type PostureEvidence, type PublisherVerifyResult, index as QA, type ResolveAuthzInput, type ResolveLocalizationInput, type ResolveUserAuthzGrantsOptions, type ResolvedAuthzContext, type ResourceUsage, type RetryOptions, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, UnknownFilterTokenError, UnresolvedFilterTokenError, type UserAuthzGrants, type VersionCompatibility, assertInitServiceRequirements, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, describeInitOrderFault, evaluateAuthGate, extractApiKey, filterTokenContextFrom, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, parseScopes, parseSignature, postureVisibleRows, readAuthoredTranslationLayer, resolveApiKeyPrincipal, resolveAuthzContext, resolveFilterToken, resolveFilterTokens, resolveLocale, resolveLocalizationContext, resolvePluginOrder, resolveUserAuthzGrants, safeExit, shouldDenyAnonymous, signPayload, validateInitServiceContract, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature, wireAuthoredTranslationSync, withTransientRetry, zonedDateStartToUtcMs };
3225
+ export { ANONYMOUS_DENY_BODY, ANONYMOUS_DENY_CODE, ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, API_KEY_PREFIX, type AnonymousDenyInput, type ApiKeyPrincipal, ApiRegistry, type ApiRegistryPluginConfig, type AuthGate, type BucketGranularity, type BulkWriteOptions, type BulkWriteRowResult, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type EngineWithTransaction, type ExecutionContextLike, type FilterTokenResolutionContext, type GeneratedApiKey, type GrantValidityWindow, HotReloadManager, type InterruptedRun, type KernelState, type KeyInput, type LadderPrincipal, type LadderRow, LiteKernel, type MigrationChunk, type MigrationChunkContext, MigrationJournalRefusal, type MigrationPlan, type MigrationPlanProvider, MigrationPlanRegistry, type MigrationPlanStep, type MigrationRunResult, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, type OrderablePlugin, POSTURE_INJECTION_RULE, POSTURE_LADDER, POSTURE_RANK, type ParsedSignature, type PermissionCheckResult$1 as PermissionCheckResult, type PermissionGrant, type Plugin, type PluginArtifactVerifyResult, PluginConfigValidator, type PluginContext, PluginHealthMonitor, type PluginHealthStatus, type PluginLoadResult, PluginLoader, type PluginMetadata, type PermissionCheckResult as PluginPermissionCheckResult, PluginPermissionEnforcer, PluginPermissionManager, type PluginPermissions, PluginSandboxRuntime, PluginSecurityScanner, type PluginSignatureConfig, PluginSignatureVerifier, type PluginStartupResult, type PostureEvidence, type PublisherVerifyResult, index as QA, type ResolveAuthzInput, type ResolveLocalizationInput, type ResolveUserAuthzGrantsOptions, type ResolvedAuthzContext, type ResourceUsage, type RetryOptions, type RunMigrationJournalOptions, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, UnknownFilterTokenError, UnresolvedFilterTokenError, type UserAuthzGrants, type VersionCompatibility, assertInitServiceRequirements, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, describeInitOrderFault, engineCanRollBack, evaluateAuthGate, extractApiKey, filterTokenContextFrom, findInterruptedRuns, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, hashMigrationPlan, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, parseScopes, parseSignature, planChunks, postureVisibleRows, readAuthoredTranslationLayer, readRunJournal, resolveApiKeyPrincipal, resolveAuthzContext, resolveFilterToken, resolveFilterTokens, resolveLocale, resolveLocalizationContext, resolvePluginOrder, resolveUserAuthzGrants, resumeMigrationJournal, runMigrationJournal, safeExit, shouldDenyAnonymous, signPayload, validateInitServiceContract, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature, wireAuthoredTranslationSync, withTransientRetry, zonedDateStartToUtcMs };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { Logger, LifecycleEventName, IServiceRegistry } from '@objectstack/spec/contracts';
1
+ import { Logger, LifecycleEventName, IServiceRegistry, IObjectQLEngine } from '@objectstack/spec/contracts';
2
2
  export { EngineSchemaRegistryView, IDataDriver, IDataEngine, IHttpRequest, IHttpResponse, IHttpServer, IObjectQLEngine, Logger, Middleware, RouteHandler } from '@objectstack/spec/contracts';
3
3
  import { z } from 'zod';
4
- import { LoggerConfig } from '@objectstack/spec/system';
4
+ import { LoggerConfig, MigrationOnCrashPolicy, MigrationJournalEvent } from '@objectstack/spec/system';
5
5
  import { ObjectLogger } from './logger.js';
6
6
  export { createLogger } from './logger.js';
7
7
  import { ConflictResolutionStrategy, ApiRegistryEntryInput, ApiRegistryEntry, ApiDiscoveryQuery, ApiDiscoveryResponse, ApiEndpointRegistration, ApiRegistry as ApiRegistry$1 } from '@objectstack/spec/api';
@@ -297,6 +297,30 @@ declare class ObjectKernel {
297
297
  */
298
298
  getState(): string;
299
299
  private initPluginWithTimeout;
300
+ /**
301
+ * Race a plugin lifecycle hook against its startup-timeout guard, and
302
+ * reclaim the guard the moment the race settles (#4813).
303
+ *
304
+ * The guard used to be armed and then abandoned: when the plugin won the
305
+ * race, its `setTimeout` stayed ref'd in the event loop for the full
306
+ * `startupTimeout`, so every process idled that long after its work was
307
+ * done. One `os migrate` finished in 3s and then sat for 120s
308
+ * (`ObjectQLPlugin.startupTimeout`), held open by 8 orphaned guards — one
309
+ * per init plus one per start.
310
+ *
311
+ * Clearing on settle rather than `unref()`-ing at arm time is deliberate.
312
+ * An unref'd guard also stops pinning the loop, but it stops being a guard
313
+ * as well: if the hook never settles and nothing else keeps the loop alive,
314
+ * Node exits before the timer can fire and the timeout is never reported.
315
+ * The guard has to stay ref'd exactly as long as the race is undecided,
316
+ * which is what `clearTimeout` in a `finally` expresses.
317
+ *
318
+ * `operation` is widened to `T | PromiseLike<T>` because the Plugin
319
+ * contract permits a synchronous hook (`init`/`start` return
320
+ * `void | Promise<void>`); such a hook wins the race immediately and the
321
+ * guard is reclaimed on the same turn.
322
+ */
323
+ private raceStartupTimeout;
300
324
  /**
301
325
  * Whether a service is resolvable on this kernel right now — direct
302
326
  * registration or a loader-registered factory. Backs the init-service
@@ -605,6 +629,16 @@ declare abstract class ObjectKernelBase {
605
629
  * Declare only unconditional registrations: a conditional service (e.g.
606
630
  * one gated behind an option) would indict this plugin for orderings it
607
631
  * cannot actually satisfy.
632
+ *
633
+ * Declaring is NOT voluntary (#4471). Everything above can only enforce what
634
+ * a plugin declares — a plugin that resolves `getService('X')` during init()
635
+ * and declares nothing was invisible to all of it, failing only under
636
+ * unlucky composition orders (#4085, and #4420 at data-consistency cost).
637
+ * `scripts/check-init-service-contract.mjs` (CI: `check:init-service-contract`)
638
+ * closes that gap: it walks every plugin's init() call graph and errors on
639
+ * any init-reachable getService of a workspace-provided service that no
640
+ * declaration covers. Best-effort tolerance is declared IN the plugin via
641
+ * `optionalDependencies`, never exempted in the checker.
608
642
  */
609
643
  /**
610
644
  * The ordering-relevant surface of a kernel plugin. Structural on purpose:
@@ -2431,6 +2465,203 @@ declare function withTransientRetry<T>(fn: (attempt: number) => Promise<T>, opts
2431
2465
  */
2432
2466
  declare function bulkWrite<TRow, TRecord = any>(rows: TRow[], opts: BulkWriteOptions<TRow, TRecord>): Promise<BulkWriteRowResult<TRecord>[]>;
2433
2467
 
2468
+ /**
2469
+ * Can this runtime actually roll back? — the ADR-0119 D4 gate, shared.
2470
+ *
2471
+ * Exported from `@objectstack/core` and consumed by
2472
+ * `@objectstack/metadata-protocol`'s `batchData` (which depends on core, so
2473
+ * the direction is legal) so the two cannot drift. They were the same two-line
2474
+ * condition written twice, which is precisely the shape that drifts by one
2475
+ * clause and leaves one caller believing it has atomicity it does not have.
2476
+ *
2477
+ * TWO levels, both necessary. `engine.transaction()` exists but runs the
2478
+ * callback with NO transaction and NO rollback when the default driver lacks
2479
+ * `beginTransaction` — a declared caveat of the contract member (ADR-0119 D1),
2480
+ * and one that turns "atomic" back into a lie precisely where it matters. So
2481
+ * where the driver registry is inspectable the driver is checked too; where it
2482
+ * is not (test doubles), the engine-level probe is all there is.
2483
+ *
2484
+ * A type predicate, not a bare boolean: every caller's next move is to CALL
2485
+ * `transaction`, and on the host surfaces that declare it optionally
2486
+ * (`MetadataHostEngine`) a boolean would leave each one re-narrowing by hand —
2487
+ * which is the same restatement this helper exists to remove.
2488
+ */
2489
+ declare function engineCanRollBack<T>(engine: T): engine is T & EngineWithTransaction;
2490
+ /** What {@link engineCanRollBack} proves is present. Mirrors `IObjectQLEngine['transaction']`. */
2491
+ interface EngineWithTransaction {
2492
+ transaction<R>(callback: (trxCtx: any) => Promise<R>, baseContext?: any): Promise<R>;
2493
+ }
2494
+ /** What a forward/compensate callback is told about the chunk it is running. */
2495
+ interface MigrationChunkContext {
2496
+ readonly runId: string;
2497
+ /** Run-global chunk index — the LIFO ordering key, stable across a resume. */
2498
+ readonly chunkIndex: number;
2499
+ /**
2500
+ * 1 on the first try. `> 1` means a previous attempt's outcome is UNKNOWN:
2501
+ * recheck by natural key before re-writing (see this file's header).
2502
+ */
2503
+ readonly attempt: number;
2504
+ /**
2505
+ * The transaction-bound execution context. Thread it to every engine call
2506
+ * this callback makes — `engine.insert(obj, row, { context })` — so the
2507
+ * write joins the chunk's transaction instead of committing beside it.
2508
+ */
2509
+ readonly context: unknown;
2510
+ }
2511
+ /** One step of a plan. Steps run in declaration order; each is chunked. */
2512
+ interface MigrationPlanStep<TRow = unknown> {
2513
+ readonly name: string;
2514
+ /**
2515
+ * Read-only preflight. Throw to refuse the run. Runs for EVERY step before
2516
+ * any step writes — a plan that would fail at step 3 must not have written
2517
+ * step 1 (ADR-0117 D8's fail-closed enable gate, generalized).
2518
+ */
2519
+ preflight?(engine: IObjectQLEngine): Promise<void>;
2520
+ /** The rows this step processes. Called once, before chunking. */
2521
+ load(engine: IObjectQLEngine): Promise<TRow[]>;
2522
+ /** Forward work for one chunk. Runs INSIDE the chunk's transaction. */
2523
+ forward(rows: TRow[], ctx: MigrationChunkContext, engine: IObjectQLEngine): Promise<void>;
2524
+ /**
2525
+ * Undo one previously-committed chunk. Runs in its OWN transaction.
2526
+ * A step without one makes the plan non-compensable — which the runner
2527
+ * refuses up front rather than discovering at the worst possible moment
2528
+ * (see {@link runMigrationJournal}'s preflight).
2529
+ */
2530
+ compensate?(rows: TRow[], ctx: MigrationChunkContext, engine: IObjectQLEngine): Promise<void>;
2531
+ }
2532
+ interface MigrationPlan {
2533
+ /** Stable plan id. Part of the plan hash; identifies the plan across runs. */
2534
+ readonly id: string;
2535
+ /** Optional join to `sys_migration.id` when this plan implements a named migration. */
2536
+ readonly migrationId?: string;
2537
+ readonly steps: ReadonlyArray<MigrationPlanStep<any>>;
2538
+ readonly chunkSize?: number;
2539
+ /**
2540
+ * What a REDISCOVERED (crashed) run should do. Note this governs restart
2541
+ * only — an in-run failure always compensates, because the runner is still
2542
+ * alive to do it and a half-applied plan is nobody's intent.
2543
+ */
2544
+ readonly onCrash?: MigrationOnCrashPolicy;
2545
+ }
2546
+ /** One chunk in the run-global chunk plan. */
2547
+ interface MigrationChunk {
2548
+ /** Run-global index, 0-based, stable for a given plan hash. */
2549
+ readonly index: number;
2550
+ readonly stepIndex: number;
2551
+ readonly stepName: string;
2552
+ readonly offset: number;
2553
+ readonly length: number;
2554
+ }
2555
+ interface MigrationRunResult {
2556
+ readonly runId: string;
2557
+ /**
2558
+ * `completed` — every chunk committed.
2559
+ * `compensated` — a chunk failed and every committed chunk was undone.
2560
+ * `failed` — a chunk failed AND compensation could not finish. The database
2561
+ * is in a partial state that needs a human; the journal says exactly where.
2562
+ */
2563
+ readonly status: 'completed' | 'compensated' | 'failed';
2564
+ readonly chunksTotal: number;
2565
+ readonly chunksCommitted: number;
2566
+ readonly chunksCompensated: number;
2567
+ readonly planHash: string;
2568
+ /** The failure that ended a non-`completed` run. */
2569
+ readonly error?: unknown;
2570
+ }
2571
+ /**
2572
+ * Where a resume finds the plan it has to re-run (#4617).
2573
+ *
2574
+ * A journal cannot hold a plan. `forward` and `compensate` are FUNCTIONS, and
2575
+ * the rows a chunk covers are produced by `load()` against the live database —
2576
+ * none of it survives a process boundary, which is why the journal records the
2577
+ * plan HASH rather than the plan. So recovery needs the plan handed back to it
2578
+ * by whoever owns the code, and that is what this registry is: the seam between
2579
+ * "the journal knows a run stopped at chunk 7" and "something in this process
2580
+ * knows what chunk 7 was supposed to do".
2581
+ *
2582
+ * Registered as the `migration-plans` kernel service. An interrupted run whose
2583
+ * plan no loaded plugin registers is REPORTED, never silently skipped — the
2584
+ * operator is told which plan id is missing, because "nothing to resume" and
2585
+ * "the code that owns this run is not loaded" are different facts and only one
2586
+ * of them is safe to ignore.
2587
+ */
2588
+ interface MigrationPlanProvider {
2589
+ register(plan: MigrationPlan): void;
2590
+ get(planId: string): MigrationPlan | undefined;
2591
+ list(): MigrationPlan[];
2592
+ }
2593
+ /** The default {@link MigrationPlanProvider}. Last registration for an id wins. */
2594
+ declare class MigrationPlanRegistry implements MigrationPlanProvider {
2595
+ private readonly plans;
2596
+ register(plan: MigrationPlan): void;
2597
+ get(planId: string): MigrationPlan | undefined;
2598
+ list(): MigrationPlan[];
2599
+ }
2600
+ /** A run found by {@link findInterruptedRuns} — started, never concluded. */
2601
+ interface InterruptedRun {
2602
+ readonly runId: string;
2603
+ readonly planId: string;
2604
+ readonly planHash: string;
2605
+ readonly migrationId?: string;
2606
+ readonly startedAt?: string;
2607
+ /** Chunks whose `chunk_done` is present — known committed. */
2608
+ readonly committedChunks: number[];
2609
+ /** Chunks with `chunk_started` and no `chunk_done` — outcome UNKNOWN. */
2610
+ readonly unknownChunks: number[];
2611
+ readonly compensatedChunks: number[];
2612
+ }
2613
+ /** Raised when the runner refuses to start or to resume. Never a partial run. */
2614
+ declare class MigrationJournalRefusal extends Error {
2615
+ readonly code: string;
2616
+ constructor(code: string, message: string);
2617
+ }
2618
+ /** Flatten steps × rows into the run-global chunk list. */
2619
+ declare function planChunks(plan: MigrationPlan, rowCounts: readonly number[], chunkSize?: number): MigrationChunk[];
2620
+ /**
2621
+ * Hash the plan SHAPE — id, step names, and the chunk boundaries.
2622
+ *
2623
+ * Resuming a changed plan against an old journal would apply chunk boundaries
2624
+ * the journal never described: "chunk 7 done" would name a different range of
2625
+ * different rows, and the resume would skip work it never did. So the hash
2626
+ * covers exactly what a chunk index means, and a mismatch REFUSES.
2627
+ */
2628
+ declare function hashMigrationPlan(plan: MigrationPlan, chunks: readonly MigrationChunk[]): string;
2629
+ /**
2630
+ * Every event for a run, ordered by `seq`.
2631
+ *
2632
+ * Sorted in memory, deliberately. `seq` is the ordering authority (wall-clock
2633
+ * stamps tie at coarse resolution and skew), and a run's journal is bounded by
2634
+ * its chunk count, so this costs nothing and removes recovery's dependence on
2635
+ * driver-side sort behaviour — which is not something a recovery path should
2636
+ * be discovering the edges of.
2637
+ */
2638
+ declare function readRunJournal(engine: IObjectQLEngine, runId: string): Promise<MigrationJournalEvent[]>;
2639
+ /**
2640
+ * Runs that started and never concluded — the boot scanner's input.
2641
+ *
2642
+ * "Concluded" means `run_done` (finished forward) or `run_failed` with every
2643
+ * committed chunk compensated (finished backward). Anything else is a run that
2644
+ * stopped mid-flight and still owes the operator an answer.
2645
+ */
2646
+ declare function findInterruptedRuns(engine: IObjectQLEngine): Promise<InterruptedRun[]>;
2647
+ interface RunMigrationJournalOptions {
2648
+ /** Supply to resume an existing run; omit to start a new one. */
2649
+ readonly runId?: string;
2650
+ readonly chunkSize?: number;
2651
+ /** Injectable for deterministic tests. */
2652
+ readonly now?: () => string;
2653
+ }
2654
+ /**
2655
+ * Run `plan` under the journal, or resume a run left behind by a crash.
2656
+ *
2657
+ * Refuses (never partially runs) when: the runtime cannot roll back; any
2658
+ * step's preflight fails; the plan declares `onCrash: 'compensate'` but some
2659
+ * step cannot compensate; or a resume's plan hash disagrees with the journal.
2660
+ */
2661
+ declare function runMigrationJournal(engine: IObjectQLEngine, plan: MigrationPlan, options?: RunMigrationJournalOptions): Promise<MigrationRunResult>;
2662
+ /** Resume a run the journal says was interrupted. Thin alias for intent at call sites. */
2663
+ declare function resumeMigrationJournal(engine: IObjectQLEngine, plan: MigrationPlan, runId: string, options?: Omit<RunMigrationJournalOptions, 'runId'>): Promise<MigrationRunResult>;
2664
+
2434
2665
  /**
2435
2666
  * The slice of an execution context the resolver reads. Structural on purpose —
2436
2667
  * see {@link filterTokenContextFrom}.
@@ -2991,4 +3222,4 @@ declare class NamespaceResolver {
2991
3222
  private suggestAlternative;
2992
3223
  }
2993
3224
 
2994
- export { ANONYMOUS_DENY_BODY, ANONYMOUS_DENY_CODE, ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, API_KEY_PREFIX, type AnonymousDenyInput, type ApiKeyPrincipal, ApiRegistry, type ApiRegistryPluginConfig, type AuthGate, type BucketGranularity, type BulkWriteOptions, type BulkWriteRowResult, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type ExecutionContextLike, type FilterTokenResolutionContext, type GeneratedApiKey, type GrantValidityWindow, HotReloadManager, type KernelState, type KeyInput, type LadderPrincipal, type LadderRow, LiteKernel, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, type OrderablePlugin, POSTURE_INJECTION_RULE, POSTURE_LADDER, POSTURE_RANK, type ParsedSignature, type PermissionCheckResult$1 as PermissionCheckResult, type PermissionGrant, type Plugin, type PluginArtifactVerifyResult, PluginConfigValidator, type PluginContext, PluginHealthMonitor, type PluginHealthStatus, type PluginLoadResult, PluginLoader, type PluginMetadata, type PermissionCheckResult as PluginPermissionCheckResult, PluginPermissionEnforcer, PluginPermissionManager, type PluginPermissions, PluginSandboxRuntime, PluginSecurityScanner, type PluginSignatureConfig, PluginSignatureVerifier, type PluginStartupResult, type PostureEvidence, type PublisherVerifyResult, index as QA, type ResolveAuthzInput, type ResolveLocalizationInput, type ResolveUserAuthzGrantsOptions, type ResolvedAuthzContext, type ResourceUsage, type RetryOptions, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, UnknownFilterTokenError, UnresolvedFilterTokenError, type UserAuthzGrants, type VersionCompatibility, assertInitServiceRequirements, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, describeInitOrderFault, evaluateAuthGate, extractApiKey, filterTokenContextFrom, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, parseScopes, parseSignature, postureVisibleRows, readAuthoredTranslationLayer, resolveApiKeyPrincipal, resolveAuthzContext, resolveFilterToken, resolveFilterTokens, resolveLocale, resolveLocalizationContext, resolvePluginOrder, resolveUserAuthzGrants, safeExit, shouldDenyAnonymous, signPayload, validateInitServiceContract, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature, wireAuthoredTranslationSync, withTransientRetry, zonedDateStartToUtcMs };
3225
+ export { ANONYMOUS_DENY_BODY, ANONYMOUS_DENY_CODE, ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, API_KEY_PREFIX, type AnonymousDenyInput, type ApiKeyPrincipal, ApiRegistry, type ApiRegistryPluginConfig, type AuthGate, type BucketGranularity, type BulkWriteOptions, type BulkWriteRowResult, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type EngineWithTransaction, type ExecutionContextLike, type FilterTokenResolutionContext, type GeneratedApiKey, type GrantValidityWindow, HotReloadManager, type InterruptedRun, type KernelState, type KeyInput, type LadderPrincipal, type LadderRow, LiteKernel, type MigrationChunk, type MigrationChunkContext, MigrationJournalRefusal, type MigrationPlan, type MigrationPlanProvider, MigrationPlanRegistry, type MigrationPlanStep, type MigrationRunResult, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, type OrderablePlugin, POSTURE_INJECTION_RULE, POSTURE_LADDER, POSTURE_RANK, type ParsedSignature, type PermissionCheckResult$1 as PermissionCheckResult, type PermissionGrant, type Plugin, type PluginArtifactVerifyResult, PluginConfigValidator, type PluginContext, PluginHealthMonitor, type PluginHealthStatus, type PluginLoadResult, PluginLoader, type PluginMetadata, type PermissionCheckResult as PluginPermissionCheckResult, PluginPermissionEnforcer, PluginPermissionManager, type PluginPermissions, PluginSandboxRuntime, PluginSecurityScanner, type PluginSignatureConfig, PluginSignatureVerifier, type PluginStartupResult, type PostureEvidence, type PublisherVerifyResult, index as QA, type ResolveAuthzInput, type ResolveLocalizationInput, type ResolveUserAuthzGrantsOptions, type ResolvedAuthzContext, type ResourceUsage, type RetryOptions, type RunMigrationJournalOptions, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, UnknownFilterTokenError, UnresolvedFilterTokenError, type UserAuthzGrants, type VersionCompatibility, assertInitServiceRequirements, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, describeInitOrderFault, engineCanRollBack, evaluateAuthGate, extractApiKey, filterTokenContextFrom, findInterruptedRuns, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, hashMigrationPlan, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, parseScopes, parseSignature, planChunks, postureVisibleRows, readAuthoredTranslationLayer, readRunJournal, resolveApiKeyPrincipal, resolveAuthzContext, resolveFilterToken, resolveFilterTokens, resolveLocale, resolveLocalizationContext, resolvePluginOrder, resolveUserAuthzGrants, resumeMigrationJournal, runMigrationJournal, safeExit, shouldDenyAnonymous, signPayload, validateInitServiceContract, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature, wireAuthoredTranslationSync, withTransientRetry, zonedDateStartToUtcMs };