@lmzhen/dsh-evolution-state-storage 0.25.9 → 0.26.26

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/lib/index.js CHANGED
@@ -427,6 +427,93 @@ async function runStateProviderConsistency(provider, assert, options = {}) {
427
427
  assert(await provider.loadReviewState(sessionPrefix + "0")).toEqual(reviewStateOf(999));
428
428
  }
429
429
  //#endregion
430
+ //#region lib/types/medium-comparison.js
431
+ /**
432
+ * Compare the pending state as two media read it.
433
+ * @param left - the provider currently in use.
434
+ * @param right - the provider the deployment would switch to.
435
+ * @returns whether the readings agree, and every disagreement, ordered by id.
436
+ */
437
+ async function comparePendingReadings(left, right) {
438
+ const [leftRecords, rightRecords] = await Promise.all([left.listPending(), right.listPending()]);
439
+ const statusOf = (records) => new Map(records.map((record) => [record.id, record.status]));
440
+ const leftStatus = statusOf(leftRecords);
441
+ const rightStatus = statusOf(rightRecords);
442
+ const ids = [...new Set([...leftStatus.keys(), ...rightStatus.keys()])].sort();
443
+ const differences = [];
444
+ for (const id of ids) {
445
+ const leftValue = leftStatus.get(id);
446
+ const rightValue = rightStatus.get(id);
447
+ if (leftValue === rightValue) continue;
448
+ const verdict = leftValue === void 0 ? "right-only" : rightValue === void 0 ? "left-only" : "status-differs";
449
+ differences.push({
450
+ id,
451
+ left: leftValue,
452
+ right: rightValue,
453
+ verdict
454
+ });
455
+ }
456
+ return {
457
+ consistent: differences.length === 0,
458
+ differences
459
+ };
460
+ }
461
+ //#endregion
462
+ //#region lib/types/medium-declaration.js
463
+ /**
464
+ * G2b/2b.3: which medium carries each record type, and what PROCESS SCOPE that medium actually gives.
465
+ *
466
+ * The family has two serialization layers and only ever wrote one of them down, for the json provider: the
467
+ * IO backend cross-process transact lock (first layer) and the caller own in-process chain (second). The
468
+ * domain medium inherits its answer from whatever backend the platform storage facility routes, and nothing
469
+ * stated it. So this file states it - and the criterion is the family one: a CLAIM must be produced by a
470
+ * capability that can be DISPROVED. processScopeViolations takes the io the provider writes through and
471
+ * reports every claim of cross-process that the io cannot back up.
472
+ *
473
+ * MIGRATION NOTE: the plan allows naming storage-sqlite for a cross-process record type. That package does
474
+ * not exist in this tree (0 hits), so today every entry is either backed by the IO transact lock or
475
+ * declares single-process. When a backend carrying its own cross-process guarantee lands, it is named here
476
+ * and the reasoning moves with it - one declaration, not two.
477
+ *
478
+ * @module @lmzhen/dsh-evolution-state-storage/medium-declaration
479
+ */
480
+ /** The declaration. */
481
+ const MEDIUM_PROCESS_DECLARATION = [
482
+ {
483
+ record: "review_state",
484
+ medium: "json",
485
+ scope: "cross-process",
486
+ because: "the json provider serializes through the IO backend transact lock, as its README states"
487
+ },
488
+ {
489
+ record: "curator_state",
490
+ medium: "json",
491
+ scope: "cross-process",
492
+ because: "same provider and same lock as review_state; the run bookkeeping rides the same write path"
493
+ },
494
+ {
495
+ record: "pending",
496
+ medium: "json",
497
+ scope: "cross-process",
498
+ because: "a staged write is claimed by name across processes, so it needs the lock more than the other two"
499
+ },
500
+ {
501
+ record: "evolution-domain",
502
+ medium: "domain",
503
+ scope: "single-process",
504
+ because: "no backend in this tree carries its own cross-process guarantee and none is named, so the domain facility lock semantics stay the backend business and this declares the weaker truth"
505
+ }
506
+ ];
507
+ /**
508
+ * Falsify the declaration against a capability reading.
509
+ * @param io - the io backend the medium writes through, or undefined when none is mounted.
510
+ * @returns one message per claim the capability cannot back up; empty when every claim holds.
511
+ */
512
+ function processScopeViolations(io) {
513
+ const backed = typeof io?.transact === "function";
514
+ return MEDIUM_PROCESS_DECLARATION.filter((claim) => claim.scope === "cross-process" && !backed).map((claim) => claim.record + " claims cross-process (" + claim.because + ") but the mounted io exposes no transact lock");
515
+ }
516
+ //#endregion
430
517
  //#region lib/types/index.js
431
518
  /**
432
519
  * Provider seam for durable evolution state.
@@ -534,6 +621,17 @@ var EvolutionStateStorageRegistry = class extends Service {
534
621
  hasProviders() {
535
622
  return this.providers.size > 0;
536
623
  }
624
+ /**
625
+ * G2b/2b.1: the registered provider names, in registration order.
626
+ *
627
+ * A consumer needs the COUNT to refuse an unpinned choice while more than one medium is mounted:
628
+ * this registry cannot make that call itself, because the pin lives in the consumer's config and the
629
+ * registry never sees it. Read-only, and it exists so the refusal can happen at mount instead of at the
630
+ * first read (where the state is already half-consumed and the message arrives too late to act on).
631
+ */
632
+ providerNames() {
633
+ return [...this.providers.keys()];
634
+ }
537
635
  provider(name) {
538
636
  if (name) {
539
637
  this.ambiguousWarned = false;
@@ -553,4 +651,4 @@ var EvolutionStateStorageRegistry = class extends Service {
553
651
  }
554
652
  };
555
653
  //#endregion
556
- export { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, EvolutionStateStorageRegistry, EvolutionStateStorageRegistry as default, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_KINDS, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PENDING_STATUSES, PENDING_TABLE, PROVIDER_DOMAIN, PROVIDER_JSON, REVIEW_STATE_FILE, REVIEW_STATE_SESSION_CAP, REVIEW_STATE_TABLE, UNKNOWN_FIELD_POLICY, assertCloneable, canClaimPending, canResolvePending, cloneRecord, recordIssue, releasedStatus, runStateProviderConsistency, selectPendingOverflow, selectSessionOverflow };
654
+ export { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, EvolutionStateStorageRegistry, EvolutionStateStorageRegistry as default, MEDIUM_PROCESS_DECLARATION, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_KINDS, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PENDING_STATUSES, PENDING_TABLE, PROVIDER_DOMAIN, PROVIDER_JSON, REVIEW_STATE_FILE, REVIEW_STATE_SESSION_CAP, REVIEW_STATE_TABLE, UNKNOWN_FIELD_POLICY, assertCloneable, canClaimPending, canResolvePending, cloneRecord, comparePendingReadings, processScopeViolations, recordIssue, releasedStatus, runStateProviderConsistency, selectPendingOverflow, selectSessionOverflow };
@@ -11,6 +11,8 @@ import type { PendingKind, PendingStatus } from './record-contract.ts';
11
11
  export * from './constants.ts';
12
12
  export * from './record-contract.ts';
13
13
  export * from './conformance.ts';
14
+ export * from './medium-comparison.ts';
15
+ export * from './medium-declaration.ts';
14
16
  /** 0.3.17 (S3.3): the claim lifecycle as ONE transition table — BOTH
15
17
  * providers (json/domain) must use these, never a hand-written copy (a second
16
18
  * copy is exactly the E-10 drift class). */
@@ -163,6 +165,15 @@ export declare class EvolutionStateStorageRegistry extends Service {
163
165
  * at start with the registry's precise message) while staying lazy when no
164
166
  * provider has registered yet (mount order not settled). */
165
167
  hasProviders(): boolean;
168
+ /**
169
+ * G2b/2b.1: the registered provider names, in registration order.
170
+ *
171
+ * A consumer needs the COUNT to refuse an unpinned choice while more than one medium is mounted:
172
+ * this registry cannot make that call itself, because the pin lives in the consumer's config and the
173
+ * registry never sees it. Read-only, and it exists so the refusal can happen at mount instead of at the
174
+ * first read (where the state is already half-consumed and the message arrives too late to act on).
175
+ */
176
+ providerNames(): readonly string[];
166
177
  provider(name?: string): EvolutionStateStorage;
167
178
  }
168
179
  export default EvolutionStateStorageRegistry;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * G2a/2a.2: the two-medium read comparison.
3
+ *
4
+ * Audit S1-B02: switching back to the json medium revives staged records the other medium never had
5
+ * (the json read face merges a legacy `pending.json`, which re-introduces an evicted record), and the
6
+ * default TTL of 0 makes age no threshold at all. The fix is not a third store - it is being able to SEE
7
+ * the disagreement: the SAME logical state read through both providers must agree.
8
+ *
9
+ * This lives in the seam package because that is the layer the two providers share. Comparing the
10
+ * packages' own file reads would bypass the seam and stop working the moment a provider is swapped.
11
+ *
12
+ * Read-only: it reports, it never writes and never repairs.
13
+ */
14
+ import type { EvolutionStateStorage, PendingStatus } from './index.ts';
15
+ /** One record that the two readings do not agree on. */
16
+ export interface MediumDifference {
17
+ readonly id: string;
18
+ readonly left: PendingStatus | undefined;
19
+ readonly right: PendingStatus | undefined;
20
+ /** `left-only` is the revival shape: the json side holds a record the other medium never had. */
21
+ readonly verdict: 'left-only' | 'right-only' | 'status-differs';
22
+ }
23
+ /** The comparison result. `consistent: false` is the red signal S1-B02 asks for. */
24
+ export interface MediumComparison {
25
+ readonly consistent: boolean;
26
+ /** One entry per record the readings disagree on, ordered by id. */
27
+ readonly differences: readonly MediumDifference[];
28
+ }
29
+ /**
30
+ * Compare the pending state as two media read it.
31
+ * @param left - the provider currently in use.
32
+ * @param right - the provider the deployment would switch to.
33
+ * @returns whether the readings agree, and every disagreement, ordered by id.
34
+ */
35
+ export declare function comparePendingReadings(left: Pick<EvolutionStateStorage, 'listPending'>, right: Pick<EvolutionStateStorage, 'listPending'>): Promise<MediumComparison>;
36
+ //# sourceMappingURL=medium-comparison.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * G2b/2b.3: which medium carries each record type, and what PROCESS SCOPE that medium actually gives.
3
+ *
4
+ * The family has two serialization layers and only ever wrote one of them down, for the json provider: the
5
+ * IO backend cross-process transact lock (first layer) and the caller own in-process chain (second). The
6
+ * domain medium inherits its answer from whatever backend the platform storage facility routes, and nothing
7
+ * stated it. So this file states it - and the criterion is the family one: a CLAIM must be produced by a
8
+ * capability that can be DISPROVED. processScopeViolations takes the io the provider writes through and
9
+ * reports every claim of cross-process that the io cannot back up.
10
+ *
11
+ * MIGRATION NOTE: the plan allows naming storage-sqlite for a cross-process record type. That package does
12
+ * not exist in this tree (0 hits), so today every entry is either backed by the IO transact lock or
13
+ * declares single-process. When a backend carrying its own cross-process guarantee lands, it is named here
14
+ * and the reasoning moves with it - one declaration, not two.
15
+ *
16
+ * @module @lmzhen/dsh-evolution-state-storage/medium-declaration
17
+ */
18
+ /** One record type: its medium, the process scope claimed for it, and why that is true. */
19
+ export interface MediumProcessClaim {
20
+ /** The record family, named as the domain declares it. */
21
+ readonly record: string;
22
+ readonly medium: 'json' | 'domain';
23
+ /** cross-process is a CLAIM: processScopeViolations refuses it without a backing transact. */
24
+ readonly scope: 'cross-process' | 'single-process';
25
+ readonly because: string;
26
+ }
27
+ /** The declaration. */
28
+ export declare const MEDIUM_PROCESS_DECLARATION: readonly MediumProcessClaim[];
29
+ /** The capability the claims are checked against: the io the medium writes through. */
30
+ export interface ProcessScopeProbe {
31
+ /** The IO backend transact entry point, when it has one. */
32
+ readonly transact?: unknown;
33
+ }
34
+ /**
35
+ * Falsify the declaration against a capability reading.
36
+ * @param io - the io backend the medium writes through, or undefined when none is mounted.
37
+ * @returns one message per claim the capability cannot back up; empty when every claim holds.
38
+ */
39
+ export declare function processScopeViolations(io: ProcessScopeProbe | undefined): readonly string[];
40
+ //# sourceMappingURL=medium-declaration.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-state-storage",
3
3
  "description": "Provider registry seam for durable evolution state (community build)",
4
- "version": "0.25.9",
4
+ "version": "0.26.26",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },