@lmzhen/dsh-evolution-state-storage 0.3.67 → 0.3.69

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
@@ -170,10 +170,66 @@ const PENDING_RESOLVED_CAP = 200;
170
170
  * interface change, no background sweeper).
171
171
  */
172
172
  const REVIEW_STATE_SESSION_CAP = 500;
173
+ /**
174
+ * V27 G2.2: WHICH pending records the audit cap evicts, as one pure rule both
175
+ * providers apply (the two implementations had drifted into separate files and
176
+ * only one of them was ever updated by a later fix).
177
+ *
178
+ * Eligible = resolved (`approved`/`rejected`) and NOT a capability approval:
179
+ * v23 (AP-1) exempts those because the Creator-mode contract reads the LIVE
180
+ * approved list and a capability cannot be re-submitted for the same package,
181
+ * so eviction would make an approved capability permanently unactivatable.
182
+ * Ordering = oldest `resolvedAt` first; a missing or unparseable timestamp sorts
183
+ * LAST (json parity, v16), and pending/executing rows are live work that is
184
+ * never trimmed. v28 G2.4 (STATE-03) — the previous wording promised "an
185
+ * unknown time must never make a record the victim"; that is only true while
186
+ * the overflow fits within the KNOWN-timestamp records. When the overflow
187
+ * exceeds them (corruption-level data: most resolved rows with broken
188
+ * timestamps), `slice` necessarily reaches into the unknown-timestamp tail —
189
+ * the cap must be enforced, so there is no alternative. The guarantee is
190
+ * therefore: unknown-timestamp records are evicted LAST, after every known
191
+ * timestamp, in insertion order among themselves (Array#sort stability).
192
+ *
193
+ * @param records - every pending record currently in the live table.
194
+ * @param cap - how many resolved records may be kept.
195
+ * @returns the records to evict, oldest first (empty when within the cap).
196
+ */
197
+ function selectPendingOverflow(records, cap = 200) {
198
+ const resolved = records.filter((record) => (record.status === "approved" || record.status === "rejected") && record.kind !== "capability");
199
+ const overflow = resolved.length - cap;
200
+ if (overflow <= 0) return [];
201
+ const resolvedAtMs = (record) => {
202
+ if (!record.resolvedAt) return Number.MAX_SAFE_INTEGER;
203
+ const parsed = Date.parse(record.resolvedAt);
204
+ return Number.isNaN(parsed) ? Number.MAX_SAFE_INTEGER : parsed;
205
+ };
206
+ return [...resolved].sort((a, b) => resolvedAtMs(a) - resolvedAtMs(b)).slice(0, overflow);
207
+ }
208
+ /**
209
+ * V27 G2.2: which review-state session rows a save evicts, as one pure rule
210
+ * both providers apply. The saving session is never a candidate (it is the most
211
+ * recent write by definition); the rest are ordered by their provider stamp
212
+ * ascending, with a missing stamp read as 0 = oldest, because an active session
213
+ * re-stamps its row on its next save and an unknown stamp is stale by
214
+ * construction. Exactly one row is dropped per over-cap save, which keeps the
215
+ * table at the cap in steady state.
216
+ *
217
+ * @param rows - the other sessions' rows, with their stamps.
218
+ * @param cap - how many session rows may exist.
219
+ * @returns the keys to delete, oldest first.
220
+ */
221
+ function selectSessionOverflow(rows, options, cap = 500) {
222
+ const overflow = rows.length - cap + 1;
223
+ if (overflow <= 0) return [];
224
+ return [...rows].sort((a, b) => options.stampOf(a) - options.stampOf(b)).slice(0, overflow).map((row) => options.keyOf(row));
225
+ }
173
226
  var EvolutionStateStorageRegistry = class extends Service {
174
227
  providers = /* @__PURE__ */ new Map();
175
228
  /** C-7 (v18): per-name dispose, mirroring the evolution-io registry. */
176
229
  disposals = /* @__PURE__ */ new Map();
230
+ /** v28 G3.1 (STATE-04): the multi-provider warn fires once per ambiguous
231
+ * period, not once per state op (provider() runs on every read/write). */
232
+ ambiguousWarned = false;
177
233
  constructor(ctx) {
178
234
  super(ctx, "evolutionStateStorage");
179
235
  }
@@ -206,14 +262,21 @@ var EvolutionStateStorageRegistry = class extends Service {
206
262
  }
207
263
  provider(name) {
208
264
  if (name) {
265
+ this.ambiguousWarned = false;
209
266
  const provider = this.providers.get(name);
210
267
  if (provider) return provider;
211
268
  throw new Error(`evolution state storage provider "${name}" is not registered`);
212
269
  }
213
270
  const first = this.providers.values().next().value;
214
271
  if (!first) throw new Error("no evolution state storage provider registered; mount @lmzhen/dsh-evolution-state-json or @lmzhen/dsh-evolution-state-domain");
272
+ if (this.providers.size > 1) {
273
+ if (!this.ambiguousWarned) {
274
+ this.ambiguousWarned = true;
275
+ this.ctx.logger.warn(`evolution state storage: ${this.providers.size} providers registered (${[...this.providers.keys()].join(", ")}), none pinned — the effective provider is "${first.name}" (registration order). Existing state under the other provider is now invisible. Pin config: { provider: json|domain } on the evolution-state row to make the choice explicit.`);
276
+ }
277
+ } else this.ambiguousWarned = false;
215
278
  return first;
216
279
  }
217
280
  };
218
281
  //#endregion
219
- export { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, EvolutionStateStorageRegistry, EvolutionStateStorageRegistry as default, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, 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 };
282
+ export { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, EvolutionStateStorageRegistry, EvolutionStateStorageRegistry as default, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, 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, selectPendingOverflow, selectSessionOverflow };
@@ -60,6 +60,48 @@ export declare const PENDING_RESOLVED_CAP = 200;
60
60
  * interface change, no background sweeper).
61
61
  */
62
62
  export declare const REVIEW_STATE_SESSION_CAP = 500;
63
+ /**
64
+ * V27 G2.2: WHICH pending records the audit cap evicts, as one pure rule both
65
+ * providers apply (the two implementations had drifted into separate files and
66
+ * only one of them was ever updated by a later fix).
67
+ *
68
+ * Eligible = resolved (`approved`/`rejected`) and NOT a capability approval:
69
+ * v23 (AP-1) exempts those because the Creator-mode contract reads the LIVE
70
+ * approved list and a capability cannot be re-submitted for the same package,
71
+ * so eviction would make an approved capability permanently unactivatable.
72
+ * Ordering = oldest `resolvedAt` first; a missing or unparseable timestamp sorts
73
+ * LAST (json parity, v16), and pending/executing rows are live work that is
74
+ * never trimmed. v28 G2.4 (STATE-03) — the previous wording promised "an
75
+ * unknown time must never make a record the victim"; that is only true while
76
+ * the overflow fits within the KNOWN-timestamp records. When the overflow
77
+ * exceeds them (corruption-level data: most resolved rows with broken
78
+ * timestamps), `slice` necessarily reaches into the unknown-timestamp tail —
79
+ * the cap must be enforced, so there is no alternative. The guarantee is
80
+ * therefore: unknown-timestamp records are evicted LAST, after every known
81
+ * timestamp, in insertion order among themselves (Array#sort stability).
82
+ *
83
+ * @param records - every pending record currently in the live table.
84
+ * @param cap - how many resolved records may be kept.
85
+ * @returns the records to evict, oldest first (empty when within the cap).
86
+ */
87
+ export declare function selectPendingOverflow(records: readonly PendingRecord[], cap?: number): PendingRecord[];
88
+ /**
89
+ * V27 G2.2: which review-state session rows a save evicts, as one pure rule
90
+ * both providers apply. The saving session is never a candidate (it is the most
91
+ * recent write by definition); the rest are ordered by their provider stamp
92
+ * ascending, with a missing stamp read as 0 = oldest, because an active session
93
+ * re-stamps its row on its next save and an unknown stamp is stale by
94
+ * construction. Exactly one row is dropped per over-cap save, which keeps the
95
+ * table at the cap in steady state.
96
+ *
97
+ * @param rows - the other sessions' rows, with their stamps.
98
+ * @param cap - how many session rows may exist.
99
+ * @returns the keys to delete, oldest first.
100
+ */
101
+ export declare function selectSessionOverflow<T>(rows: readonly T[], options: {
102
+ keyOf(row: T): string;
103
+ stampOf(row: T): number;
104
+ }, cap?: number): string[];
63
105
  /**
64
106
  * Claim lifecycle (S3.3): pending →(claim)→ executing →(resolve)→ approved/rejected.
65
107
  * release() rolls executing back to pending (failure path). A crash between
@@ -113,6 +155,13 @@ export interface EvolutionStateStorage {
113
155
  * whole read → transform → write runs inside one provider transact, so a
114
156
  * setPaused racing the run-core bookkeeping write can never interleave a
115
157
  * stale load with a newer save.
158
+ *
159
+ * V27 S4: the record handed to `task` belongs to the task — a provider must
160
+ * NEVER pass the object it stores (both current providers hand out a copy:
161
+ * json re-parses its medium, the domain clones its record). Mutating the
162
+ * argument is not a supported way to write: a task that does so and then lets
163
+ * validation refuse the result would otherwise leave a mutated object in the
164
+ * provider's in-memory store.
116
165
  */
117
166
  transactCuratorState(task: (current: CuratorStateRecord | null) => CuratorStateRecord | null): Promise<void>;
118
167
  listPending(status?: PendingStatus): Promise<PendingRecord[]>;
@@ -139,6 +188,9 @@ export declare class EvolutionStateStorageRegistry extends Service {
139
188
  private readonly providers;
140
189
  /** C-7 (v18): per-name dispose, mirroring the evolution-io registry. */
141
190
  private readonly disposals;
191
+ /** v28 G3.1 (STATE-04): the multi-provider warn fires once per ambiguous
192
+ * period, not once per state op (provider() runs on every read/write). */
193
+ private ambiguousWarned;
142
194
  constructor(ctx: Context);
143
195
  /** C-7 (v18): re-registering the IDENTICAL provider object is idempotent and
144
196
  * returns the original dispose (HMR / re-mounted row); a DIFFERENT object
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.3.67",
4
+ "version": "0.3.69",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },