@lmzhen/dsh-evolution-state-storage 0.3.81 → 0.3.83
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/README.md +15 -2
- package/lib/index.js +268 -27
- package/lib/types/conformance.d.ts +27 -0
- package/lib/types/constants.d.ts +26 -0
- package/lib/types/index.d.ts +1 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @lmzhen/dsh-evolution-state-storage
|
|
2
2
|
|
|
3
3
|
Provider registry seam for durable evolution state
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ Provider registry seam for durable evolution state
|
|
|
9
9
|
|
|
10
10
|
#### What the model sees
|
|
11
11
|
|
|
12
|
-
`@
|
|
12
|
+
`@lmzhen/dsh-evolution-state-storage` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
|
|
13
13
|
|
|
14
14
|
#### Token effect
|
|
15
15
|
|
|
@@ -19,6 +19,19 @@ Zero direct token effect from this package; consumers add any model-visible toke
|
|
|
19
19
|
|
|
20
20
|
Independent of request-prefix construction. This package does not alter the assembled prompt or tool list.
|
|
21
21
|
|
|
22
|
+
## Conformance suite (S3-2 / J-5)
|
|
23
|
+
|
|
24
|
+
The package publishes `runStateProviderConsistency(provider, assert)` — the whole seam contract as one call: field-complete pending round-trips, claim/resolve rollback, refusals at the write boundary, clone/alias independence, unknown-field preservation, and the two caps (resolved pending tail, review-state session rows). A third-party provider checks itself by running the suite against its own instance:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { runStateProviderConsistency } from '@lmzhen/dsh-evolution-state-storage'
|
|
28
|
+
import { expect } from 'vitest' // or any runner wrapped into `ConformanceAssert`
|
|
29
|
+
|
|
30
|
+
await runStateProviderConsistency(myProvider, expect)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The assertion surface is INJECTED rather than imported, which is what lets this module ship: it carries no test-runner dependency, so loading the package never loads vitest. Both shipped providers run the same suite (`evolution-state-json` / `evolution-state-domain` `tests/provider-consistency.spec.ts`), and `consistency-forge.spec.ts` proves the suite fails when a single field is forged.
|
|
34
|
+
|
|
22
35
|
## Known Limitations and Deferred Work
|
|
23
36
|
|
|
24
37
|
|
package/lib/index.js
CHANGED
|
@@ -32,6 +32,32 @@ const PROVIDER_DOMAIN = "domain";
|
|
|
32
32
|
const REVIEW_STATE_TABLE = "review_state";
|
|
33
33
|
const CURATOR_STATE_TABLE = "curator_state";
|
|
34
34
|
const PENDING_TABLE = "pending";
|
|
35
|
+
/** P2-4 (v15): the live pending map/table is BOUNDED on the RESOLVE path —
|
|
36
|
+
* `tryResolvePending` drops the oldest resolved (approved/rejected) records by
|
|
37
|
+
* `resolvedAt` once more than this many exist. Single source (the v15 audit
|
|
38
|
+
* found the bound was json-only, so domain deployments grew the table without
|
|
39
|
+
* bound).
|
|
40
|
+
*
|
|
41
|
+
* C-6 (v18) contract precision: a direct `savePending` of an already-resolved
|
|
42
|
+
* record does NOT trigger eviction (the cap is maintained by the resolve
|
|
43
|
+
* operation, not by the writer), and pending/executing records are never
|
|
44
|
+
* trimmed. Callers that write resolved audit records themselves own that
|
|
45
|
+
* growth; the seam's resolve path is what keeps the table bounded.
|
|
46
|
+
* The audit ARCHIVE sidecar that json maintains beyond the cap stays
|
|
47
|
+
* json-specific (domain has no sidecar facility) — declared in both READMEs. */
|
|
48
|
+
const PENDING_RESOLVED_CAP = 200;
|
|
49
|
+
/**
|
|
50
|
+
* V24-08 (v24): session rows in the review-state table, per session id. The
|
|
51
|
+
* review pipeline saves on EVERY turn/end of EVERY session and nothing ever
|
|
52
|
+
* deleted rows, so the file grew (and was fully rewritten) with the deploy's
|
|
53
|
+
* whole session history — the same unbounded-growth class the pending cap
|
|
54
|
+
* above already fixed for approvals. A review row is advisory cadence state:
|
|
55
|
+
* evicting the least-recently-active session merely lets that session's next
|
|
56
|
+
* review fire from a fresh counter, so a generous cap is loss-less in
|
|
57
|
+
* practice. Enforced by BOTH providers inside their save path (no seam
|
|
58
|
+
* interface change, no background sweeper).
|
|
59
|
+
*/
|
|
60
|
+
const REVIEW_STATE_SESSION_CAP = 500;
|
|
35
61
|
//#endregion
|
|
36
62
|
//#region lib/types/record-contract.js
|
|
37
63
|
/**
|
|
@@ -127,6 +153,247 @@ function cloneRecord(record) {
|
|
|
127
153
|
return structuredClone(record);
|
|
128
154
|
}
|
|
129
155
|
//#endregion
|
|
156
|
+
//#region lib/types/conformance.js
|
|
157
|
+
/**
|
|
158
|
+
* Published conformance suite for `EvolutionStateStorage` providers (J-5 / S3-2).
|
|
159
|
+
*
|
|
160
|
+
* A third-party provider (or a new medium inside this family) runs ONE call to
|
|
161
|
+
* check the seam contract end to end: field-complete round-trips, claim/resolve
|
|
162
|
+
* rollback, refusal at the write boundary, clone/alias independence, unknown-field
|
|
163
|
+
* preservation, and the two seam caps (the resolved pending tail and the
|
|
164
|
+
* review-state session rows).
|
|
165
|
+
*
|
|
166
|
+
* The assertion surface is INJECTED ({@link ConformanceAssert}) rather than
|
|
167
|
+
* imported: vitest satisfies it in-tree, and a host that only has node:assert can
|
|
168
|
+
* wrap it in a few lines. That keeps this module free of any test-runner import,
|
|
169
|
+
* so it ships inside the published package.
|
|
170
|
+
*/
|
|
171
|
+
/**
|
|
172
|
+
* Cross-provider consistency harness (G7.4, 0.3.22; V4-07 0.3.28).
|
|
173
|
+
*
|
|
174
|
+
* Runs the SAME observable operation sequence against an
|
|
175
|
+
* `EvolutionStateStorage` provider (json or domain) and asserts both yield the
|
|
176
|
+
* same field-complete result. Every pending-record field that is carried
|
|
177
|
+
* through a round-trip is compared field by field: id, kind, status, summary,
|
|
178
|
+
* args, createdAt, origin, sessionId, claimedBy. The wall-clock holder stamps
|
|
179
|
+
* (claimedAt, resolvedAt) are generated by each provider at its own transact
|
|
180
|
+
* time and legitimately differ in value between the two claims, so they are
|
|
181
|
+
* asserted only for presence/type (a string), never for equality — and
|
|
182
|
+
* claimedAt is asserted to be cleared on release. Status filtering / release
|
|
183
|
+
* rollback must agree. S2-4 (v29): the two seam caps — the resolved pending
|
|
184
|
+
* tail (P2-4) and the review-state session rows (V24-08) — are exercised through
|
|
185
|
+
* the same public operations on both providers, so neither medium can keep the
|
|
186
|
+
* bound to itself.
|
|
187
|
+
*/
|
|
188
|
+
/** Build a fixture pending record, optionally carrying origin/sessionId. */
|
|
189
|
+
const pendingOf = (id, kind = "memory", attribution = {}) => ({
|
|
190
|
+
id,
|
|
191
|
+
kind,
|
|
192
|
+
summary: `${kind}:${id}`,
|
|
193
|
+
args: {},
|
|
194
|
+
createdAt: "now",
|
|
195
|
+
status: "pending",
|
|
196
|
+
...attribution
|
|
197
|
+
});
|
|
198
|
+
/** Every non-holder pending field must equal the input, field by field. */
|
|
199
|
+
const expectPendingCarried = (assert, actual, input) => {
|
|
200
|
+
assert(actual).toBeDefined();
|
|
201
|
+
const carried = actual;
|
|
202
|
+
assert(carried.id).toBe(input.id);
|
|
203
|
+
assert(carried.kind).toBe(input.kind);
|
|
204
|
+
assert(carried.summary).toBe(input.summary);
|
|
205
|
+
assert(carried.args).toEqual(input.args);
|
|
206
|
+
assert(carried.createdAt).toBe(input.createdAt);
|
|
207
|
+
assert(carried.origin).toBe(input.origin);
|
|
208
|
+
assert(carried.sessionId).toBe(input.sessionId);
|
|
209
|
+
};
|
|
210
|
+
/** The listing must contain `id`; a missing row fails the SUITE, not the runner. */
|
|
211
|
+
const findRecord = (records, id) => {
|
|
212
|
+
const found = records.find((record) => record.id === id);
|
|
213
|
+
if (found === void 0) throw new Error(`conformance: pending record "${id}" missing from the listing`);
|
|
214
|
+
return found;
|
|
215
|
+
};
|
|
216
|
+
async function runStateProviderConsistency(provider, assert) {
|
|
217
|
+
await provider.saveReviewState("s-consistent", {
|
|
218
|
+
turnsSinceMemory: 1,
|
|
219
|
+
turnsSinceSkill: 2,
|
|
220
|
+
lastTurn: 3
|
|
221
|
+
});
|
|
222
|
+
assert(await provider.loadReviewState("s-consistent")).toEqual({
|
|
223
|
+
turnsSinceMemory: 1,
|
|
224
|
+
turnsSinceSkill: 2,
|
|
225
|
+
lastTurn: 3
|
|
226
|
+
});
|
|
227
|
+
assert(await provider.loadCuratorState()).toBeNull();
|
|
228
|
+
await provider.transactCuratorState(() => ({
|
|
229
|
+
lastRunAt: 10,
|
|
230
|
+
runCount: 0,
|
|
231
|
+
lastSummary: "seed",
|
|
232
|
+
paused: false
|
|
233
|
+
}));
|
|
234
|
+
assert(await provider.loadCuratorState()).toEqual({
|
|
235
|
+
lastRunAt: 10,
|
|
236
|
+
runCount: 0,
|
|
237
|
+
lastSummary: "seed",
|
|
238
|
+
paused: false
|
|
239
|
+
});
|
|
240
|
+
await provider.transactCuratorState(() => null);
|
|
241
|
+
assert(await provider.loadCuratorState()).toEqual({
|
|
242
|
+
lastRunAt: 10,
|
|
243
|
+
runCount: 0,
|
|
244
|
+
lastSummary: "seed",
|
|
245
|
+
paused: false
|
|
246
|
+
});
|
|
247
|
+
await provider.transactCuratorState((current) => ({
|
|
248
|
+
...current,
|
|
249
|
+
lastSummary: "updated"
|
|
250
|
+
}));
|
|
251
|
+
assert(await provider.loadCuratorState()).toEqual({
|
|
252
|
+
lastRunAt: 10,
|
|
253
|
+
runCount: 0,
|
|
254
|
+
lastSummary: "updated",
|
|
255
|
+
paused: false
|
|
256
|
+
});
|
|
257
|
+
await provider.transactCuratorState((current) => {
|
|
258
|
+
if (current !== null) {
|
|
259
|
+
current.runCount = 999;
|
|
260
|
+
current.paused = true;
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
});
|
|
264
|
+
assert(await provider.loadCuratorState()).toEqual({
|
|
265
|
+
lastRunAt: 10,
|
|
266
|
+
runCount: 0,
|
|
267
|
+
lastSummary: "updated",
|
|
268
|
+
paused: false
|
|
269
|
+
});
|
|
270
|
+
const live = pendingOf("c-live");
|
|
271
|
+
await provider.savePending(live);
|
|
272
|
+
assert((await provider.listPending("pending")).map((record) => record.id)).toContain("c-live");
|
|
273
|
+
const claimed = await provider.claimPending("c-live", "claim-a");
|
|
274
|
+
assert(claimed?.status).toBe("executing");
|
|
275
|
+
assert(claimed?.claimedBy).toBe("claim-a");
|
|
276
|
+
expectPendingCarried(assert, claimed, live);
|
|
277
|
+
assert(typeof claimed?.claimedAt).toBe("string");
|
|
278
|
+
const resolved = await provider.tryResolvePending("c-live", "approved");
|
|
279
|
+
assert(resolved.applied).toBe(true);
|
|
280
|
+
assert(resolved.record?.id).toBe("c-live");
|
|
281
|
+
assert(resolved.record?.status).toBe("approved");
|
|
282
|
+
expectPendingCarried(assert, resolved.record, live);
|
|
283
|
+
assert(resolved.record?.claimedBy).toBe("claim-a");
|
|
284
|
+
assert(typeof resolved.record?.claimedAt).toBe("string");
|
|
285
|
+
assert(typeof resolved.record?.resolvedAt).toBe("string");
|
|
286
|
+
assert((await provider.listPending("approved")).map((record) => record.id)).toContain("c-live");
|
|
287
|
+
assert((await provider.listPending("pending")).map((record) => record.id)).not.toContain("c-live");
|
|
288
|
+
const attrib = pendingOf("c-attrib", "memory", {
|
|
289
|
+
origin: "background_review",
|
|
290
|
+
sessionId: "sess-attrib"
|
|
291
|
+
});
|
|
292
|
+
await provider.savePending(attrib);
|
|
293
|
+
expectPendingCarried(assert, await provider.claimPending("c-attrib", "claim-a"), attrib);
|
|
294
|
+
const attribResolved = await provider.tryResolvePending("c-attrib", "approved");
|
|
295
|
+
expectPendingCarried(assert, attribResolved.record, attrib);
|
|
296
|
+
assert(attribResolved.record?.origin).toBe("background_review");
|
|
297
|
+
assert(attribResolved.record?.sessionId).toBe("sess-attrib");
|
|
298
|
+
assert(typeof attribResolved.record?.resolvedAt).toBe("string");
|
|
299
|
+
const rel = pendingOf("c-release", "skill");
|
|
300
|
+
await provider.savePending(rel);
|
|
301
|
+
assert((await provider.claimPending("c-release", "claim-b"))?.status).toBe("executing");
|
|
302
|
+
await provider.releasePendingClaim("c-release", "claim-b");
|
|
303
|
+
const released = (await provider.listPending("pending")).find((record) => record.id === "c-release");
|
|
304
|
+
assert(released?.status).toBe("pending");
|
|
305
|
+
expectPendingCarried(assert, released, rel);
|
|
306
|
+
assert(released?.claimedBy).toBeUndefined();
|
|
307
|
+
assert(released?.claimedAt).toBeUndefined();
|
|
308
|
+
const scoped = pendingOf("c-scoped");
|
|
309
|
+
await provider.savePending(scoped);
|
|
310
|
+
await provider.claimPending("c-scoped", "claim-owner");
|
|
311
|
+
assert((await provider.tryResolvePending("c-scoped", "approved", "claim-foreign")).applied).toBe(false);
|
|
312
|
+
assert((await provider.listPending("executing")).map((record) => record.id)).toContain("c-scoped");
|
|
313
|
+
const ownerScope = await provider.tryResolvePending("c-scoped", "approved", "claim-owner");
|
|
314
|
+
assert(ownerScope.applied).toBe(true);
|
|
315
|
+
assert(ownerScope.record?.status).toBe("approved");
|
|
316
|
+
const filt = pendingOf("c-filter");
|
|
317
|
+
await provider.savePending(filt);
|
|
318
|
+
assert((await provider.listPending("pending")).map((record) => record.id)).toContain("c-filter");
|
|
319
|
+
assert((await provider.listPending("approved")).map((record) => record.id)).not.toContain("c-filter");
|
|
320
|
+
const post = pendingOf("c-post-resolve", "memory");
|
|
321
|
+
await provider.savePending(post);
|
|
322
|
+
await provider.claimPending("c-post-resolve", "claim-c");
|
|
323
|
+
await provider.tryResolvePending("c-post-resolve", "rejected");
|
|
324
|
+
await provider.releasePendingClaim("c-post-resolve", "claim-c");
|
|
325
|
+
const postResolved = (await provider.listPending("rejected")).find((record) => record.id === "c-post-resolve");
|
|
326
|
+
assert(postResolved?.status).toBe("rejected");
|
|
327
|
+
assert(postResolved?.claimedBy).toBe("claim-c");
|
|
328
|
+
assert(typeof postResolved?.claimedAt).toBe("string");
|
|
329
|
+
assert(typeof postResolved?.resolvedAt).toBe("string");
|
|
330
|
+
await assert(provider.savePending({
|
|
331
|
+
id: "c-bad",
|
|
332
|
+
kind: "memory",
|
|
333
|
+
summary: "bad",
|
|
334
|
+
createdAt: "now",
|
|
335
|
+
status: "pending"
|
|
336
|
+
})).rejects.toThrow();
|
|
337
|
+
const poison = pendingOf("c-poison");
|
|
338
|
+
poison.args = { nested: { value: 1 } };
|
|
339
|
+
await provider.savePending(poison);
|
|
340
|
+
const returned = findRecord(await provider.listPending("pending"), "c-poison");
|
|
341
|
+
returned.summary = "poisoned";
|
|
342
|
+
returned.args.nested.value = 99;
|
|
343
|
+
const reread = findRecord(await provider.listPending("pending"), "c-poison");
|
|
344
|
+
assert(reread.summary).toBe("memory:c-poison");
|
|
345
|
+
assert(reread.args.nested.value).toBe(1);
|
|
346
|
+
const uncloneable = {
|
|
347
|
+
...pendingOf("c-uncloneable"),
|
|
348
|
+
args: { fn: () => {} }
|
|
349
|
+
};
|
|
350
|
+
await assert(provider.savePending(uncloneable)).rejects.toThrow();
|
|
351
|
+
const aliased = pendingOf("c-alias");
|
|
352
|
+
aliased.args = { nested: { value: 1 } };
|
|
353
|
+
await provider.savePending(aliased);
|
|
354
|
+
aliased.args.nested.value = 99;
|
|
355
|
+
assert(findRecord(await provider.listPending("pending"), "c-alias").args.nested.value).toBe(1);
|
|
356
|
+
await assert(provider.saveCuratorState({
|
|
357
|
+
lastRunAt: 1,
|
|
358
|
+
runCount: 0,
|
|
359
|
+
lastSummary: "x",
|
|
360
|
+
paused: false,
|
|
361
|
+
schemaVersion: -1
|
|
362
|
+
})).rejects.toThrow();
|
|
363
|
+
await provider.savePending({
|
|
364
|
+
...pendingOf("c-extra"),
|
|
365
|
+
extraField: "kept"
|
|
366
|
+
});
|
|
367
|
+
assert((await provider.listPending("pending")).find((record) => record.id === "c-extra").extraField).toBe("kept");
|
|
368
|
+
const capPrefix = "c-cap-";
|
|
369
|
+
await provider.savePending(pendingOf("c-cap-live", "skill"));
|
|
370
|
+
for (let index = 0; index <= 201; index += 1) {
|
|
371
|
+
const id = capPrefix + String(index).padStart(3, "0");
|
|
372
|
+
await provider.savePending(pendingOf(id));
|
|
373
|
+
await provider.claimPending(id, "claim-cap");
|
|
374
|
+
await provider.tryResolvePending(id, "approved");
|
|
375
|
+
}
|
|
376
|
+
const resolvedNow = (await provider.listPending("approved")).concat(await provider.listPending("rejected")).filter((record) => record.kind !== "capability");
|
|
377
|
+
assert(resolvedNow).toHaveLength(200);
|
|
378
|
+
const resolvedIds = resolvedNow.map((record) => record.id);
|
|
379
|
+
assert(resolvedIds).not.toContain("c-cap-000");
|
|
380
|
+
assert(resolvedIds).not.toContain("c-cap-001");
|
|
381
|
+
assert(resolvedIds).toContain(capPrefix + String(201).padStart(3, "0"));
|
|
382
|
+
assert((await provider.listPending("pending")).map((record) => record.id)).toContain("c-cap-live");
|
|
383
|
+
const sessionPrefix = "s-cap-";
|
|
384
|
+
const reviewStateOf = (turns) => ({
|
|
385
|
+
turnsSinceMemory: turns,
|
|
386
|
+
turnsSinceSkill: 0,
|
|
387
|
+
lastTurn: turns
|
|
388
|
+
});
|
|
389
|
+
for (let index = 0; index <= 500; index += 1) await provider.saveReviewState(sessionPrefix + String(index), reviewStateOf(index));
|
|
390
|
+
assert(await provider.loadReviewState("s-cap-0")).toBeNull();
|
|
391
|
+
assert(await provider.loadReviewState("s-cap-1")).toEqual(reviewStateOf(1));
|
|
392
|
+
assert(await provider.loadReviewState(sessionPrefix + String(500))).toEqual(reviewStateOf(500));
|
|
393
|
+
await provider.saveReviewState("s-cap-0", reviewStateOf(999));
|
|
394
|
+
assert(await provider.loadReviewState("s-cap-0")).toEqual(reviewStateOf(999));
|
|
395
|
+
}
|
|
396
|
+
//#endregion
|
|
130
397
|
//#region lib/types/index.js
|
|
131
398
|
/**
|
|
132
399
|
* Provider seam for durable evolution state.
|
|
@@ -144,32 +411,6 @@ const canResolvePending = (status) => status === "pending" || status === "execut
|
|
|
144
411
|
/** Releasing a claim on an executing record rolls it back to pending (a
|
|
145
412
|
* runner FAILURE is retryable); other statuses pass through unchanged. */
|
|
146
413
|
const releasedStatus = (status) => status === "executing" ? "pending" : status;
|
|
147
|
-
/** P2-4 (v15): the live pending map/table is BOUNDED on the RESOLVE path —
|
|
148
|
-
* `tryResolvePending` drops the oldest resolved (approved/rejected) records by
|
|
149
|
-
* `resolvedAt` once more than this many exist. Single source (the v15 audit
|
|
150
|
-
* found the bound was json-only, so domain deployments grew the table without
|
|
151
|
-
* bound).
|
|
152
|
-
*
|
|
153
|
-
* C-6 (v18) contract precision: a direct `savePending` of an already-resolved
|
|
154
|
-
* record does NOT trigger eviction (the cap is maintained by the resolve
|
|
155
|
-
* operation, not by the writer), and pending/executing records are never
|
|
156
|
-
* trimmed. Callers that write resolved audit records themselves own that
|
|
157
|
-
* growth; the seam's resolve path is what keeps the table bounded.
|
|
158
|
-
* The audit ARCHIVE sidecar that json maintains beyond the cap stays
|
|
159
|
-
* json-specific (domain has no sidecar facility) — declared in both READMEs. */
|
|
160
|
-
const PENDING_RESOLVED_CAP = 200;
|
|
161
|
-
/**
|
|
162
|
-
* V24-08 (v24): session rows in the review-state table, per session id. The
|
|
163
|
-
* review pipeline saves on EVERY turn/end of EVERY session and nothing ever
|
|
164
|
-
* deleted rows, so the file grew (and was fully rewritten) with the deploy's
|
|
165
|
-
* whole session history — the same unbounded-growth class the pending cap
|
|
166
|
-
* above already fixed for approvals. A review row is advisory cadence state:
|
|
167
|
-
* evicting the least-recently-active session merely lets that session's next
|
|
168
|
-
* review fire from a fresh counter, so a generous cap is loss-less in
|
|
169
|
-
* practice. Enforced by BOTH providers inside their save path (no seam
|
|
170
|
-
* interface change, no background sweeper).
|
|
171
|
-
*/
|
|
172
|
-
const REVIEW_STATE_SESSION_CAP = 500;
|
|
173
414
|
/**
|
|
174
415
|
* V27 G2.2: WHICH pending records the audit cap evicts, as one pure rule both
|
|
175
416
|
* providers apply (the two implementations had drifted into separate files and
|
|
@@ -279,4 +520,4 @@ var EvolutionStateStorageRegistry = class extends Service {
|
|
|
279
520
|
}
|
|
280
521
|
};
|
|
281
522
|
//#endregion
|
|
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 };
|
|
523
|
+
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, runStateProviderConsistency, selectPendingOverflow, selectSessionOverflow };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EvolutionStateStorage } from './index.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The assertion surface this suite needs. Vitest's `expect` satisfies it in-tree;
|
|
4
|
+
* a host with any other runner wraps its own in a few lines. Deliberately
|
|
5
|
+
* structural and minimal — the suite must not import a test runner.
|
|
6
|
+
*/
|
|
7
|
+
export interface ConformanceAssert {
|
|
8
|
+
(actual: unknown): {
|
|
9
|
+
toBe(expected: unknown): void;
|
|
10
|
+
toEqual(expected: unknown): void;
|
|
11
|
+
toBeDefined(): void;
|
|
12
|
+
toBeUndefined(): void;
|
|
13
|
+
toBeNull(): void;
|
|
14
|
+
toContain(expected: unknown): void;
|
|
15
|
+
toHaveLength(length: number): void;
|
|
16
|
+
toBeGreaterThan(expected: number): void;
|
|
17
|
+
not: {
|
|
18
|
+
toBe(expected: unknown): void;
|
|
19
|
+
toContain(expected: unknown): void;
|
|
20
|
+
};
|
|
21
|
+
rejects: {
|
|
22
|
+
toThrow(expected?: unknown): Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export declare function runStateProviderConsistency(provider: EvolutionStateStorage, assert: ConformanceAssert): Promise<void>;
|
|
27
|
+
//# sourceMappingURL=conformance.d.ts.map
|
package/lib/types/constants.d.ts
CHANGED
|
@@ -30,4 +30,30 @@ export declare const PROVIDER_DOMAIN = "domain";
|
|
|
30
30
|
export declare const REVIEW_STATE_TABLE = "review_state";
|
|
31
31
|
export declare const CURATOR_STATE_TABLE = "curator_state";
|
|
32
32
|
export declare const PENDING_TABLE = "pending";
|
|
33
|
+
/** P2-4 (v15): the live pending map/table is BOUNDED on the RESOLVE path —
|
|
34
|
+
* `tryResolvePending` drops the oldest resolved (approved/rejected) records by
|
|
35
|
+
* `resolvedAt` once more than this many exist. Single source (the v15 audit
|
|
36
|
+
* found the bound was json-only, so domain deployments grew the table without
|
|
37
|
+
* bound).
|
|
38
|
+
*
|
|
39
|
+
* C-6 (v18) contract precision: a direct `savePending` of an already-resolved
|
|
40
|
+
* record does NOT trigger eviction (the cap is maintained by the resolve
|
|
41
|
+
* operation, not by the writer), and pending/executing records are never
|
|
42
|
+
* trimmed. Callers that write resolved audit records themselves own that
|
|
43
|
+
* growth; the seam's resolve path is what keeps the table bounded.
|
|
44
|
+
* The audit ARCHIVE sidecar that json maintains beyond the cap stays
|
|
45
|
+
* json-specific (domain has no sidecar facility) — declared in both READMEs. */
|
|
46
|
+
export declare const PENDING_RESOLVED_CAP = 200;
|
|
47
|
+
/**
|
|
48
|
+
* V24-08 (v24): session rows in the review-state table, per session id. The
|
|
49
|
+
* review pipeline saves on EVERY turn/end of EVERY session and nothing ever
|
|
50
|
+
* deleted rows, so the file grew (and was fully rewritten) with the deploy's
|
|
51
|
+
* whole session history — the same unbounded-growth class the pending cap
|
|
52
|
+
* above already fixed for approvals. A review row is advisory cadence state:
|
|
53
|
+
* evicting the least-recently-active session merely lets that session's next
|
|
54
|
+
* review fire from a fresh counter, so a generous cap is loss-less in
|
|
55
|
+
* practice. Enforced by BOTH providers inside their save path (no seam
|
|
56
|
+
* interface change, no background sweeper).
|
|
57
|
+
*/
|
|
58
|
+
export declare const REVIEW_STATE_SESSION_CAP = 500;
|
|
33
59
|
//# sourceMappingURL=constants.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
10
10
|
export * from './constants.ts';
|
|
11
11
|
export * from './record-contract.ts';
|
|
12
|
+
export * from './conformance.ts';
|
|
12
13
|
/** 0.3.17 (S3.5, D-4): 'skill_batch' removed — nothing ever created one
|
|
13
14
|
* (dead enum member); the historic value, if it ever reached disk, is read as
|
|
14
15
|
* an unknown kind by consumers rather than minted here.
|
|
@@ -34,32 +35,6 @@ export declare const canResolvePending: (status: PendingStatus) => boolean;
|
|
|
34
35
|
/** Releasing a claim on an executing record rolls it back to pending (a
|
|
35
36
|
* runner FAILURE is retryable); other statuses pass through unchanged. */
|
|
36
37
|
export declare const releasedStatus: (status: PendingStatus) => PendingStatus;
|
|
37
|
-
/** P2-4 (v15): the live pending map/table is BOUNDED on the RESOLVE path —
|
|
38
|
-
* `tryResolvePending` drops the oldest resolved (approved/rejected) records by
|
|
39
|
-
* `resolvedAt` once more than this many exist. Single source (the v15 audit
|
|
40
|
-
* found the bound was json-only, so domain deployments grew the table without
|
|
41
|
-
* bound).
|
|
42
|
-
*
|
|
43
|
-
* C-6 (v18) contract precision: a direct `savePending` of an already-resolved
|
|
44
|
-
* record does NOT trigger eviction (the cap is maintained by the resolve
|
|
45
|
-
* operation, not by the writer), and pending/executing records are never
|
|
46
|
-
* trimmed. Callers that write resolved audit records themselves own that
|
|
47
|
-
* growth; the seam's resolve path is what keeps the table bounded.
|
|
48
|
-
* The audit ARCHIVE sidecar that json maintains beyond the cap stays
|
|
49
|
-
* json-specific (domain has no sidecar facility) — declared in both READMEs. */
|
|
50
|
-
export declare const PENDING_RESOLVED_CAP = 200;
|
|
51
|
-
/**
|
|
52
|
-
* V24-08 (v24): session rows in the review-state table, per session id. The
|
|
53
|
-
* review pipeline saves on EVERY turn/end of EVERY session and nothing ever
|
|
54
|
-
* deleted rows, so the file grew (and was fully rewritten) with the deploy's
|
|
55
|
-
* whole session history — the same unbounded-growth class the pending cap
|
|
56
|
-
* above already fixed for approvals. A review row is advisory cadence state:
|
|
57
|
-
* evicting the least-recently-active session merely lets that session's next
|
|
58
|
-
* review fire from a fresh counter, so a generous cap is loss-less in
|
|
59
|
-
* practice. Enforced by BOTH providers inside their save path (no seam
|
|
60
|
-
* interface change, no background sweeper).
|
|
61
|
-
*/
|
|
62
|
-
export declare const REVIEW_STATE_SESSION_CAP = 500;
|
|
63
38
|
/**
|
|
64
39
|
* V27 G2.2: WHICH pending records the audit cap evicts, as one pure rule both
|
|
65
40
|
* providers apply (the two implementations had drifted into separate files and
|