@modelprofile.com/flexharness 3.8.0 → 4.0.1

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.
@@ -0,0 +1,226 @@
1
+ import {
2
+ FlexHarnessStoreFormatError,
3
+ } from './errors.js';
4
+ import {
5
+ FLEX_PROJECT_MANAGEMENT_LIMITS,
6
+ FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION,
7
+ FLEX_SESSION_GENERATION_ID_MAX_BYTES,
8
+ } from './interfaces.js';
9
+ import type {
10
+ IFlexProjectManagementSnapshot,
11
+ IFlexProjectManagementTombstone,
12
+ IFlexProjectTask,
13
+ TFlexProjectManagementRecord,
14
+ } from './interfaces.js';
15
+ import { assertJsonSerializable } from './utils.json.js';
16
+
17
+ function requireRecord(value: unknown, path: string): Record<string, unknown> {
18
+ if (
19
+ !value
20
+ || typeof value !== 'object'
21
+ || Array.isArray(value)
22
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))
23
+ ) {
24
+ throw new FlexHarnessStoreFormatError(`${path} must be a plain object.`);
25
+ }
26
+ return value as Record<string, unknown>;
27
+ }
28
+
29
+ function requireOnlyKeys(
30
+ value: Record<string, unknown>,
31
+ keys: readonly string[],
32
+ path: string,
33
+ ): void {
34
+ const unsupported = Object.keys(value).find((key) => !keys.includes(key));
35
+ if (unsupported) {
36
+ throw new FlexHarnessStoreFormatError(`${path}.${unsupported} is not supported.`);
37
+ }
38
+ }
39
+
40
+ function requireBoundedString(
41
+ value: unknown,
42
+ path: string,
43
+ maxBytes: number,
44
+ nonEmpty = false,
45
+ ): asserts value is string {
46
+ if (
47
+ typeof value !== 'string'
48
+ || (nonEmpty && !value.trim())
49
+ || Buffer.byteLength(value, 'utf8') > maxBytes
50
+ ) {
51
+ throw new FlexHarnessStoreFormatError(
52
+ `${path} must be ${nonEmpty ? 'a non-empty ' : 'a '}string of at most ${maxBytes} UTF-8 bytes.`,
53
+ );
54
+ }
55
+ }
56
+
57
+ function requireTimestamp(value: unknown, path: string): asserts value is string {
58
+ if (typeof value !== 'string') {
59
+ throw new FlexHarnessStoreFormatError(`${path} must be a canonical ISO timestamp.`);
60
+ }
61
+ const timestamp = Date.parse(value);
62
+ if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== value) {
63
+ throw new FlexHarnessStoreFormatError(`${path} must be a canonical ISO timestamp.`);
64
+ }
65
+ }
66
+
67
+ function requireSessionGeneration(value: Record<string, unknown>, path: string): void {
68
+ requireBoundedString(
69
+ value.sessionGenerationId,
70
+ `${path}.sessionGenerationId`,
71
+ FLEX_SESSION_GENERATION_ID_MAX_BYTES,
72
+ true,
73
+ );
74
+ if (
75
+ !Number.isSafeInteger(value.sessionGenerationSequence)
76
+ || Number(value.sessionGenerationSequence) < 1
77
+ ) {
78
+ throw new FlexHarnessStoreFormatError(
79
+ `${path}.sessionGenerationSequence must be a positive integer.`,
80
+ );
81
+ }
82
+ }
83
+
84
+ function validateTask(value: unknown, path: string): IFlexProjectTask {
85
+ const task = requireRecord(value, path);
86
+ requireOnlyKeys(
87
+ task,
88
+ ['id', 'content', 'status', 'priority', 'createdAt', 'updatedAt'],
89
+ path,
90
+ );
91
+ requireBoundedString(task.id, `${path}.id`, FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskIdBytes, true);
92
+ requireBoundedString(
93
+ task.content,
94
+ `${path}.content`,
95
+ FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskContentBytes,
96
+ true,
97
+ );
98
+ if (!['pending', 'in_progress', 'completed', 'cancelled'].includes(String(task.status))) {
99
+ throw new FlexHarnessStoreFormatError(`${path}.status is invalid.`);
100
+ }
101
+ if (!['high', 'medium', 'low'].includes(String(task.priority))) {
102
+ throw new FlexHarnessStoreFormatError(`${path}.priority is invalid.`);
103
+ }
104
+ requireTimestamp(task.createdAt, `${path}.createdAt`);
105
+ requireTimestamp(task.updatedAt, `${path}.updatedAt`);
106
+ if (task.updatedAt < task.createdAt) {
107
+ throw new FlexHarnessStoreFormatError(`${path}.updatedAt cannot precede createdAt.`);
108
+ }
109
+ return task as unknown as IFlexProjectTask;
110
+ }
111
+
112
+ export function createEmptyFlexProjectManagementSnapshot(
113
+ sessionGenerationId: string,
114
+ sessionGenerationSequence: number,
115
+ ): IFlexProjectManagementSnapshot {
116
+ const snapshot: IFlexProjectManagementSnapshot = {
117
+ schemaVersion: FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION,
118
+ revision: 0,
119
+ sessionGenerationId,
120
+ sessionGenerationSequence,
121
+ scratchpad: '',
122
+ tasks: [],
123
+ };
124
+ assertFlexProjectManagementSnapshot(snapshot);
125
+ return snapshot;
126
+ }
127
+
128
+ export function assertFlexProjectManagementSnapshot(
129
+ value: unknown,
130
+ ): asserts value is IFlexProjectManagementSnapshot {
131
+ assertJsonSerializable(value, '$snapshot');
132
+ const snapshot = requireRecord(value, '$snapshot');
133
+ requireOnlyKeys(
134
+ snapshot,
135
+ [
136
+ 'schemaVersion',
137
+ 'revision',
138
+ 'sessionGenerationId',
139
+ 'sessionGenerationSequence',
140
+ 'goal',
141
+ 'scratchpad',
142
+ 'tasks',
143
+ ],
144
+ '$snapshot',
145
+ );
146
+ if (snapshot.schemaVersion !== FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION) {
147
+ throw new FlexHarnessStoreFormatError(
148
+ `Project management snapshot schemaVersion must be ${FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION}.`,
149
+ );
150
+ }
151
+ if (!Number.isSafeInteger(snapshot.revision) || Number(snapshot.revision) < 0) {
152
+ throw new FlexHarnessStoreFormatError('Project management snapshot revision must be a non-negative integer.');
153
+ }
154
+ requireSessionGeneration(snapshot, '$snapshot');
155
+ if (snapshot.goal !== undefined) {
156
+ requireBoundedString(
157
+ snapshot.goal,
158
+ '$snapshot.goal',
159
+ FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes,
160
+ true,
161
+ );
162
+ }
163
+ requireBoundedString(
164
+ snapshot.scratchpad,
165
+ '$snapshot.scratchpad',
166
+ FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
167
+ );
168
+ if (!Array.isArray(snapshot.tasks)) {
169
+ throw new FlexHarnessStoreFormatError('$snapshot.tasks must be an array.');
170
+ }
171
+ if (snapshot.tasks.length > FLEX_PROJECT_MANAGEMENT_LIMITS.maxTasks) {
172
+ throw new FlexHarnessStoreFormatError('$snapshot.tasks exceeds its task-count limit.');
173
+ }
174
+ const taskIds = new Set<string>();
175
+ for (let index = 0; index < snapshot.tasks.length; index++) {
176
+ const task = validateTask(snapshot.tasks[index], `$snapshot.tasks[${index}]`);
177
+ if (taskIds.has(task.id)) {
178
+ throw new FlexHarnessStoreFormatError(`$snapshot.tasks contains duplicate task "${task.id}".`);
179
+ }
180
+ taskIds.add(task.id);
181
+ }
182
+ if (Buffer.byteLength(JSON.stringify(snapshot), 'utf8') > FLEX_PROJECT_MANAGEMENT_LIMITS.maxSnapshotBytes) {
183
+ throw new FlexHarnessStoreFormatError('Project management snapshot exceeds its serialized byte limit.');
184
+ }
185
+ }
186
+
187
+ export function assertFlexProjectManagementTombstone(
188
+ value: unknown,
189
+ ): asserts value is IFlexProjectManagementTombstone {
190
+ assertJsonSerializable(value, '$tombstone');
191
+ const tombstone = requireRecord(value, '$tombstone');
192
+ requireOnlyKeys(
193
+ tombstone,
194
+ [
195
+ 'schemaVersion',
196
+ 'revision',
197
+ 'sessionGenerationId',
198
+ 'sessionGenerationSequence',
199
+ 'deletedAt',
200
+ ],
201
+ '$tombstone',
202
+ );
203
+ if (tombstone.schemaVersion !== FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION) {
204
+ throw new FlexHarnessStoreFormatError(
205
+ `Project management tombstone schemaVersion must be ${FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION}.`,
206
+ );
207
+ }
208
+ if (!Number.isSafeInteger(tombstone.revision) || Number(tombstone.revision) < 1) {
209
+ throw new FlexHarnessStoreFormatError(
210
+ 'Project management tombstone revision must be a positive integer.',
211
+ );
212
+ }
213
+ requireSessionGeneration(tombstone, '$tombstone');
214
+ requireTimestamp(tombstone.deletedAt, '$tombstone.deletedAt');
215
+ }
216
+
217
+ export function assertFlexProjectManagementRecord(
218
+ value: unknown,
219
+ ): asserts value is TFlexProjectManagementRecord {
220
+ const record = requireRecord(value, '$record');
221
+ if (Object.prototype.hasOwnProperty.call(record, 'deletedAt')) {
222
+ assertFlexProjectManagementTombstone(value);
223
+ } else {
224
+ assertFlexProjectManagementSnapshot(value);
225
+ }
226
+ }