@devflow-tools/database 0.16.18 → 0.16.20
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/CHANGELOG.md +27 -0
- package/__tests__/database.host-actions.test.ts +405 -0
- package/__tests__/database.retrieval-sessions.test.ts +79 -0
- package/dist/database.d.ts +31 -0
- package/dist/database.js +481 -0
- package/dist/host-actions.d.ts +47 -0
- package/dist/host-actions.js +152 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +9 -1
- package/dist/retrieval-sessions.d.ts +74 -0
- package/dist/retrieval-sessions.js +117 -0
- package/package.json +1 -1
- package/src/database.ts +589 -0
- package/src/host-actions.ts +211 -0
- package/src/index.ts +25 -0
- package/src/retrieval-sessions.ts +196 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isHostActionState = isHostActionState;
|
|
4
|
+
exports.mapHostActionRow = mapHostActionRow;
|
|
5
|
+
exports.serializeHostActionReport = serializeHostActionReport;
|
|
6
|
+
exports.hostActionReportsEqual = hostActionReportsEqual;
|
|
7
|
+
const HOST_ACTION_STATES = new Set([
|
|
8
|
+
'waiting',
|
|
9
|
+
'running',
|
|
10
|
+
'reported',
|
|
11
|
+
'verified',
|
|
12
|
+
'failed',
|
|
13
|
+
'cancelled',
|
|
14
|
+
'degraded',
|
|
15
|
+
]);
|
|
16
|
+
function isHostActionState(value) {
|
|
17
|
+
return typeof value === 'string' && HOST_ACTION_STATES.has(value);
|
|
18
|
+
}
|
|
19
|
+
function mapHostActionRow(row) {
|
|
20
|
+
if (!isHostActionState(row.state)) {
|
|
21
|
+
throw new Error(`Invalid persisted host action state: ${String(row.state)}`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
actionId: asRequiredString(row.action_id, 'action_id'),
|
|
25
|
+
runId: asRequiredString(row.run_id, 'run_id'),
|
|
26
|
+
engineRunId: asRequiredString(row.engine_run_id, 'engine_run_id'),
|
|
27
|
+
stepId: asRequiredString(row.step_id, 'step_id'),
|
|
28
|
+
projectRoot: asRequiredString(row.project_root, 'project_root'),
|
|
29
|
+
sessionId: asOptionalString(row.session_id),
|
|
30
|
+
executionId: asOptionalString(row.execution_id),
|
|
31
|
+
contextReceipt: asOptionalString(row.context_receipt),
|
|
32
|
+
state: row.state,
|
|
33
|
+
report: parseReport(row.report_json),
|
|
34
|
+
evidenceHash: asOptionalString(row.evidence_hash),
|
|
35
|
+
createdAt: asFiniteNumber(row.created_at, 'created_at'),
|
|
36
|
+
updatedAt: asFiniteNumber(row.updated_at, 'updated_at'),
|
|
37
|
+
finishedAt: asOptionalFiniteNumber(row.finished_at),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function serializeHostActionReport(report) {
|
|
41
|
+
if (!isPlainObject(report))
|
|
42
|
+
throw new Error('Host action report must be a plain object');
|
|
43
|
+
return JSON.stringify(canonicalizeJson(report, '$', new WeakSet()));
|
|
44
|
+
}
|
|
45
|
+
function hostActionReportsEqual(left, right) {
|
|
46
|
+
return serializeHostActionReport(left) === serializeHostActionReport(right);
|
|
47
|
+
}
|
|
48
|
+
function parseReport(value) {
|
|
49
|
+
if (typeof value !== 'string') {
|
|
50
|
+
throw new Error('Invalid persisted host action report_json: expected JSON text');
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(value);
|
|
54
|
+
if (!isPlainObject(parsed)) {
|
|
55
|
+
throw new Error('expected a JSON object');
|
|
56
|
+
}
|
|
57
|
+
serializeHostActionReport(parsed);
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
62
|
+
throw new Error(`Invalid persisted host action report_json: ${detail}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function canonicalizeJson(value, path, ancestors) {
|
|
66
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
67
|
+
return value;
|
|
68
|
+
if (typeof value === 'number') {
|
|
69
|
+
if (!Number.isFinite(value) || Object.is(value, -0)) {
|
|
70
|
+
throw nonJsonValueError(path);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
if (typeof value !== 'object')
|
|
75
|
+
throw nonJsonValueError(path);
|
|
76
|
+
if (ancestors.has(value))
|
|
77
|
+
throw new Error(`Host action report contains a cycle at ${path}`);
|
|
78
|
+
ancestors.add(value);
|
|
79
|
+
try {
|
|
80
|
+
if (Array.isArray(value))
|
|
81
|
+
return canonicalizeArray(value, path, ancestors);
|
|
82
|
+
if (!isPlainObject(value))
|
|
83
|
+
throw nonJsonValueError(path);
|
|
84
|
+
const result = Object.create(null);
|
|
85
|
+
const keys = Reflect.ownKeys(value);
|
|
86
|
+
for (const key of keys) {
|
|
87
|
+
if (typeof key !== 'string')
|
|
88
|
+
throw nonJsonValueError(`${path}[symbol]`);
|
|
89
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
90
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) {
|
|
91
|
+
throw nonJsonValueError(`${path}.${key}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const key of keys.slice().sort(comparePropertyKeys)) {
|
|
95
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
96
|
+
result[key] = canonicalizeJson(descriptor.value, `${path}.${key}`, ancestors);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
ancestors.delete(value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function canonicalizeArray(value, path, ancestors) {
|
|
105
|
+
if (Object.getPrototypeOf(value) !== Array.prototype)
|
|
106
|
+
throw nonJsonValueError(path);
|
|
107
|
+
const keys = Reflect.ownKeys(value);
|
|
108
|
+
const expectedKeys = new Set(['length', ...Array.from({ length: value.length }, (_, index) => String(index))]);
|
|
109
|
+
if (keys.some(key => typeof key !== 'string' || !expectedKeys.has(key))) {
|
|
110
|
+
throw nonJsonValueError(path);
|
|
111
|
+
}
|
|
112
|
+
return Array.from({ length: value.length }, (_, index) => {
|
|
113
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
114
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) {
|
|
115
|
+
throw nonJsonValueError(`${path}[${index}]`);
|
|
116
|
+
}
|
|
117
|
+
return canonicalizeJson(descriptor.value, `${path}[${index}]`, ancestors);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function comparePropertyKeys(left, right) {
|
|
121
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
122
|
+
}
|
|
123
|
+
function nonJsonValueError(path) {
|
|
124
|
+
return new Error(`Host action report contains a non-JSON value at ${path}`);
|
|
125
|
+
}
|
|
126
|
+
function isPlainObject(value) {
|
|
127
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
128
|
+
return false;
|
|
129
|
+
const prototype = Object.getPrototypeOf(value);
|
|
130
|
+
return prototype === Object.prototype || prototype === null;
|
|
131
|
+
}
|
|
132
|
+
function asRequiredString(value, column) {
|
|
133
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
134
|
+
throw new Error(`Invalid persisted host action ${column}`);
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
function asOptionalString(value) {
|
|
139
|
+
return typeof value === 'string' ? value : undefined;
|
|
140
|
+
}
|
|
141
|
+
function asFiniteNumber(value, column) {
|
|
142
|
+
const number = Number(value);
|
|
143
|
+
if (!Number.isFinite(number))
|
|
144
|
+
throw new Error(`Invalid persisted host action ${column}`);
|
|
145
|
+
return number;
|
|
146
|
+
}
|
|
147
|
+
function asOptionalFiniteNumber(value) {
|
|
148
|
+
if (value === null || value === undefined)
|
|
149
|
+
return undefined;
|
|
150
|
+
const number = Number(value);
|
|
151
|
+
return Number.isFinite(number) ? number : undefined;
|
|
152
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { DevFlowDatabase, getGlobalDevFlowDbPath, openGlobalDevFlowDatabase, } from './database';
|
|
2
2
|
export type { BenchmarkReportMetaRecord, BenchmarkReportRecord, FeedbackRecord, GovernanceAuditRecord, GovernanceRuleRecord, HookFallbackRecord, HookReceiptRecord, ContextReceiptRecord, ContextSelectionEventRecord, MemoryDistillCheckpointRecord, MemoryTurnRecord, MemoryTurnStatus, TelemetryFailureRecord, } from './database';
|
|
3
3
|
export type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, SessionClosureRecord, SessionClosureState, WorkError, WorkItemRecord, WorkKind, WorkQueueHealth, WorkState, } from './work-queue';
|
|
4
|
+
export { hostActionReportsEqual, isHostActionState, mapHostActionRow, serializeHostActionReport, } from './host-actions';
|
|
5
|
+
export { RETRIEVAL_MAX_CYCLES, isRetrievalSessionState } from './retrieval-sessions';
|
|
6
|
+
export type { AppendRetrievalCycleInput, CreateRetrievalSessionInput, RetrievalCycleRecord, RetrievalGapKind, RetrievalGapRecord, RetrievalSessionRecord, RetrievalSessionState, } from './retrieval-sessions';
|
|
7
|
+
export type { FailHostActionInput, HostActionRecord, HostActionState, ReportHostActionInput, RequestHostActionInput, StartHostActionInput, VerifyHostActionInput, } from './host-actions';
|
|
4
8
|
export { mapSessionObligationRow, normalizeTurnId, } from './obligation-ledger';
|
|
5
9
|
export type { SessionObligationKind, SessionObligationRecord, SessionObligationState, } from './obligation-ledger';
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeTurnId = exports.mapSessionObligationRow = exports.openGlobalDevFlowDatabase = exports.getGlobalDevFlowDbPath = exports.DevFlowDatabase = void 0;
|
|
3
|
+
exports.normalizeTurnId = exports.mapSessionObligationRow = exports.isRetrievalSessionState = exports.RETRIEVAL_MAX_CYCLES = exports.serializeHostActionReport = exports.mapHostActionRow = exports.isHostActionState = exports.hostActionReportsEqual = exports.openGlobalDevFlowDatabase = exports.getGlobalDevFlowDbPath = exports.DevFlowDatabase = void 0;
|
|
4
4
|
var database_1 = require("./database");
|
|
5
5
|
Object.defineProperty(exports, "DevFlowDatabase", { enumerable: true, get: function () { return database_1.DevFlowDatabase; } });
|
|
6
6
|
Object.defineProperty(exports, "getGlobalDevFlowDbPath", { enumerable: true, get: function () { return database_1.getGlobalDevFlowDbPath; } });
|
|
7
7
|
Object.defineProperty(exports, "openGlobalDevFlowDatabase", { enumerable: true, get: function () { return database_1.openGlobalDevFlowDatabase; } });
|
|
8
|
+
var host_actions_1 = require("./host-actions");
|
|
9
|
+
Object.defineProperty(exports, "hostActionReportsEqual", { enumerable: true, get: function () { return host_actions_1.hostActionReportsEqual; } });
|
|
10
|
+
Object.defineProperty(exports, "isHostActionState", { enumerable: true, get: function () { return host_actions_1.isHostActionState; } });
|
|
11
|
+
Object.defineProperty(exports, "mapHostActionRow", { enumerable: true, get: function () { return host_actions_1.mapHostActionRow; } });
|
|
12
|
+
Object.defineProperty(exports, "serializeHostActionReport", { enumerable: true, get: function () { return host_actions_1.serializeHostActionReport; } });
|
|
13
|
+
var retrieval_sessions_1 = require("./retrieval-sessions");
|
|
14
|
+
Object.defineProperty(exports, "RETRIEVAL_MAX_CYCLES", { enumerable: true, get: function () { return retrieval_sessions_1.RETRIEVAL_MAX_CYCLES; } });
|
|
15
|
+
Object.defineProperty(exports, "isRetrievalSessionState", { enumerable: true, get: function () { return retrieval_sessions_1.isRetrievalSessionState; } });
|
|
8
16
|
var obligation_ledger_1 = require("./obligation-ledger");
|
|
9
17
|
Object.defineProperty(exports, "mapSessionObligationRow", { enumerable: true, get: function () { return obligation_ledger_1.mapSessionObligationRow; } });
|
|
10
18
|
Object.defineProperty(exports, "normalizeTurnId", { enumerable: true, get: function () { return obligation_ledger_1.normalizeTurnId; } });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export declare const RETRIEVAL_MAX_CYCLES: 3;
|
|
2
|
+
export type RetrievalSessionState = 'open' | 'satisfied' | 'exhausted' | 'expired';
|
|
3
|
+
export type RetrievalGapKind = 'target' | 'caller' | 'route' | 'resource' | 'test' | 'configuration' | 'symbol_ambiguity' | 'memory_applicability' | 'knowledge_version' | 'runtime_evidence' | 'unavailable_channel';
|
|
4
|
+
export interface RetrievalGapRecord {
|
|
5
|
+
kind: RetrievalGapKind;
|
|
6
|
+
target?: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
evidence: string[];
|
|
9
|
+
resolver: 'codegraph' | 'memory' | 'knowledge' | 'analyzer' | 'none';
|
|
10
|
+
priority: number;
|
|
11
|
+
}
|
|
12
|
+
export interface RetrievalCycleRecord {
|
|
13
|
+
retrievalSessionId: string;
|
|
14
|
+
cycle: number;
|
|
15
|
+
gaps: RetrievalGapRecord[];
|
|
16
|
+
selectedIds: string[];
|
|
17
|
+
rejectedIds: string[];
|
|
18
|
+
tokenCost: number;
|
|
19
|
+
remainingTokenBudget: number;
|
|
20
|
+
quality: Record<string, unknown>;
|
|
21
|
+
receipt: string;
|
|
22
|
+
evidenceHash: string;
|
|
23
|
+
createdAt: number;
|
|
24
|
+
}
|
|
25
|
+
export interface RetrievalSessionRecord {
|
|
26
|
+
id: string;
|
|
27
|
+
requestId: string;
|
|
28
|
+
projectRoot: string;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
executionId?: string;
|
|
31
|
+
query: string;
|
|
32
|
+
intent: string;
|
|
33
|
+
state: RetrievalSessionState;
|
|
34
|
+
cycle: number;
|
|
35
|
+
maxCycles: typeof RETRIEVAL_MAX_CYCLES;
|
|
36
|
+
initialTokenBudget: number;
|
|
37
|
+
remainingTokenBudget: number;
|
|
38
|
+
baselineReceipt: string;
|
|
39
|
+
finalReceipt?: string;
|
|
40
|
+
createdAt: number;
|
|
41
|
+
updatedAt: number;
|
|
42
|
+
expiresAt: number;
|
|
43
|
+
}
|
|
44
|
+
export interface CreateRetrievalSessionInput {
|
|
45
|
+
id?: string;
|
|
46
|
+
requestId: string;
|
|
47
|
+
projectRoot: string;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
executionId?: string;
|
|
50
|
+
query: string;
|
|
51
|
+
intent: string;
|
|
52
|
+
tokenBudget: number;
|
|
53
|
+
baselineReceipt: string;
|
|
54
|
+
expiresAt: number;
|
|
55
|
+
}
|
|
56
|
+
export interface AppendRetrievalCycleInput {
|
|
57
|
+
retrievalSessionId: string;
|
|
58
|
+
projectRoot: string;
|
|
59
|
+
sessionId: string;
|
|
60
|
+
baselineReceipt: string;
|
|
61
|
+
cycle: number;
|
|
62
|
+
gaps: RetrievalGapRecord[];
|
|
63
|
+
selectedIds: string[];
|
|
64
|
+
rejectedIds: string[];
|
|
65
|
+
tokenCost: number;
|
|
66
|
+
remainingTokenBudget: number;
|
|
67
|
+
quality: Record<string, unknown>;
|
|
68
|
+
receipt: string;
|
|
69
|
+
evidenceHash: string;
|
|
70
|
+
}
|
|
71
|
+
export declare function mapRetrievalSessionRow(row: Record<string, unknown>): RetrievalSessionRecord;
|
|
72
|
+
export declare function mapRetrievalCycleRow(row: Record<string, unknown>): RetrievalCycleRecord;
|
|
73
|
+
export declare function serializeRetrievalJson(value: unknown): string;
|
|
74
|
+
export declare function isRetrievalSessionState(value: unknown): value is RetrievalSessionState;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RETRIEVAL_MAX_CYCLES = void 0;
|
|
4
|
+
exports.mapRetrievalSessionRow = mapRetrievalSessionRow;
|
|
5
|
+
exports.mapRetrievalCycleRow = mapRetrievalCycleRow;
|
|
6
|
+
exports.serializeRetrievalJson = serializeRetrievalJson;
|
|
7
|
+
exports.isRetrievalSessionState = isRetrievalSessionState;
|
|
8
|
+
exports.RETRIEVAL_MAX_CYCLES = 3;
|
|
9
|
+
function mapRetrievalSessionRow(row) {
|
|
10
|
+
const state = requiredString(row.state, 'state');
|
|
11
|
+
if (!isRetrievalSessionState(state)) {
|
|
12
|
+
throw new Error(`Invalid persisted retrieval session state: ${state}`);
|
|
13
|
+
}
|
|
14
|
+
const maxCycles = finiteInteger(row.max_cycles, 'max_cycles');
|
|
15
|
+
if (maxCycles !== exports.RETRIEVAL_MAX_CYCLES) {
|
|
16
|
+
throw new Error(`Invalid persisted retrieval max_cycles: ${maxCycles}`);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
id: requiredString(row.id, 'id'),
|
|
20
|
+
requestId: requiredString(row.request_id, 'request_id'),
|
|
21
|
+
projectRoot: requiredString(row.project_root, 'project_root'),
|
|
22
|
+
sessionId: requiredString(row.session_id, 'session_id'),
|
|
23
|
+
executionId: optionalString(row.execution_id),
|
|
24
|
+
query: requiredString(row.query, 'query'),
|
|
25
|
+
intent: requiredString(row.intent, 'intent'),
|
|
26
|
+
state,
|
|
27
|
+
cycle: finiteInteger(row.cycle, 'cycle'),
|
|
28
|
+
maxCycles: exports.RETRIEVAL_MAX_CYCLES,
|
|
29
|
+
initialTokenBudget: finiteInteger(row.initial_token_budget, 'initial_token_budget'),
|
|
30
|
+
remainingTokenBudget: finiteInteger(row.remaining_token_budget, 'remaining_token_budget'),
|
|
31
|
+
baselineReceipt: requiredString(row.baseline_receipt, 'baseline_receipt'),
|
|
32
|
+
finalReceipt: optionalString(row.final_receipt),
|
|
33
|
+
createdAt: finiteInteger(row.created_at, 'created_at'),
|
|
34
|
+
updatedAt: finiteInteger(row.updated_at, 'updated_at'),
|
|
35
|
+
expiresAt: finiteInteger(row.expires_at, 'expires_at'),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function mapRetrievalCycleRow(row) {
|
|
39
|
+
return {
|
|
40
|
+
retrievalSessionId: requiredString(row.retrieval_session_id, 'retrieval_session_id'),
|
|
41
|
+
cycle: finiteInteger(row.cycle, 'cycle'),
|
|
42
|
+
gaps: parseJsonArray(row.gaps_json, 'gaps_json'),
|
|
43
|
+
selectedIds: parseStringArray(row.selected_ids, 'selected_ids'),
|
|
44
|
+
rejectedIds: parseStringArray(row.rejected_ids, 'rejected_ids'),
|
|
45
|
+
tokenCost: finiteInteger(row.token_cost, 'token_cost'),
|
|
46
|
+
remainingTokenBudget: finiteInteger(row.remaining_token_budget, 'remaining_token_budget'),
|
|
47
|
+
quality: parseJsonObject(row.quality_json, 'quality_json'),
|
|
48
|
+
receipt: requiredString(row.receipt, 'receipt'),
|
|
49
|
+
evidenceHash: requiredString(row.evidence_hash, 'evidence_hash'),
|
|
50
|
+
createdAt: finiteInteger(row.created_at, 'created_at'),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function serializeRetrievalJson(value) {
|
|
54
|
+
return JSON.stringify(canonicalize(value));
|
|
55
|
+
}
|
|
56
|
+
function isRetrievalSessionState(value) {
|
|
57
|
+
return value === 'open' || value === 'satisfied' || value === 'exhausted' || value === 'expired';
|
|
58
|
+
}
|
|
59
|
+
function canonicalize(value) {
|
|
60
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
61
|
+
return value;
|
|
62
|
+
if (typeof value === 'number') {
|
|
63
|
+
if (!Number.isFinite(value) || Object.is(value, -0))
|
|
64
|
+
throw new Error('Retrieval evidence must be lossless JSON');
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value))
|
|
68
|
+
return value.map(canonicalize);
|
|
69
|
+
if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
70
|
+
throw new Error('Retrieval evidence must be plain JSON');
|
|
71
|
+
}
|
|
72
|
+
const record = value;
|
|
73
|
+
return Object.fromEntries(Object.keys(record).sort().map(key => [key, canonicalize(record[key])]));
|
|
74
|
+
}
|
|
75
|
+
function parseJson(value, field) {
|
|
76
|
+
if (typeof value !== 'string')
|
|
77
|
+
throw new Error(`Invalid persisted ${field}: expected JSON text`);
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(value);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
throw new Error(`Invalid persisted ${field}: malformed JSON`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function parseJsonArray(value, field) {
|
|
86
|
+
const parsed = parseJson(value, field);
|
|
87
|
+
if (!Array.isArray(parsed))
|
|
88
|
+
throw new Error(`Invalid persisted ${field}: expected array`);
|
|
89
|
+
return parsed;
|
|
90
|
+
}
|
|
91
|
+
function parseStringArray(value, field) {
|
|
92
|
+
const parsed = parseJsonArray(value, field);
|
|
93
|
+
if (parsed.some(item => typeof item !== 'string'))
|
|
94
|
+
throw new Error(`Invalid persisted ${field}: expected strings`);
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
function parseJsonObject(value, field) {
|
|
98
|
+
const parsed = parseJson(value, field);
|
|
99
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
100
|
+
throw new Error(`Invalid persisted ${field}: expected object`);
|
|
101
|
+
}
|
|
102
|
+
return parsed;
|
|
103
|
+
}
|
|
104
|
+
function requiredString(value, field) {
|
|
105
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
106
|
+
throw new Error(`Invalid persisted ${field}`);
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function optionalString(value) {
|
|
110
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
111
|
+
}
|
|
112
|
+
function finiteInteger(value, field) {
|
|
113
|
+
const result = Number(value);
|
|
114
|
+
if (!Number.isSafeInteger(result) || result < 0)
|
|
115
|
+
throw new Error(`Invalid persisted ${field}`);
|
|
116
|
+
return result;
|
|
117
|
+
}
|