@evo-dev/core 0.0.1-alpha.1 → 0.0.1-alpha.11

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.
Files changed (66) hide show
  1. package/assets/skills/coding/knowledge-distillation/SKILL.md +117 -114
  2. package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
  3. package/assets/team/agents/code-reviewer.md +48 -0
  4. package/assets/team/agents/docs-maintainer.md +51 -0
  5. package/assets/team/agents/implementation-engineer.md +51 -0
  6. package/assets/team/agents/product-scope-analyst.md +58 -0
  7. package/assets/team/agents/release-engineer.md +55 -0
  8. package/assets/team/agents/security-boundary-reviewer.md +50 -0
  9. package/assets/team/agents/solution-architect.md +51 -0
  10. package/assets/team/agents/verification-engineer.md +51 -0
  11. package/assets/team/team.md +102 -0
  12. package/dist/config/index.js +925 -97
  13. package/dist/index.js +13107 -5618
  14. package/package.json +5 -1
  15. package/src/agents/index.ts +56 -264
  16. package/src/code-agent-traces/index.ts +520 -0
  17. package/src/config/index.ts +5 -0
  18. package/src/config/paths.ts +1 -1
  19. package/src/config/settings.ts +149 -0
  20. package/src/config/store.ts +2 -0
  21. package/src/daemon/index.ts +99 -50
  22. package/src/evolution/candidates/index.ts +564 -0
  23. package/src/evolution/control/index.ts +20 -0
  24. package/src/evolution/evidence/analysis.ts +533 -0
  25. package/src/evolution/evidence/index.ts +3 -0
  26. package/src/evolution/evidence/session-memory/analysis.ts +281 -0
  27. package/src/evolution/evidence/session-memory/constants.ts +9 -0
  28. package/src/evolution/evidence/session-memory/index.ts +7 -0
  29. package/src/evolution/evidence/session-memory/paths.ts +29 -0
  30. package/src/evolution/evidence/session-memory/policy.ts +39 -0
  31. package/src/evolution/evidence/session-memory/segment.ts +202 -0
  32. package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
  33. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  34. package/src/evolution/evidence/session-memory/storage.ts +379 -0
  35. package/src/evolution/evidence/session-memory/types.ts +221 -0
  36. package/src/evolution/evidence/session-memory/updater.ts +191 -0
  37. package/src/evolution/formatters.ts +169 -0
  38. package/src/evolution/index.ts +16 -2356
  39. package/src/evolution/knowledge/index.ts +5427 -0
  40. package/src/evolution/paths.ts +44 -0
  41. package/src/evolution/processor/distillation.ts +518 -0
  42. package/src/evolution/processor/index.ts +3 -0
  43. package/src/evolution/processor/process.ts +528 -0
  44. package/src/{learning → evolution/review}/index.ts +10 -14
  45. package/src/evolution/schema.ts +568 -0
  46. package/src/evolution/shared.ts +758 -0
  47. package/src/evolution/triggers/classification.ts +102 -0
  48. package/src/evolution/triggers/index.ts +295 -0
  49. package/src/hooks/index.ts +438 -179
  50. package/src/index.ts +12 -3
  51. package/src/projects/index.ts +453 -0
  52. package/src/runtime-logs/index.ts +490 -24
  53. package/src/team/index.ts +1429 -185
  54. package/src/team/mcp.ts +9 -5
  55. package/src/team/prompts.ts +141 -0
  56. package/src/utils/errors.ts +13 -0
  57. package/src/utils/fs.ts +40 -0
  58. package/src/utils/hash.ts +9 -0
  59. package/src/utils/ids.ts +12 -0
  60. package/src/utils/index.ts +7 -0
  61. package/src/utils/parsing.ts +11 -0
  62. package/src/utils/text.ts +18 -0
  63. package/src/utils/time.ts +5 -0
  64. package/src/workflow/index.ts +3 -21
  65. package/src/project/index.ts +0 -507
  66. package/src/task/index.ts +0 -840
package/src/index.ts CHANGED
@@ -1,18 +1,27 @@
1
1
  export * from "./agents/index.ts";
2
2
  export * from "./assets/index.ts";
3
+ export * from "./code-agent-traces/index.ts";
3
4
  export * from "./config/index.ts";
4
5
  export * from "./daemon/index.ts";
5
6
  export * from "./evolution/index.ts";
7
+ export * as evolutionCandidates from "./evolution/candidates/index.ts";
8
+ export * as evolutionControl from "./evolution/control/index.ts";
9
+ export * as evolutionEvidence from "./evolution/evidence/index.ts";
10
+ export * from "./evolution/evidence/session-memory/index.ts";
11
+ export * from "./evolution/knowledge/index.ts";
12
+ export * as evolutionKnowledge from "./evolution/knowledge/index.ts";
13
+ export * as evolutionProcessor from "./evolution/processor/index.ts";
14
+ export * from "./evolution/review/index.ts";
15
+ export * as evolutionReview from "./evolution/review/index.ts";
16
+ export * as evolutionTriggers from "./evolution/triggers/index.ts";
6
17
  export * from "./hooks/index.ts";
7
- export * from "./learning/index.ts";
8
18
  export * from "./observability/index.ts";
9
19
  export * from "./pack/index.ts";
10
20
  export * from "./plugins/index.ts";
11
- export * from "./project/index.ts";
12
21
  export * from "./protected-zones/index.ts";
22
+ export * from "./projects/index.ts";
13
23
  export * from "./runtime-logs/index.ts";
14
24
  export * from "./sync/index.ts";
15
- export * from "./task/index.ts";
16
25
  export * from "./team/index.ts";
17
26
  export * from "./team/mcp.ts";
18
27
  export * from "./workflow/index.ts";
@@ -0,0 +1,453 @@
1
+ import { lstat, readFile, readdir, realpath, stat } from "node:fs/promises";
2
+ import { basename, dirname, isAbsolute, join, parse } from "node:path";
3
+ import { resolveEvoDevPaths } from "../config/paths.ts";
4
+ import { resolveProjectLogKey } from "../runtime-logs/index.ts";
5
+ import {
6
+ isFileExistsError,
7
+ isNotFoundError,
8
+ normalizeTimestamp,
9
+ sanitizeStorageId,
10
+ sha256Short,
11
+ writeJsonFile,
12
+ } from "../utils/index.ts";
13
+
14
+ export type ProjectWorkspaceKind = "git" | "directory";
15
+ export type ProjectDiscoveryTarget = "claude" | "codex";
16
+
17
+ export interface ResolvedProjectWorkspace {
18
+ projectKey: string;
19
+ displayName: string;
20
+ workspaceRoot: string;
21
+ workspaceKind: ProjectWorkspaceKind;
22
+ cwd: string;
23
+ }
24
+
25
+ export interface DiscoveredProjectRecordV1 {
26
+ schemaVersion: 1;
27
+ kind: "discovered-project";
28
+ projectKey: string;
29
+ displayName: string;
30
+ workspaceRoot: string;
31
+ lastCwd: string;
32
+ workspaceKind: ProjectWorkspaceKind;
33
+ sourceTargets: ProjectDiscoveryTarget[];
34
+ lastSessionKey: string | null;
35
+ firstSeenAt: string;
36
+ lastSeenAt: string;
37
+ localOnly: true;
38
+ sourceContentStored: false;
39
+ }
40
+
41
+ export interface RegisteredProjectRecordV1 {
42
+ schemaVersion: 1;
43
+ kind: "registered-project";
44
+ projectKey: string;
45
+ displayName: string;
46
+ workspaceRoot: string;
47
+ workspaceKind: ProjectWorkspaceKind;
48
+ registeredAt: string;
49
+ lastSeenAt: string;
50
+ localOnly: true;
51
+ sourceContentStored: false;
52
+ }
53
+
54
+ export class ProjectRegistrationError extends Error {
55
+ readonly code: "invalid" | "not-found" | "conflict";
56
+
57
+ constructor(code: "invalid" | "not-found" | "conflict", message: string) {
58
+ super(message);
59
+ this.name = "ProjectRegistrationError";
60
+ this.code = code;
61
+ }
62
+ }
63
+
64
+ export function resolveProjectRegistryPaths(homeDir: string): {
65
+ discoveredDir: string;
66
+ registeredDir: string;
67
+ discoveredPath: (projectKey: string) => string;
68
+ registeredPath: (projectKey: string) => string;
69
+ } {
70
+ const paths = resolveEvoDevPaths(homeDir);
71
+ const discoveredDir = join(paths.stateDir, "projects", "discovered");
72
+ const registeredDir = join(paths.rootDir, "projects");
73
+ return {
74
+ discoveredDir,
75
+ registeredDir,
76
+ discoveredPath: (projectKey) => join(discoveredDir, `${projectKey}.json`),
77
+ registeredPath: (projectKey) => join(registeredDir, projectKey, "workspace.json"),
78
+ };
79
+ }
80
+
81
+ export async function resolveProjectWorkspaceFromCwd(input: {
82
+ homeDir: string;
83
+ cwd: string;
84
+ }): Promise<ResolvedProjectWorkspace | null> {
85
+ if (!isAbsolute(input.cwd)) return null;
86
+
87
+ let cwd: string;
88
+ try {
89
+ const info = await stat(input.cwd);
90
+ if (!info.isDirectory()) return null;
91
+ cwd = await realpath(input.cwd);
92
+ } catch (error) {
93
+ if (isNotFoundError(error)) return null;
94
+ throw error;
95
+ }
96
+
97
+ const gitRoot = await findNearestGitRoot(cwd);
98
+ const workspaceRoot = gitRoot ?? cwd;
99
+ return {
100
+ projectKey: sanitizeStorageId(resolveProjectLogKey(input.homeDir, workspaceRoot), "project"),
101
+ displayName: basename(workspaceRoot) || parse(workspaceRoot).root,
102
+ workspaceRoot,
103
+ workspaceKind: gitRoot === null ? "directory" : "git",
104
+ cwd,
105
+ };
106
+ }
107
+
108
+ export async function recordDiscoveredProject(input: {
109
+ homeDir: string;
110
+ cwd: string;
111
+ target: ProjectDiscoveryTarget;
112
+ sessionKey?: string | null;
113
+ now?: Date | string;
114
+ }): Promise<{ record: DiscoveredProjectRecordV1; path: string } | null> {
115
+ const workspace = await resolveProjectWorkspaceFromCwd(input);
116
+ if (workspace === null) return null;
117
+
118
+ const now = normalizeTimestamp(input.now);
119
+ const paths = resolveProjectRegistryPaths(input.homeDir);
120
+ const path = paths.discoveredPath(workspace.projectKey);
121
+ const existing = await readDiscoveredProject(path);
122
+ const record: DiscoveredProjectRecordV1 = {
123
+ schemaVersion: 1,
124
+ kind: "discovered-project",
125
+ projectKey: workspace.projectKey,
126
+ displayName: workspace.displayName,
127
+ workspaceRoot: workspace.workspaceRoot,
128
+ lastCwd: workspace.cwd,
129
+ workspaceKind: workspace.workspaceKind,
130
+ sourceTargets: mergeTargets(existing?.sourceTargets ?? [], [input.target]),
131
+ lastSessionKey: normalizeSessionKey(input.sessionKey ?? existing?.lastSessionKey ?? null),
132
+ firstSeenAt: existing?.firstSeenAt ?? now,
133
+ lastSeenAt: now,
134
+ localOnly: true,
135
+ sourceContentStored: false,
136
+ };
137
+ await writeJsonFile(path, record);
138
+ return { record, path };
139
+ }
140
+
141
+ export async function listDiscoveredProjects(input: {
142
+ homeDir: string;
143
+ }): Promise<DiscoveredProjectRecordV1[]> {
144
+ const paths = resolveProjectRegistryPaths(input.homeDir);
145
+ const records = new Map<string, DiscoveredProjectRecordV1>();
146
+
147
+ for (const path of await listJsonFilePaths(paths.discoveredDir)) {
148
+ const record = await readDiscoveredProject(path);
149
+ if (record !== null) mergeDiscoveredRecord(records, record);
150
+ }
151
+
152
+ for (const binding of await listLegacyHookBindings(input.homeDir)) {
153
+ if (binding.cwd === null) continue;
154
+ const workspace = await resolveProjectWorkspaceFromCwd({
155
+ homeDir: input.homeDir,
156
+ cwd: binding.cwd,
157
+ });
158
+ if (workspace === null) continue;
159
+ mergeDiscoveredRecord(records, {
160
+ schemaVersion: 1,
161
+ kind: "discovered-project",
162
+ projectKey: workspace.projectKey,
163
+ displayName: workspace.displayName,
164
+ workspaceRoot: workspace.workspaceRoot,
165
+ lastCwd: workspace.cwd,
166
+ workspaceKind: workspace.workspaceKind,
167
+ sourceTargets: [binding.target],
168
+ lastSessionKey: normalizeSessionKey(binding.sessionKey),
169
+ firstSeenAt: binding.updatedAt,
170
+ lastSeenAt: binding.updatedAt,
171
+ localOnly: true,
172
+ sourceContentStored: false,
173
+ });
174
+ }
175
+
176
+ return [...records.values()].sort(
177
+ (left, right) =>
178
+ right.lastSeenAt.localeCompare(left.lastSeenAt) ||
179
+ left.projectKey.localeCompare(right.projectKey),
180
+ );
181
+ }
182
+
183
+ export async function listRegisteredProjects(input: {
184
+ homeDir: string;
185
+ }): Promise<RegisteredProjectRecordV1[]> {
186
+ const { registeredDir } = resolveProjectRegistryPaths(input.homeDir);
187
+ const records: RegisteredProjectRecordV1[] = [];
188
+ for (const projectKey of await listDirectoryNames(registeredDir)) {
189
+ const record = await readRegisteredProject(join(registeredDir, projectKey, "workspace.json"));
190
+ if (record !== null && record.projectKey === projectKey) records.push(record);
191
+ }
192
+ return records.sort(
193
+ (left, right) =>
194
+ right.lastSeenAt.localeCompare(left.lastSeenAt) ||
195
+ left.projectKey.localeCompare(right.projectKey),
196
+ );
197
+ }
198
+
199
+ export async function registerDiscoveredProject(input: {
200
+ homeDir: string;
201
+ projectKey: string;
202
+ now?: Date | string;
203
+ }): Promise<{ record: RegisteredProjectRecordV1; path: string; changed: boolean }> {
204
+ assertProjectKey(input.projectKey);
205
+ const paths = resolveProjectRegistryPaths(input.homeDir);
206
+ const path = paths.registeredPath(input.projectKey);
207
+ const existing = await readRegisteredProject(path);
208
+ if (existing !== null) return { record: existing, path, changed: false };
209
+
210
+ const discovered = (await listDiscoveredProjects({ homeDir: input.homeDir })).find(
211
+ (record) => record.projectKey === input.projectKey,
212
+ );
213
+ if (discovered === undefined) {
214
+ throw new ProjectRegistrationError(
215
+ "not-found",
216
+ "The project must be discovered by a local Code Agent session before it can be added.",
217
+ );
218
+ }
219
+
220
+ const record: RegisteredProjectRecordV1 = {
221
+ schemaVersion: 1,
222
+ kind: "registered-project",
223
+ projectKey: discovered.projectKey,
224
+ displayName: discovered.displayName,
225
+ workspaceRoot: discovered.workspaceRoot,
226
+ workspaceKind: discovered.workspaceKind,
227
+ registeredAt: normalizeTimestamp(input.now),
228
+ lastSeenAt: discovered.lastSeenAt,
229
+ localOnly: true,
230
+ sourceContentStored: false,
231
+ };
232
+ try {
233
+ await writeJsonFile(path, record, { overwrite: false });
234
+ return { record, path, changed: true };
235
+ } catch (error) {
236
+ if (!isFileExistsError(error)) throw error;
237
+ const concurrent = await readRegisteredProject(path);
238
+ if (concurrent !== null) return { record: concurrent, path, changed: false };
239
+ throw new ProjectRegistrationError("conflict", "The project registration already exists.");
240
+ }
241
+ }
242
+
243
+ async function findNearestGitRoot(cwd: string): Promise<string | null> {
244
+ let current = cwd;
245
+ for (;;) {
246
+ try {
247
+ const marker = await lstat(join(current, ".git"));
248
+ if (marker.isDirectory() || marker.isFile()) return current;
249
+ } catch (error) {
250
+ if (!isNotFoundError(error)) throw error;
251
+ }
252
+ const parent = dirname(current);
253
+ if (parent === current) return null;
254
+ current = parent;
255
+ }
256
+ }
257
+
258
+ async function readDiscoveredProject(path: string): Promise<DiscoveredProjectRecordV1 | null> {
259
+ try {
260
+ return parseDiscoveredProject(JSON.parse(await readFile(path, "utf8")) as unknown);
261
+ } catch (error) {
262
+ if (isNotFoundError(error) || error instanceof ProjectRegistrationError) return null;
263
+ throw error;
264
+ }
265
+ }
266
+
267
+ async function readRegisteredProject(path: string): Promise<RegisteredProjectRecordV1 | null> {
268
+ try {
269
+ return parseRegisteredProject(JSON.parse(await readFile(path, "utf8")) as unknown);
270
+ } catch (error) {
271
+ if (isNotFoundError(error) || error instanceof ProjectRegistrationError) return null;
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ function parseDiscoveredProject(value: unknown): DiscoveredProjectRecordV1 {
277
+ const record = requireRecord(value);
278
+ if (
279
+ record.schemaVersion !== 1 ||
280
+ record.kind !== "discovered-project" ||
281
+ !isProjectKey(record.projectKey) ||
282
+ !isNonEmptyString(record.displayName) ||
283
+ !isAbsoluteString(record.workspaceRoot) ||
284
+ !isAbsoluteString(record.lastCwd) ||
285
+ !isWorkspaceKind(record.workspaceKind) ||
286
+ !isDiscoveryTargetArray(record.sourceTargets) ||
287
+ !(record.lastSessionKey === null || isNonEmptyString(record.lastSessionKey)) ||
288
+ !isNonEmptyString(record.firstSeenAt) ||
289
+ !isNonEmptyString(record.lastSeenAt) ||
290
+ record.localOnly !== true ||
291
+ record.sourceContentStored !== false
292
+ ) {
293
+ throw new ProjectRegistrationError("invalid", "Discovered project record is invalid.");
294
+ }
295
+ return record as unknown as DiscoveredProjectRecordV1;
296
+ }
297
+
298
+ function parseRegisteredProject(value: unknown): RegisteredProjectRecordV1 {
299
+ const record = requireRecord(value);
300
+ if (
301
+ record.schemaVersion !== 1 ||
302
+ record.kind !== "registered-project" ||
303
+ !isProjectKey(record.projectKey) ||
304
+ !isNonEmptyString(record.displayName) ||
305
+ !isAbsoluteString(record.workspaceRoot) ||
306
+ !isWorkspaceKind(record.workspaceKind) ||
307
+ !isNonEmptyString(record.registeredAt) ||
308
+ !isNonEmptyString(record.lastSeenAt) ||
309
+ record.localOnly !== true ||
310
+ record.sourceContentStored !== false
311
+ ) {
312
+ throw new ProjectRegistrationError("invalid", "Registered project record is invalid.");
313
+ }
314
+ return record as unknown as RegisteredProjectRecordV1;
315
+ }
316
+
317
+ function mergeDiscoveredRecord(
318
+ records: Map<string, DiscoveredProjectRecordV1>,
319
+ next: DiscoveredProjectRecordV1,
320
+ ): void {
321
+ const previous = records.get(next.projectKey);
322
+ if (previous === undefined) {
323
+ records.set(next.projectKey, next);
324
+ return;
325
+ }
326
+ const newer = next.lastSeenAt >= previous.lastSeenAt ? next : previous;
327
+ records.set(next.projectKey, {
328
+ ...newer,
329
+ sourceTargets: mergeTargets(previous.sourceTargets, next.sourceTargets),
330
+ firstSeenAt: previous.firstSeenAt <= next.firstSeenAt ? previous.firstSeenAt : next.firstSeenAt,
331
+ lastSeenAt: previous.lastSeenAt >= next.lastSeenAt ? previous.lastSeenAt : next.lastSeenAt,
332
+ });
333
+ }
334
+
335
+ function mergeTargets(
336
+ left: ProjectDiscoveryTarget[],
337
+ right: ProjectDiscoveryTarget[],
338
+ ): ProjectDiscoveryTarget[] {
339
+ return [...new Set([...left, ...right])].sort();
340
+ }
341
+
342
+ function normalizeSessionKey(value: string | null | undefined): string | null {
343
+ if (value === null || value === undefined) return null;
344
+ return /^session-[a-f0-9]{16}$/u.test(value) ? value : `session-${sha256Short(value)}`;
345
+ }
346
+
347
+ interface LegacyHookBinding {
348
+ target: ProjectDiscoveryTarget;
349
+ sessionKey: string;
350
+ cwd: string | null;
351
+ updatedAt: string;
352
+ }
353
+
354
+ async function listLegacyHookBindings(homeDir: string): Promise<LegacyHookBinding[]> {
355
+ const roots = [
356
+ join(homeDir, ".evodev", "state", "hooks", "sessions"),
357
+ join(homeDir, ".evodev", "STATE", "hooks", "sessions"),
358
+ ];
359
+ const bindings: LegacyHookBinding[] = [];
360
+ for (const root of roots) {
361
+ for (const sessionDir of await listDirectoryNames(root)) {
362
+ try {
363
+ const value = JSON.parse(
364
+ await readFile(join(root, sessionDir, "binding.json"), "utf8"),
365
+ ) as Record<string, unknown> | null;
366
+ if (
367
+ value === null ||
368
+ (value.target !== "claude" && value.target !== "codex") ||
369
+ !isNonEmptyString(value.sessionKey) ||
370
+ !(value.cwd === null || isAbsoluteString(value.cwd)) ||
371
+ !isNonEmptyString(value.updatedAt)
372
+ ) {
373
+ continue;
374
+ }
375
+ bindings.push({
376
+ target: value.target,
377
+ sessionKey: value.sessionKey,
378
+ cwd: value.cwd,
379
+ updatedAt: value.updatedAt,
380
+ });
381
+ } catch (error) {
382
+ if (!isNotFoundError(error) && !(error instanceof SyntaxError)) throw error;
383
+ }
384
+ }
385
+ }
386
+ return bindings;
387
+ }
388
+
389
+ async function listJsonFilePaths(path: string): Promise<string[]> {
390
+ try {
391
+ return (await readdir(path, { withFileTypes: true }))
392
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
393
+ .map((entry) => join(path, entry.name))
394
+ .sort();
395
+ } catch (error) {
396
+ if (isNotFoundError(error)) return [];
397
+ throw error;
398
+ }
399
+ }
400
+
401
+ async function listDirectoryNames(path: string): Promise<string[]> {
402
+ try {
403
+ return (await readdir(path, { withFileTypes: true }))
404
+ .filter((entry) => entry.isDirectory())
405
+ .map((entry) => entry.name)
406
+ .sort();
407
+ } catch (error) {
408
+ if (isNotFoundError(error)) return [];
409
+ throw error;
410
+ }
411
+ }
412
+
413
+ function assertProjectKey(value: string): void {
414
+ if (!isProjectKey(value)) {
415
+ throw new ProjectRegistrationError("invalid", "projectKey is invalid.");
416
+ }
417
+ }
418
+
419
+ function isProjectKey(value: unknown): value is string {
420
+ return (
421
+ typeof value === "string" &&
422
+ value !== "." &&
423
+ value !== ".." &&
424
+ /^[A-Za-z0-9._-]{1,120}$/.test(value)
425
+ );
426
+ }
427
+
428
+ function isNonEmptyString(value: unknown): value is string {
429
+ return typeof value === "string" && value.trim() !== "";
430
+ }
431
+
432
+ function isAbsoluteString(value: unknown): value is string {
433
+ return isNonEmptyString(value) && isAbsolute(value);
434
+ }
435
+
436
+ function isWorkspaceKind(value: unknown): value is ProjectWorkspaceKind {
437
+ return value === "git" || value === "directory";
438
+ }
439
+
440
+ function isDiscoveryTargetArray(value: unknown): value is ProjectDiscoveryTarget[] {
441
+ return (
442
+ Array.isArray(value) &&
443
+ value.length > 0 &&
444
+ value.every((item) => item === "claude" || item === "codex")
445
+ );
446
+ }
447
+
448
+ function requireRecord(value: unknown): Record<string, unknown> {
449
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
450
+ throw new ProjectRegistrationError("invalid", "Project record must be an object.");
451
+ }
452
+ return value as Record<string, unknown>;
453
+ }