@lmzhen/dsh-evolution-state-storage 0.3.64 → 0.3.66
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 +94 -1
- package/lib/types/index.d.ts +11 -1
- package/lib/types/record-contract.d.ts +42 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -33,6 +33,99 @@ const REVIEW_STATE_TABLE = "review_state";
|
|
|
33
33
|
const CURATOR_STATE_TABLE = "curator_state";
|
|
34
34
|
const PENDING_TABLE = "pending";
|
|
35
35
|
//#endregion
|
|
36
|
+
//#region lib/types/record-contract.js
|
|
37
|
+
/**
|
|
38
|
+
* Record contract of the three seam tables (P2-12/14/15/16/18, v19).
|
|
39
|
+
*
|
|
40
|
+
* The contract belongs to the SEAM, not to each provider: before this module
|
|
41
|
+
* the json provider gated writes with plain field predicates while the domain
|
|
42
|
+
* provider relied on its zod schemas, so the same consumer code behaved
|
|
43
|
+
* differently per medium — `transactCuratorState` skipped validation entirely
|
|
44
|
+
* on domain, unknown fields were stripped by zod but preserved by json,
|
|
45
|
+
* `args` was shared by reference on domain, and a non-cloneable payload
|
|
46
|
+
* poisoned every later read. Both providers now call these functions.
|
|
47
|
+
*
|
|
48
|
+
* UNKNOWN_FIELD_POLICY is `preserve`: a record written by a newer version must
|
|
49
|
+
* survive a round-trip through an older provider, so unknown fields are kept
|
|
50
|
+
* (json does this naturally; the domain schemas are `.loose()`).
|
|
51
|
+
*
|
|
52
|
+
* @module @lmzhen/dsh-evolution-state-storage/src/record-contract
|
|
53
|
+
*/
|
|
54
|
+
/** Unknown fields survive a provider round-trip (json preserves by
|
|
55
|
+
* construction; the domain schemas are `.loose()`). */
|
|
56
|
+
const UNKNOWN_FIELD_POLICY = "preserve";
|
|
57
|
+
const isNonNegInt = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
58
|
+
const optionalString = (value) => value === void 0 || typeof value === "string";
|
|
59
|
+
const PENDING_KINDS = new Set([
|
|
60
|
+
"memory",
|
|
61
|
+
"skill",
|
|
62
|
+
"capability"
|
|
63
|
+
]);
|
|
64
|
+
const PENDING_STATUSES = new Set([
|
|
65
|
+
"pending",
|
|
66
|
+
"executing",
|
|
67
|
+
"approved",
|
|
68
|
+
"rejected"
|
|
69
|
+
]);
|
|
70
|
+
/**
|
|
71
|
+
* The write gate for one record. Both providers call this before persisting,
|
|
72
|
+
* so a record the other provider would refuse can never land.
|
|
73
|
+
* @param table - the seam table the record belongs to.
|
|
74
|
+
* @param record - the candidate record.
|
|
75
|
+
* @returns a human-readable issue, or null when the record is well-formed.
|
|
76
|
+
*/
|
|
77
|
+
function recordIssue(table, record) {
|
|
78
|
+
if (record === null || typeof record !== "object" || Array.isArray(record)) return "record must be a plain object";
|
|
79
|
+
const value = record;
|
|
80
|
+
if (table === "review_state") {
|
|
81
|
+
if (!isNonNegInt(value.turnsSinceMemory)) return "turnsSinceMemory must be a non-negative integer";
|
|
82
|
+
if (!isNonNegInt(value.turnsSinceSkill)) return "turnsSinceSkill must be a non-negative integer";
|
|
83
|
+
if (!isNonNegInt(value.lastTurn)) return "lastTurn must be a non-negative integer";
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (table === "curator_state") {
|
|
87
|
+
if (value.schemaVersion !== void 0 && !isNonNegInt(value.schemaVersion)) return "schemaVersion must be a non-negative integer when present";
|
|
88
|
+
if (typeof value.lastRunAt !== "number" || !Number.isFinite(value.lastRunAt) || value.lastRunAt < 0) return "lastRunAt must be a finite non-negative number";
|
|
89
|
+
if (!isNonNegInt(value.runCount)) return "runCount must be a non-negative integer";
|
|
90
|
+
if (typeof value.lastSummary !== "string") return "lastSummary must be a string";
|
|
91
|
+
if (typeof value.paused !== "boolean") return "paused must be a boolean";
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (typeof value.id !== "string") return "id must be a string";
|
|
95
|
+
if (typeof value.kind !== "string" || !PENDING_KINDS.has(value.kind)) return "kind must be memory|skill|capability";
|
|
96
|
+
if (typeof value.summary !== "string") return "summary must be a string";
|
|
97
|
+
if (!Object.prototype.hasOwnProperty.call(value, "args")) return "args key is required (may be any cloneable value)";
|
|
98
|
+
if (typeof value.createdAt !== "string") return "createdAt must be a string";
|
|
99
|
+
if (typeof value.status !== "string" || !PENDING_STATUSES.has(value.status)) return "status must be pending|executing|approved|rejected";
|
|
100
|
+
for (const field of [
|
|
101
|
+
"resolvedAt",
|
|
102
|
+
"claimedBy",
|
|
103
|
+
"claimedAt",
|
|
104
|
+
"origin",
|
|
105
|
+
"sessionId"
|
|
106
|
+
]) if (!optionalString(value[field])) return `${field} must be a string when present`;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* P2-15 (v19): the value must survive the seam's copy discipline. A payload
|
|
111
|
+
* that cannot be structured-cloned (functions, symbols, class instances with
|
|
112
|
+
* private state) would make every later read throw far away from the write.
|
|
113
|
+
* @param record - the candidate record.
|
|
114
|
+
* @returns a human-readable issue, or null when the value is cloneable.
|
|
115
|
+
*/
|
|
116
|
+
function assertCloneable(record) {
|
|
117
|
+
try {
|
|
118
|
+
structuredClone(record);
|
|
119
|
+
return null;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return `record is not structured-cloneable (${error instanceof Error ? error.message : String(error)})`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Deep copy for every boundary crossing (read AND write). @param record - the value to copy. @returns an independent deep copy. */
|
|
125
|
+
function cloneRecord(record) {
|
|
126
|
+
return structuredClone(record);
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
36
129
|
//#region lib/types/index.js
|
|
37
130
|
/**
|
|
38
131
|
* Provider seam for durable evolution state.
|
|
@@ -110,4 +203,4 @@ var EvolutionStateStorageRegistry = class extends Service {
|
|
|
110
203
|
}
|
|
111
204
|
};
|
|
112
205
|
//#endregion
|
|
113
|
-
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_TABLE, canClaimPending, canResolvePending, releasedStatus };
|
|
206
|
+
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_TABLE, UNKNOWN_FIELD_POLICY, assertCloneable, canClaimPending, canResolvePending, cloneRecord, recordIssue, releasedStatus };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -8,9 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
10
10
|
export * from './constants.ts';
|
|
11
|
+
export * from './record-contract.ts';
|
|
11
12
|
/** 0.3.17 (S3.5, D-4): 'skill_batch' removed — nothing ever created one
|
|
12
13
|
* (dead enum member); the historic value, if it ever reached disk, is read as
|
|
13
|
-
* an unknown kind by consumers rather than minted here.
|
|
14
|
+
* an unknown kind by consumers rather than minted here.
|
|
15
|
+
*
|
|
16
|
+
* 0.3.66: 'capability' is retained with NO producer — the evolution-capability
|
|
17
|
+
* adapter was removed. It is a read-compatibility member: state written by an
|
|
18
|
+
* install that used that adapter (≤0.3.65) still holds such records, and they
|
|
19
|
+
* must keep loading, listing in `/evolution pending`, and answering approve or
|
|
20
|
+
* reject. Dropping it would strand them two ways: json quarantines the row to
|
|
21
|
+
* `<file>.corrupt` and refuses the resolving write, while the domain provider
|
|
22
|
+
* validates every stored record at mount, so one such row fails the whole domain
|
|
23
|
+
* with `invalid-record`. */
|
|
14
24
|
export type PendingKind = 'memory' | 'skill' | 'capability';
|
|
15
25
|
/** 0.3.17 (S3.3, E-24): 'executing' = claimed, runner in flight — a fresh
|
|
16
26
|
* claim only takes 'pending', and resolve accepts 'pending'/'executing', so a
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record contract of the three seam tables (P2-12/14/15/16/18, v19).
|
|
3
|
+
*
|
|
4
|
+
* The contract belongs to the SEAM, not to each provider: before this module
|
|
5
|
+
* the json provider gated writes with plain field predicates while the domain
|
|
6
|
+
* provider relied on its zod schemas, so the same consumer code behaved
|
|
7
|
+
* differently per medium — `transactCuratorState` skipped validation entirely
|
|
8
|
+
* on domain, unknown fields were stripped by zod but preserved by json,
|
|
9
|
+
* `args` was shared by reference on domain, and a non-cloneable payload
|
|
10
|
+
* poisoned every later read. Both providers now call these functions.
|
|
11
|
+
*
|
|
12
|
+
* UNKNOWN_FIELD_POLICY is `preserve`: a record written by a newer version must
|
|
13
|
+
* survive a round-trip through an older provider, so unknown fields are kept
|
|
14
|
+
* (json does this naturally; the domain schemas are `.loose()`).
|
|
15
|
+
*
|
|
16
|
+
* @module @lmzhen/dsh-evolution-state-storage/src/record-contract
|
|
17
|
+
*/
|
|
18
|
+
import { CURATOR_STATE_TABLE, PENDING_TABLE, REVIEW_STATE_TABLE } from './constants.ts';
|
|
19
|
+
/** The three seam tables a record can belong to. */
|
|
20
|
+
export type SeamRecordTable = typeof REVIEW_STATE_TABLE | typeof CURATOR_STATE_TABLE | typeof PENDING_TABLE;
|
|
21
|
+
/** Unknown fields survive a provider round-trip (json preserves by
|
|
22
|
+
* construction; the domain schemas are `.loose()`). */
|
|
23
|
+
export declare const UNKNOWN_FIELD_POLICY: "preserve";
|
|
24
|
+
/**
|
|
25
|
+
* The write gate for one record. Both providers call this before persisting,
|
|
26
|
+
* so a record the other provider would refuse can never land.
|
|
27
|
+
* @param table - the seam table the record belongs to.
|
|
28
|
+
* @param record - the candidate record.
|
|
29
|
+
* @returns a human-readable issue, or null when the record is well-formed.
|
|
30
|
+
*/
|
|
31
|
+
export declare function recordIssue(table: SeamRecordTable, record: unknown): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* P2-15 (v19): the value must survive the seam's copy discipline. A payload
|
|
34
|
+
* that cannot be structured-cloned (functions, symbols, class instances with
|
|
35
|
+
* private state) would make every later read throw far away from the write.
|
|
36
|
+
* @param record - the candidate record.
|
|
37
|
+
* @returns a human-readable issue, or null when the value is cloneable.
|
|
38
|
+
*/
|
|
39
|
+
export declare function assertCloneable(record: unknown): string | null;
|
|
40
|
+
/** Deep copy for every boundary crossing (read AND write). @param record - the value to copy. @returns an independent deep copy. */
|
|
41
|
+
export declare function cloneRecord<T>(record: T): T;
|
|
42
|
+
//# sourceMappingURL=record-contract.d.ts.map
|