@objectstack/core 17.0.0-rc.0 → 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, IServiceRegistry } from '@objectstack/spec/contracts';
2
- export { IDataDriver, IDataEngine, IHttpRequest, IHttpResponse, IHttpServer, Logger, Middleware, RouteHandler } from '@objectstack/spec/contracts';
1
+ import { Logger, LifecycleEventName, IServiceRegistry, IObjectQLEngine } from '@objectstack/spec/contracts';
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';
@@ -9,6 +9,7 @@ import * as QA from '@objectstack/spec/qa';
9
9
  import { KeyObject } from 'node:crypto';
10
10
  import { PluginCapability, PluginPermissions as PluginPermissions$1, PluginPermissionSet, ResourceType, PermissionAction, PluginPermission, SandboxConfig, KernelSecurityScanResult, KernelSecurityVulnerability, PluginHealthCheck, PluginHealthStatus as PluginHealthStatus$1, PluginHealthReport, HotReloadConfig, VersionConstraint, DependencyConflict, SemanticVersion, CompatibilityLevel } from '@objectstack/spec/kernel';
11
11
  import { AuthzPosture } from '@objectstack/spec/security';
12
+ export { nextUtcCalendarDay, utcInstantMs } from '@objectstack/spec/data';
12
13
 
13
14
  /**
14
15
  * Service Lifecycle Types
@@ -213,6 +214,12 @@ declare class ObjectKernel {
213
214
  private startedPlugins;
214
215
  private pluginStartTimes;
215
216
  private shutdownHandlers;
217
+ /**
218
+ * Name of the plugin whose init() is currently executing (Phase 1 is
219
+ * sequential, so at most one). Lets a getService miss during init name
220
+ * the structural fault (#4131) instead of only the symptom.
221
+ */
222
+ private currentlyInitializing?;
216
223
  constructor(config?: ObjectKernelConfig);
217
224
  /**
218
225
  * Register a plugin with enhanced validation
@@ -290,9 +297,51 @@ declare class ObjectKernel {
290
297
  */
291
298
  getState(): string;
292
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;
324
+ /**
325
+ * Whether a service is resolvable on this kernel right now — direct
326
+ * registration or a loader-registered factory. Backs the init-service
327
+ * contract checks (#4131).
328
+ */
329
+ private hasAnyService;
330
+ /**
331
+ * When a getService miss happens while a plugin's init() is running,
332
+ * append the structural diagnosis (#4131): which plugin was initializing,
333
+ * and — when a composed plugin declares the service — who provides it.
334
+ * Empty string outside Phase 1, so non-boot messages stay unchanged.
335
+ */
336
+ private describeInitOrderFault;
293
337
  private startPluginWithTimeout;
294
338
  private rollbackStartedPlugins;
295
339
  private performShutdown;
340
+ /**
341
+ * Topological order over `dependencies` (hard) + `optionalDependencies`
342
+ * (order-if-present) — ADR-0116, #4131. One implementation shared with
343
+ * LiteKernel via `plugin-order.ts`.
344
+ */
296
345
  private resolveDependencies;
297
346
  private registerShutdownSignals;
298
347
  /**
@@ -350,17 +399,22 @@ interface PluginContext {
350
399
  */
351
400
  getServices(): Map<string, any>;
352
401
  /**
353
- * Register a hook handler
402
+ * Register a hook handler.
403
+ *
404
+ * Known lifecycle-bus names (see `IPluginLifecycleEvents` in
405
+ * `@objectstack/spec`) autocomplete; the bus stays open to custom
406
+ * cross-plugin event names, so any string remains valid.
407
+ *
354
408
  * @param name - Hook name (e.g., 'kernel:ready', 'data:beforeInsert')
355
409
  * @param handler - Hook handler function
356
410
  */
357
- hook(name: string, handler: (...args: any[]) => void | Promise<void>): void;
411
+ hook(name: LifecycleEventName | (string & {}), handler: (...args: any[]) => void | Promise<void>): void;
358
412
  /**
359
413
  * Trigger a hook
360
- * @param name - Hook name
414
+ * @param name - Hook name (known lifecycle names autocomplete; custom names stay legal)
361
415
  * @param args - Arguments to pass to hook handlers
362
416
  */
363
- trigger(name: string, ...args: any[]): Promise<void>;
417
+ trigger(name: LifecycleEventName | (string & {}), ...args: any[]): Promise<void>;
364
418
  /**
365
419
  * Logger instance
366
420
  */
@@ -393,8 +447,35 @@ interface Plugin {
393
447
  /**
394
448
  * List of other plugin names that this plugin depends on.
395
449
  * The kernel ensures these plugins are initialized before this one.
450
+ * A name that is not registered on the kernel is a boot error.
396
451
  */
397
452
  dependencies?: string[];
453
+ /**
454
+ * Soft dependencies — order-if-present (ADR-0116, #4131).
455
+ * Registered names are hoisted ahead exactly like `dependencies`;
456
+ * absent names are silently skipped instead of failing the boot.
457
+ * For plugins that DEGRADE gracefully without the dependency but must
458
+ * never initialize before it when both are composed (e.g. AppPlugin on
459
+ * an engine-less metadata-only kernel).
460
+ */
461
+ optionalDependencies?: string[];
462
+ /**
463
+ * Services this plugin resolves SYNCHRONOUSLY during `init()`
464
+ * (ADR-0116, #4131). The kernel validates the resolved order before
465
+ * Phase 1 (a required service whose only declared provider initializes
466
+ * later is a named boot error) and re-checks immediately before this
467
+ * plugin's init runs. Declare only hard init-time needs — a service the
468
+ * init merely probes behind a try/catch does not belong here.
469
+ */
470
+ requiresServices?: string[];
471
+ /**
472
+ * Services this plugin's `init()` UNCONDITIONALLY registers
473
+ * (ADR-0116, #4131). Powers the pre-Phase-1 ordering validation and
474
+ * lets misordering errors name the provider. Never declare a service
475
+ * that is registered conditionally (option-gated, environment-gated):
476
+ * the kernel would blame orderings this plugin cannot satisfy.
477
+ */
478
+ providesServices?: string[];
398
479
  /**
399
480
  * Init Phase: Register services
400
481
  * Called when kernel is initializing.
@@ -437,6 +518,12 @@ declare abstract class ObjectKernelBase {
437
518
  protected state: KernelState;
438
519
  protected logger: Logger;
439
520
  protected context: PluginContext;
521
+ /**
522
+ * Name of the plugin whose init() is currently executing (Phase 1 runs
523
+ * sequentially, so there is at most one). Lets a getService miss during
524
+ * init name the structural fault (#4131) instead of only the symptom.
525
+ */
526
+ protected currentlyInitializing?: string;
440
527
  constructor(logger: Logger);
441
528
  /**
442
529
  * Validate kernel state
@@ -454,10 +541,30 @@ declare abstract class ObjectKernelBase {
454
541
  */
455
542
  protected createContext(): PluginContext;
456
543
  /**
457
- * Resolve plugin dependencies using topological sort
544
+ * Resolve plugin dependencies using topological sort — `dependencies`
545
+ * hard, `optionalDependencies` order-if-present (ADR-0116, #4131). One
546
+ * implementation shared with ObjectKernel via `plugin-order.ts`.
458
547
  * @returns Ordered list of plugins (dependencies first)
459
548
  */
460
549
  protected resolveDependencies(): Plugin[];
550
+ /**
551
+ * Whether a service is registered on this kernel right now. Backs the
552
+ * init-service contract checks (#4131).
553
+ */
554
+ protected hasRegisteredService(name: string): boolean;
555
+ /**
556
+ * Pre-Phase-1 ordering validation (ADR-0116, #4131): a plugin whose
557
+ * `requiresServices` names a service provided only by a LATER plugin is
558
+ * a named boot error before any init side effects.
559
+ */
560
+ protected validateInitServices(ordered: Plugin[]): void;
561
+ /**
562
+ * When a getService miss happens while a plugin's init() is running,
563
+ * append the structural diagnosis (#4131): which plugin was initializing,
564
+ * and — when a composed plugin declares the service — who provides it.
565
+ * Empty string outside Phase 1, so non-boot messages stay unchanged.
566
+ */
567
+ protected describeInitOrderFault(serviceName: string): string;
461
568
  /**
462
569
  * Run plugin init phase
463
570
  * @param plugin - Plugin to initialize
@@ -495,6 +602,100 @@ declare abstract class ObjectKernelBase {
495
602
  abstract destroy(): Promise<void>;
496
603
  }
497
604
 
605
+ /**
606
+ * Plugin ordering + init-service contract (ADR-0116, #4131).
607
+ *
608
+ * The kernel resolves BOTH init and start order from the plugin dependency
609
+ * graph, so `kernel.use()` registration order proves nothing. Twice a plugin
610
+ * relied on list position anyway and shipped a boot that dies inside init —
611
+ * the first cut of DefaultDatasourcePlugin (started after boot schema-sync;
612
+ * server with no tables) and AppPlugin (#4085: `manifest` grabbed in init
613
+ * before ObjectQLPlugin registered it). Both times the fix existed only as a
614
+ * convention: put the plugin in the right slot, write a comment. This module
615
+ * is the enforced form of that contract, shared by ObjectKernel and
616
+ * LiteKernel so there is exactly one ordering semantic:
617
+ *
618
+ * - `dependencies` — hard: hoisted ahead, missing ⇒ boot error (unchanged).
619
+ * - `optionalDependencies` — order-if-present: hoisted ahead when composed,
620
+ * silently skipped when absent. For plugins that DEGRADE without the
621
+ * dependency but must never init before it (AppPlugin on an engine-less
622
+ * metadata-only kernel).
623
+ * - `requiresServices` — services a plugin resolves SYNCHRONOUSLY during
624
+ * `init()`. Validated before Phase 1 (provable misordering ⇒ named error
625
+ * instead of a crash inside init) and again immediately before each init
626
+ * (authoritative: the service is either registered by now or init dies).
627
+ * - `providesServices` — services a plugin's `init()` UNCONDITIONALLY
628
+ * registers. Powers the pre-Phase-1 check and the named diagnostics.
629
+ * Declare only unconditional registrations: a conditional service (e.g.
630
+ * one gated behind an option) would indict this plugin for orderings it
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.
642
+ */
643
+ /**
644
+ * The ordering-relevant surface of a kernel plugin. Structural on purpose:
645
+ * ObjectKernel sorts `PluginMetadata`, LiteKernel sorts `Plugin`, and both
646
+ * satisfy this shape.
647
+ */
648
+ interface OrderablePlugin {
649
+ name: string;
650
+ /** Hard dependencies — hoisted ahead; missing ⇒ boot error. */
651
+ dependencies?: string[];
652
+ /** Soft dependencies — hoisted ahead when composed, skipped when absent. */
653
+ optionalDependencies?: string[];
654
+ /** Services resolved synchronously during init(). */
655
+ requiresServices?: string[];
656
+ /** Services init() unconditionally registers. */
657
+ providesServices?: string[];
658
+ }
659
+ /**
660
+ * Topologically order plugins: every plugin's `dependencies` (throw when
661
+ * missing) and `optionalDependencies` (skip when missing) init before it.
662
+ * Insertion order is preserved for plugins with no edges between them.
663
+ * Cycles through either edge kind throw — an optional dependency is a real
664
+ * edge whenever both sides are composed.
665
+ */
666
+ declare function resolvePluginOrder<P extends OrderablePlugin>(plugins: Map<string, P>): P[];
667
+ /**
668
+ * Pre-Phase-1 check: walk the resolved order and prove no plugin requires a
669
+ * service whose only declared provider initializes AFTER it. A violation is
670
+ * the exact #4085 class — misplaced composition — reported as a named,
671
+ * structural boot error BEFORE any init side effects, instead of a bare
672
+ * "Service not found" thrown from inside the victim's init.
673
+ *
674
+ * Deliberately does NOT fail when a required service has no declared provider
675
+ * and is not yet registered: an earlier plugin may register it without
676
+ * declaring `providesServices`. That case is settled authoritatively by
677
+ * {@link assertInitServiceRequirements} immediately before the requiring
678
+ * plugin's init runs.
679
+ */
680
+ declare function validateInitServiceContract<P extends OrderablePlugin>(ordered: P[], isServiceRegistered: (name: string) => boolean): void;
681
+ /**
682
+ * Diagnosis suffix for a getService miss that happens WHILE a plugin's
683
+ * init() is running: names the initializing plugin and — when a composed
684
+ * plugin declares the service — the provider and the directive to declare
685
+ * the ordering. Returns '' when no plugin is initializing, so non-boot
686
+ * error messages stay byte-identical. Shared by both kernels.
687
+ */
688
+ declare function describeInitOrderFault(currentlyInitializing: string | undefined, plugins: Iterable<OrderablePlugin>, serviceName: string): string;
689
+ /**
690
+ * Just-before-init check: every service in `requiresServices` must be
691
+ * registered at the moment the plugin's init() is about to run. At this
692
+ * point the verdict is authoritative — Phase 1 runs sequentially, so a
693
+ * service absent now is absent for this init, and the init would die on a
694
+ * bare "Service not found" anyway. This turns that crash into a named
695
+ * composition error.
696
+ */
697
+ declare function assertInitServiceRequirements(plugin: OrderablePlugin, isServiceRegistered: (name: string) => boolean): void;
698
+
498
699
  /**
499
700
  * ObjectKernel - MiniKernel Architecture
500
701
  *
@@ -1997,18 +2198,16 @@ declare function evaluateAuthGate(sessionUser: any, path: string): AuthGate | nu
1997
2198
 
1998
2199
  /** HTTP status every seam returns for an anonymous-denied request. */
1999
2200
  declare const ANONYMOUS_DENY_STATUS: 401;
2000
- /** Stable machine code (mirrors the REST `enforceAuth` seam). */
2001
- declare const ANONYMOUS_DENY_CODE: "unauthenticated";
2201
+ /** Stable machine code (mirrors the REST `enforceAuth` seam). ADR-0112: SCREAMING, a `StandardErrorCode` member. */
2202
+ declare const ANONYMOUS_DENY_CODE: "UNAUTHENTICATED";
2002
2203
  /** Human-facing message. */
2003
2204
  declare const ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
2004
2205
  /** The single 401 body shape every seam returns: `{ error, message }`. */
2005
2206
  declare const ANONYMOUS_DENY_BODY: {
2006
- readonly error: "unauthenticated";
2207
+ readonly error: "UNAUTHENTICATED";
2007
2208
  readonly message: "Authentication is required to access this endpoint.";
2008
2209
  };
2009
2210
  interface AnonymousDenyInput {
2010
- /** The `requireAuth` posture. Falsy ⇒ no-op (demo / single-tenant). */
2011
- requireAuth: boolean | undefined;
2012
2211
  /** Resolved caller id, if any. */
2013
2212
  userId?: string | null;
2014
2213
  /** Internal system context (never set on inbound HTTP; cannot be forged). */
@@ -2115,6 +2314,7 @@ declare function calendarPartsInTzOrUtc(d: Date, tz?: string): CalendarParts;
2115
2314
  * reference-tz calendar, so its bucket boundary is that tz's midnight instant.
2116
2315
  */
2117
2316
  declare function zonedDateStartToUtcMs(ymd: string, tz?: string): number;
2317
+
2118
2318
  /**
2119
2319
  * Granularity of a canonical date-bucket key. Mirrors `@objectstack/spec`'s
2120
2320
  * `DateGranularity` enum but kept as a local literal union so this low-level
@@ -2265,6 +2465,203 @@ declare function withTransientRetry<T>(fn: (attempt: number) => Promise<T>, opts
2265
2465
  */
2266
2466
  declare function bulkWrite<TRow, TRecord = any>(rows: TRow[], opts: BulkWriteOptions<TRow, TRecord>): Promise<BulkWriteRowResult<TRecord>[]>;
2267
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
+
2268
2665
  /**
2269
2666
  * The slice of an execution context the resolver reads. Structural on purpose —
2270
2667
  * see {@link filterTokenContextFrom}.
@@ -2357,9 +2754,22 @@ declare function filterTokenContextFrom(execCtx: ExecutionContextLike | undefine
2357
2754
  * Implements the ICacheService contract with basic get/set/delete/has/clear
2358
2755
  * and TTL expiry. Used by ObjectKernel as an automatic fallback when no
2359
2756
  * real cache plugin (e.g. Redis) is registered.
2757
+ *
2758
+ * [#4058] Self-describes as `degraded`, not `stub` (ADR-0076 D12): this is a
2759
+ * real cache — it stores, expires, and reports true stats — just process-local
2760
+ * and unshared. The non-standard `_fallback: true` it used to carry was read by
2761
+ * nothing (`readServiceSelfInfo` reads only `__serviceInfo` — `_dev`, the other
2762
+ * marker it knew back then, was itself retired in #4319), so discovery reported
2763
+ * it as fully `available`. `handlerReady: false` because
2764
+ * no HTTP surface is mounted for `cache` at all — the same reason realtime
2765
+ * reports false.
2360
2766
  */
2361
2767
  declare function createMemoryCache(): {
2362
- _fallback: boolean;
2768
+ __serviceInfo: {
2769
+ status: "degraded";
2770
+ handlerReady: boolean;
2771
+ message: string;
2772
+ };
2363
2773
  _serviceName: string;
2364
2774
  get<T = unknown>(key: string): Promise<T | undefined>;
2365
2775
  set<T = unknown>(key: string, value: T, ttl?: number): Promise<void>;
@@ -2379,9 +2789,18 @@ declare function createMemoryCache(): {
2379
2789
  * Implements the IQueueService contract with synchronous in-process delivery.
2380
2790
  * Used by ObjectKernel as an automatic fallback when no real queue plugin
2381
2791
  * (e.g. BullMQ / RabbitMQ) is registered.
2792
+ *
2793
+ * [#4058] `degraded`, not `stub` (ADR-0076 D12): messages really reach real
2794
+ * subscribers — synchronously, in-process, with no durability or retry.
2795
+ * `getQueueSize()` answering 0 follows from that rather than faking it: nothing
2796
+ * is ever buffered. `handlerReady: false` — no HTTP surface exists for `queue`.
2382
2797
  */
2383
2798
  declare function createMemoryQueue(): {
2384
- _fallback: boolean;
2799
+ __serviceInfo: {
2800
+ status: "degraded";
2801
+ handlerReady: boolean;
2802
+ message: string;
2803
+ };
2385
2804
  _serviceName: string;
2386
2805
  publish<T = unknown>(queue: string, data: T): Promise<string>;
2387
2806
  subscribe(queue: string, handler: (msg: any) => Promise<void>): Promise<void>;
@@ -2396,9 +2815,19 @@ declare function createMemoryQueue(): {
2396
2815
  * Implements the IJobService contract with basic schedule/cancel/trigger
2397
2816
  * operations. Used by ObjectKernel as an automatic fallback when no real
2398
2817
  * job plugin (e.g. Agenda / BullMQ) is registered.
2818
+ *
2819
+ * [#4058] `degraded` (ADR-0076 D12), with the missing half named in the
2820
+ * message rather than left for a deployer to discover: `trigger()` really runs
2821
+ * the registered handler, but nothing here owns a timer, so a `schedule()`d job
2822
+ * NEVER fires on its own. That is reduced capability, not fabricated output —
2823
+ * no call returns a made-up answer. `handlerReady: false`: no HTTP surface.
2399
2824
  */
2400
2825
  declare function createMemoryJob(): {
2401
- _fallback: boolean;
2826
+ __serviceInfo: {
2827
+ status: "degraded";
2828
+ handlerReady: boolean;
2829
+ message: string;
2830
+ };
2402
2831
  _serviceName: string;
2403
2832
  schedule(name: string, schedule: any, handler: any): Promise<void>;
2404
2833
  cancel(name: string): Promise<void>;
@@ -2439,7 +2868,10 @@ declare function resolveLocale(requestedLocale: string, availableLocales: string
2439
2868
  * Does not load files from disk — operates purely in-memory.
2440
2869
  */
2441
2870
  declare function createMemoryI18n(): {
2442
- _fallback: boolean;
2871
+ __serviceInfo: {
2872
+ status: "degraded";
2873
+ message: string;
2874
+ };
2443
2875
  _serviceName: string;
2444
2876
  t(key: string, locale: string, params?: Record<string, unknown>): string;
2445
2877
  getTranslations(locale: string): Record<string, unknown>;
@@ -2464,7 +2896,10 @@ declare function createMemoryI18n(): {
2464
2896
  * (e.g. MetadataPlugin with file-system persistence) is registered.
2465
2897
  */
2466
2898
  declare function createMemoryMetadata(): {
2467
- _fallback: boolean;
2899
+ __serviceInfo: {
2900
+ status: "degraded";
2901
+ message: string;
2902
+ };
2468
2903
  _serviceName: string;
2469
2904
  register(type: string, name: string, data: any): Promise<void>;
2470
2905
  registerInMemory(type: string, name: string, data: any): void;
@@ -2787,4 +3222,4 @@ declare class NamespaceResolver {
2787
3222
  private suggestAlternative;
2788
3223
  }
2789
3224
 
2790
- 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, 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, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, evaluateAuthGate, extractApiKey, filterTokenContextFrom, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, parseScopes, parseSignature, postureVisibleRows, readAuthoredTranslationLayer, resolveApiKeyPrincipal, resolveAuthzContext, resolveFilterToken, resolveFilterTokens, resolveLocale, resolveLocalizationContext, resolveUserAuthzGrants, safeExit, shouldDenyAnonymous, signPayload, 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 };