engineering-memory 0.1.0

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 (40) hide show
  1. package/bin/engineering-memory.mjs +120 -0
  2. package/dispatcher/managed-section.mjs +59 -0
  3. package/dispatcher/sections.mjs +14 -0
  4. package/install/api-url.mjs +39 -0
  5. package/install/cli.mjs +93 -0
  6. package/install/commands.mjs +140 -0
  7. package/install/files.mjs +416 -0
  8. package/install/git-hook.mjs +270 -0
  9. package/install/installer.mjs +279 -0
  10. package/install/mcp-registration.mjs +457 -0
  11. package/package.json +28 -0
  12. package/runtime/dist/src/auth/browser-auth.js +184 -0
  13. package/runtime/dist/src/auth/credential-store.js +181 -0
  14. package/runtime/dist/src/cache/etag-cache.js +123 -0
  15. package/runtime/dist/src/config.js +59 -0
  16. package/runtime/dist/src/git/git-inspector.js +375 -0
  17. package/runtime/dist/src/git/pre-commit.js +44 -0
  18. package/runtime/dist/src/git/verification-gate.js +221 -0
  19. package/runtime/dist/src/index.js +60 -0
  20. package/runtime/dist/src/journal/journal-store.js +1300 -0
  21. package/runtime/dist/src/mcp/server.js +11 -0
  22. package/runtime/dist/src/mcp/tool-definitions.js +405 -0
  23. package/runtime/dist/src/project/repository.js +79 -0
  24. package/runtime/dist/src/runtime/active-context-store.js +356 -0
  25. package/runtime/dist/src/runtime/api-client.js +229 -0
  26. package/runtime/dist/src/runtime/bridge-service.js +2226 -0
  27. package/runtime/dist/src/runtime/offline-outbox.js +274 -0
  28. package/runtime/dist/src/runtime/principal-state.js +97 -0
  29. package/runtime/dist/src/types.js +2 -0
  30. package/runtime/dist/src/utilities/files.js +189 -0
  31. package/runtime/dist/src/utilities/hash.js +19 -0
  32. package/runtime/dist/src/utilities/process.js +32 -0
  33. package/runtime/package-lock.json +137 -0
  34. package/runtime/package.json +32 -0
  35. package/skill/SKILL.md +29 -0
  36. package/skill/agents/openai.yaml +6 -0
  37. package/skill/references/lifecycle.md +102 -0
  38. package/skill/references/memory-updates.md +25 -0
  39. package/skill/references/questionnaires.md +98 -0
  40. package/skill/references/scaffolding.md +38 -0
@@ -0,0 +1,11 @@
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
+ import { registerEngineeringMemoryTools } from './tool-definitions.js';
3
+ export function createEngineeringMemoryServer(service) {
4
+ const server = new McpServer({
5
+ name: 'engineering-memory',
6
+ version: '0.1.0',
7
+ });
8
+ registerEngineeringMemoryTools(server, service);
9
+ return server;
10
+ }
11
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,405 @@
1
+ import * as z from 'zod/v4';
2
+ import { validationIds } from '../runtime/bridge-service.js';
3
+ const optionalRepoRoot = z.string().min(1).optional();
4
+ const stringList = z.array(z.string().min(1));
5
+ const jsonValue = z.lazy(() => z.union([
6
+ z.string(),
7
+ z.number(),
8
+ z.boolean(),
9
+ z.null(),
10
+ z.array(jsonValue),
11
+ z.record(z.string(), jsonValue),
12
+ ]));
13
+ const jsonObject = z.record(z.string(), jsonValue);
14
+ const stateDocument = z.object({
15
+ objective: z.string().optional(),
16
+ phase: z.string().optional(),
17
+ completed: stringList.optional(),
18
+ nextAction: z.string().optional(),
19
+ blockers: stringList.optional(),
20
+ forbiddenActions: stringList.optional(),
21
+ });
22
+ const handoffDocument = z.object({
23
+ completed: stringList.optional(),
24
+ remaining: stringList.optional(),
25
+ restartFrom: z.string().optional(),
26
+ });
27
+ const documents = z
28
+ .object({
29
+ state: stateDocument.optional(),
30
+ decisions: stringList.optional(),
31
+ discovery: stringList.optional(),
32
+ validation: stringList.optional(),
33
+ handoff: handoffDocument.optional(),
34
+ })
35
+ .optional();
36
+ const checkpointBase = {
37
+ repoRoot: optionalRepoRoot,
38
+ taskId: z.string().min(1),
39
+ projectId: z.string().min(1),
40
+ taskSlug: z.string().min(1),
41
+ summary: z.string().min(1),
42
+ details: z.string().optional(),
43
+ documents,
44
+ idempotencyKey: z.string().uuid().optional(),
45
+ };
46
+ export const engineeringMemoryToolNames = [
47
+ 'session.bootstrap',
48
+ 'session.resume',
49
+ 'context.prepare_change',
50
+ 'context.refresh',
51
+ 'memory.query',
52
+ 'memory.history',
53
+ 'memory.propose_revision',
54
+ 'memory.list_proposals',
55
+ 'memory.review_proposal',
56
+ 'task.checkpoint',
57
+ 'task.record_correction',
58
+ 'task.self_review',
59
+ 'task.reconcile',
60
+ 'task.resolve_pending_delivery',
61
+ 'task.verify',
62
+ 'task.close',
63
+ 'architecture.plan',
64
+ 'architecture.module',
65
+ 'architecture.record_application',
66
+ 'organization.list',
67
+ 'organization.create',
68
+ 'project.setup',
69
+ 'project.list',
70
+ 'project.resolve',
71
+ 'project.member_add',
72
+ 'auth.status',
73
+ 'auth.signin_browser',
74
+ 'auth.logout',
75
+ ];
76
+ export function registerEngineeringMemoryTools(server, service) {
77
+ server.registerTool('session.bootstrap', {
78
+ description: 'Authenticate, resolve the repository project, open or resume a write or read-only task, and load mandatory engineering context before planning.',
79
+ inputSchema: z.object({
80
+ repoRoot: optionalRepoRoot,
81
+ projectId: z.string().optional(),
82
+ externalTaskId: z.string().min(2),
83
+ objective: z.string().min(2),
84
+ taskKind: z.string().min(2),
85
+ mode: z.enum(['write', 'read_only', 'scaffold']).optional(),
86
+ knownRevisions: z.record(z.string(), z.number().int().min(0)).optional(),
87
+ }),
88
+ }, async (input) => toolResult(await service.sessionBootstrap(input)));
89
+ server.registerTool('session.resume', {
90
+ description: 'Merge backend events, local journal, offline outbox, pinned context and current Git state after a new chat or context compaction.',
91
+ inputSchema: z.object({
92
+ repoRoot: optionalRepoRoot,
93
+ projectId: z.string().min(1).optional(),
94
+ taskSlug: z.string().min(1).optional(),
95
+ sessionId: z.string().min(1).optional(),
96
+ afterSequence: z.number().int().min(0).optional(),
97
+ }),
98
+ }, async (input) => toolResult(await service.sessionResume(input)));
99
+ server.registerTool('context.prepare_change', {
100
+ description: 'Load path-specific engineering memory, obtain a revision-pinned change lease, or explicitly transition a read-only task to write mode before editing.',
101
+ inputSchema: z.object({
102
+ repoRoot: optionalRepoRoot,
103
+ sessionId: z.string().min(1),
104
+ changedPaths: stringList.min(1),
105
+ transitionToWrite: z.boolean().optional(),
106
+ }),
107
+ }, async (input) => toolResult(await service.contextPrepareChange(input)));
108
+ server.registerTool('context.refresh', {
109
+ description: 'Invalidate stale revision pins after an approved memory change and require a new prepare-change lease.',
110
+ inputSchema: z.object({
111
+ sessionId: z.string().min(1),
112
+ }),
113
+ }, async (input) => toolResult(await service.contextRefresh(input)));
114
+ server.registerTool('memory.query', {
115
+ description: 'Read the smallest project memory pack needed for screens, components, services, navigation, localization or state work.',
116
+ inputSchema: z.object({
117
+ projectId: z.string().min(1),
118
+ sessionId: z.string().min(1),
119
+ kinds: stringList.optional(),
120
+ changedPaths: stringList.optional(),
121
+ codeSymbols: stringList.optional(),
122
+ figmaNodeIds: stringList.optional(),
123
+ tags: stringList.optional(),
124
+ }),
125
+ }, async (input) => toolResult(await service.memoryQuery(input)));
126
+ server.registerTool('memory.history', {
127
+ description: 'Read why the code you are about to change is the way it is: the revision history of the screen, component or contract records covering the given paths or keys, each revision with the task that produced it and the stated reason, plus the earlier tasks that touched those records and how many corrections each of them took. Also returns the task references found in the Git history of those paths. Verification refuses while a record with earlier work on it was never read.',
128
+ inputSchema: z.object({
129
+ repoRoot: optionalRepoRoot,
130
+ sessionId: z.string().min(1),
131
+ resourceKeys: z.array(z.string().min(1)).optional(),
132
+ paths: z.array(z.string().min(1)).optional(),
133
+ depth: z.number().int().min(1).max(50).optional(),
134
+ }),
135
+ }, async (input) => toolResult(await service.memoryHistory(input)));
136
+ server.registerTool('memory.propose_revision', {
137
+ description: 'Create an inactive, reviewable proposal for a permanent project or organization engineering memory revision.',
138
+ inputSchema: z.object({
139
+ repoRoot: optionalRepoRoot,
140
+ resourceId: z.string().optional(),
141
+ projectId: z.string().min(1),
142
+ taskId: z.string().optional(),
143
+ scope: z.enum(['product', 'organization', 'project']),
144
+ kind: z.enum([
145
+ 'engineering_rule',
146
+ 'project_profile',
147
+ 'service_contract',
148
+ 'localization_contract',
149
+ 'navigation_contract',
150
+ 'state_contract',
151
+ 'screen_logic',
152
+ 'component_mapping',
153
+ 'figma_mapping',
154
+ 'figma_reference',
155
+ 'flow_logic',
156
+ 'current_deviation',
157
+ 'quality_gate',
158
+ 'task_history',
159
+ 'architecture_template',
160
+ ]),
161
+ resourceKey: z.string().min(2),
162
+ title: z.string().min(2),
163
+ baseRevision: z.number().int().min(0),
164
+ content: z.string().min(1),
165
+ reason: z.string().min(1),
166
+ affectedAreas: stringList,
167
+ regressionEvidence: jsonObject,
168
+ pathPatterns: stringList.optional(),
169
+ codeSymbols: stringList.optional(),
170
+ figmaNodeIds: stringList.optional(),
171
+ tags: stringList.optional(),
172
+ metadata: jsonObject.optional(),
173
+ provenance: jsonObject.optional(),
174
+ }),
175
+ }, async (input) => toolResult(await service.memoryProposeRevision(input)));
176
+ server.registerTool('memory.list_proposals', {
177
+ description: 'List the permanent memory proposals still waiting for review, so an authorized reviewer can find them without database access. Pass proposalId to read one proposal with its proposed content.',
178
+ inputSchema: z.object({
179
+ projectId: z.string().min(1),
180
+ proposalId: z.string().min(1).optional(),
181
+ }),
182
+ }, async (input) => toolResult(await service.memoryListProposals(input)));
183
+ server.registerTool('memory.review_proposal', {
184
+ description: 'Approve or reject a permanent memory proposal after the user explicitly selects its scope.',
185
+ inputSchema: z.object({
186
+ proposalId: z.string().min(1),
187
+ projectId: z.string().min(1),
188
+ decision: z.enum(['approved', 'rejected']),
189
+ note: z.string().min(1).optional(),
190
+ }),
191
+ }, async (input) => toolResult(await service.memoryReviewProposal(input)));
192
+ server.registerTool('task.checkpoint', {
193
+ description: 'Atomically update local task Markdown projections and synchronize an idempotent mandatory checkpoint.',
194
+ inputSchema: z.object({
195
+ ...checkpointBase,
196
+ checkpointType: z.enum([
197
+ 'bootstrap',
198
+ 'discovery',
199
+ 'pre_edit',
200
+ 'phase_transition',
201
+ 'validation_before',
202
+ 'validation_after',
203
+ 'handoff_before',
204
+ ]),
205
+ }),
206
+ }, async (input) => toolResult(await service.taskCheckpoint(input)));
207
+ server.registerTool('task.record_correction', {
208
+ description: 'Record a user correction immediately, then ask the user in the same reply where it belongs. Omitting correctionScope, or sending "pending", leaves the scope decision open and blocks task.verify until it is answered. Send the answer with the same correctionRef.',
209
+ inputSchema: z.object({
210
+ ...checkpointBase,
211
+ correctionScope: z
212
+ .enum(['pending', 'task', 'project', 'organization', 'product_default'])
213
+ .optional(),
214
+ correctionRef: z.string().min(1).optional(),
215
+ }),
216
+ }, async (input) => toolResult(await service.taskRecordCorrection(input)));
217
+ server.registerTool('task.self_review', {
218
+ description: 'Record that the changed code was read back against the rules that govern it, naming the knowledge resources reviewed and every conflict found with how it was resolved. task.verify refuses until this exists for the current diff, so any further edit requires reviewing again.',
219
+ inputSchema: z.object({
220
+ repoRoot: optionalRepoRoot,
221
+ taskId: z.string().min(1),
222
+ reviewedResourceIds: z.array(z.string().min(1)),
223
+ findings: z.array(z.object({
224
+ path: z.string().min(1),
225
+ rule: z.string().min(1),
226
+ issue: z.string().min(8),
227
+ resolution: z.string().min(8),
228
+ })),
229
+ }),
230
+ }, async (input) => toolResult(await service.taskSelfReview(input)));
231
+ server.registerTool('task.reconcile', {
232
+ description: 'Reconcile each changed screen or component with an approved revision or an explicit no-semantic-memory-change reason.',
233
+ inputSchema: z
234
+ .object({
235
+ repoRoot: optionalRepoRoot,
236
+ taskId: z.string().min(1),
237
+ resourceId: z.string().min(1),
238
+ type: z.enum(['approved_revision', 'no_semantic_memory_change']),
239
+ proposalId: z.string().optional(),
240
+ revisionId: z.string().optional(),
241
+ reason: z.string().optional(),
242
+ })
243
+ .superRefine((value, context) => {
244
+ if (value.type === 'approved_revision' && (!value.proposalId || !value.revisionId)) {
245
+ context.addIssue({
246
+ code: 'custom',
247
+ message: 'Approved reconciliation requires proposalId and revisionId',
248
+ });
249
+ }
250
+ if (value.type === 'no_semantic_memory_change' &&
251
+ (!value.reason || value.reason.length < 8)) {
252
+ context.addIssue({
253
+ code: 'custom',
254
+ message: 'No-semantic-change reconciliation requires a reason of at least 8 characters',
255
+ });
256
+ }
257
+ }),
258
+ }, async (input) => toolResult(await service.taskReconcile(input)));
259
+ server.registerTool('task.resolve_pending_delivery', {
260
+ description: 'Inspect or explicitly discard any pending delivery; journal events can be rebased after a version conflict, while permanent proposals require refreshed revision comparison and native user approval.',
261
+ inputSchema: z.object({
262
+ repoRoot: optionalRepoRoot,
263
+ outboxId: z.string().regex(/^[0-9a-f]{64}$/),
264
+ action: z.enum(['inspect', 'rebase', 'discard']),
265
+ confirmedBaseRevision: z.number().int().min(0).optional(),
266
+ proposalRebaseApproved: z.boolean().optional(),
267
+ confirm: z.boolean().optional(),
268
+ }),
269
+ }, async (input) => toolResult(await service.taskResolvePendingDelivery(input)));
270
+ server.registerTool('task.verify', {
271
+ description: 'Verify a write task with its lease or a read-only task against its pinned Git baseline, mandatory checkpoints, structured validations and synchronized outbox.',
272
+ inputSchema: z.object({
273
+ repoRoot: optionalRepoRoot,
274
+ taskId: z.string().min(1),
275
+ sessionId: z.string().min(1),
276
+ leaseId: z.string().min(1).optional(),
277
+ changedPaths: stringList.min(1).optional(),
278
+ validations: z
279
+ .array(z.object({
280
+ validationId: z.enum(validationIds),
281
+ command: z.string().min(1),
282
+ passed: z.boolean(),
283
+ outputHash: z.string().regex(/^[0-9a-f]{64}$/),
284
+ }))
285
+ .min(1),
286
+ newResources: z
287
+ .array(z.object({
288
+ path: z.string().min(1),
289
+ kind: z.enum(['screen_logic', 'component_mapping']),
290
+ resourceKey: z.string().min(2),
291
+ }))
292
+ .optional(),
293
+ }),
294
+ }, async (input) => toolResult(await service.taskVerify(input)));
295
+ server.registerTool('task.close', {
296
+ description: 'Close a task only when backend verification still matches the current Git diff hash.',
297
+ inputSchema: z.object({
298
+ repoRoot: optionalRepoRoot,
299
+ taskId: z.string().min(1),
300
+ }),
301
+ }, async (input) => toolResult(await service.taskClose(input)));
302
+ server.registerTool('organization.create', {
303
+ description: 'Create an organization the user named, with the short identifier they typed. Offer this underneath the organizations they already belong to; never invent the identifier from the name.',
304
+ inputSchema: z.object({
305
+ name: z.string().min(2).max(120),
306
+ slug: z.string().min(2).max(120),
307
+ }),
308
+ }, async (input) => toolResult(await service.organizationCreate(input)));
309
+ server.registerTool('project.setup', {
310
+ description: 'Create a policy-ready backend project from the native questionnaire and local repository audit, then write its secret-free marker.',
311
+ inputSchema: z.object({
312
+ repoRoot: optionalRepoRoot,
313
+ organizationId: z.string().uuid(),
314
+ projectName: z.string().min(2),
315
+ projectSlug: z.string().min(2).optional(),
316
+ framework: z.string().optional(),
317
+ figmaConfig: jsonObject,
318
+ initialProjectProfile: z.object({
319
+ title: z.string().min(2),
320
+ content: z.string().min(2),
321
+ screenPathPatterns: z.array(z.string().min(1)).min(1),
322
+ componentPathPatterns: z.array(z.string().min(1)).min(1),
323
+ metadata: jsonObject,
324
+ provenance: jsonObject,
325
+ }),
326
+ }),
327
+ }, async (input) => toolResult(await service.projectSetup(input)));
328
+ server.registerTool('architecture.plan', {
329
+ description: 'List the organization architecture modules in apply order with their manifests, rename map, asset contract and tenant-specific points. Returns no file bodies.',
330
+ inputSchema: z.object({
331
+ projectId: z.string().uuid(),
332
+ sessionId: z.string().uuid(),
333
+ moduleKeys: z.array(z.string().min(1)).optional(),
334
+ }),
335
+ }, async (input) => toolResult(await service.architecturePlan(input)));
336
+ server.registerTool('architecture.module', {
337
+ description: 'Load one architecture module with its full file bodies so the agent can apply the rename map and write the files itself.',
338
+ inputSchema: z.object({
339
+ projectId: z.string().uuid(),
340
+ sessionId: z.string().uuid(),
341
+ moduleKey: z.string().min(1),
342
+ }),
343
+ }, async (input) => toolResult(await service.architectureModule(input)));
344
+ server.registerTool('architecture.record_application', {
345
+ description: 'Record that a scaffold task wrote one architecture module, hashing each written file after the rename so verification can exempt untouched scaffolded files.',
346
+ inputSchema: z.object({
347
+ repoRoot: optionalRepoRoot,
348
+ taskId: z.string().min(1),
349
+ projectId: z.string().uuid(),
350
+ templateResourceId: z.string().uuid(),
351
+ templateRevisionId: z.string().uuid(),
352
+ files: z
353
+ .array(z.object({
354
+ templatePath: z.string().min(1),
355
+ path: z.string().min(1),
356
+ }))
357
+ .min(1),
358
+ }),
359
+ }, async (input) => toolResult(await service.architectureRecordApplication(input)));
360
+ server.registerTool('organization.list', {
361
+ description: 'List the organizations the authenticated user belongs to so project setup can inherit an existing engineering core instead of silently creating an empty organization.',
362
+ inputSchema: z.object({}),
363
+ }, async () => toolResult(await service.organizationList()));
364
+ server.registerTool('project.list', {
365
+ description: 'List projects available to the authenticated user so the agent can present them through the native questionnaire UI.',
366
+ inputSchema: z.object({}),
367
+ }, async () => toolResult(await service.projectList()));
368
+ server.registerTool('project.resolve', {
369
+ description: 'Resolve the current repository fingerprint or bind an explicitly selected project, then write the secret-free marker only after backend authorization succeeds.',
370
+ inputSchema: z.object({
371
+ repoRoot: optionalRepoRoot,
372
+ bind: z.boolean().optional(),
373
+ projectId: z.string().uuid().optional(),
374
+ }),
375
+ }, async (input) => toolResult(await service.projectResolve(input)));
376
+ server.registerTool('project.member_add', {
377
+ description: 'Add an existing Engineering Memory account to a project with an explicit role.',
378
+ inputSchema: z.object({
379
+ projectId: z.string().min(1),
380
+ email: z.string().email(),
381
+ role: z.enum(['owner', 'maintainer', 'member', 'reader']),
382
+ }),
383
+ }, async (input) => toolResult(await service.projectMemberAdd(input)));
384
+ server.registerTool('auth.logout', {
385
+ description: 'Revoke the refresh session and delete local credentials; transient revoke failures preserve the session unless the user explicitly confirms local-only logout.',
386
+ inputSchema: z.object({
387
+ confirmLocalOnly: z.boolean().optional(),
388
+ }),
389
+ }, async (input) => toolResult(await service.authLogout(input)));
390
+ server.registerTool('auth.status', {
391
+ description: 'Check whether OS credential storage contains an active local Engineering Memory session.',
392
+ inputSchema: z.object({}),
393
+ }, async () => toolResult(await service.authStatus()));
394
+ server.registerTool('auth.signin_browser', {
395
+ description: 'Start loopback PKCE browser signin and return the URL that Codex or Claude should open for the user. Set restart after an expired, rejected, or otherwise unusable browser link.',
396
+ inputSchema: z.object({ restart: z.boolean().optional() }),
397
+ }, async (input) => toolResult(await service.authSigninBrowser(input)));
398
+ }
399
+ function toolResult(result) {
400
+ return {
401
+ content: [{ type: 'text', text: JSON.stringify(result) }],
402
+ isError: !result.ok,
403
+ };
404
+ }
405
+ //# sourceMappingURL=tool-definitions.js.map
@@ -0,0 +1,79 @@
1
+ import { dirname, join, parse, resolve } from 'node:path';
2
+ import { canonicalPath, pathExists, readJson, writeJson } from '../utilities/files.js';
3
+ export class RepositoryResolver {
4
+ git;
5
+ markerSchemaVersion;
6
+ constructor(git, markerSchemaVersion) {
7
+ this.git = git;
8
+ this.markerSchemaVersion = markerSchemaVersion;
9
+ }
10
+ async resolve(startPath, explicitProjectId) {
11
+ const repoRoot = await this.git.findRoot(startPath);
12
+ const markerPath = await this.findMarker(startPath, repoRoot);
13
+ const marker = markerPath ? await this.readMarker(markerPath, repoRoot) : null;
14
+ if (explicitProjectId) {
15
+ assertProjectId(explicitProjectId);
16
+ }
17
+ if (explicitProjectId && marker && explicitProjectId !== marker.projectId) {
18
+ throw new Error('Explicit project does not match the repository marker');
19
+ }
20
+ const [repoFingerprint, git] = await Promise.all([
21
+ this.git.fingerprint(repoRoot),
22
+ this.git.manifest(repoRoot),
23
+ ]);
24
+ return {
25
+ repoRoot,
26
+ markerPath,
27
+ projectId: explicitProjectId ?? marker?.projectId ?? null,
28
+ schemaVersion: marker?.schemaVersion ?? null,
29
+ repoFingerprint,
30
+ git,
31
+ };
32
+ }
33
+ async writeMarker(repoRoot, projectId) {
34
+ assertProjectId(projectId);
35
+ const canonicalRoot = await canonicalPath(repoRoot);
36
+ const markerPath = join(canonicalRoot, '.engineering-memory', 'project.json');
37
+ await writeJson(markerPath, {
38
+ projectId,
39
+ schemaVersion: this.markerSchemaVersion,
40
+ }, canonicalRoot);
41
+ return markerPath;
42
+ }
43
+ async findMarker(startPath, repoRoot) {
44
+ let current = await canonicalPath(startPath);
45
+ const root = await canonicalPath(repoRoot);
46
+ while (true) {
47
+ const candidate = join(current, '.engineering-memory', 'project.json');
48
+ if (await pathExists(candidate, root)) {
49
+ return candidate;
50
+ }
51
+ if (current === root || current === parse(current).root) {
52
+ return null;
53
+ }
54
+ current = dirname(current);
55
+ }
56
+ }
57
+ async readMarker(path, repoRoot) {
58
+ const marker = await readJson(path, repoRoot);
59
+ const schemaVersion = marker?.schemaVersion;
60
+ if (!marker ||
61
+ typeof marker.projectId !== 'string' ||
62
+ typeof schemaVersion !== 'number' ||
63
+ !Number.isInteger(schemaVersion) ||
64
+ schemaVersion < 1) {
65
+ throw new Error(`Invalid Engineering Memory project marker: ${resolve(path)}`);
66
+ }
67
+ if (schemaVersion > this.markerSchemaVersion) {
68
+ throw new Error(`Unsupported project marker schema: ${schemaVersion}`);
69
+ }
70
+ assertProjectId(marker.projectId);
71
+ return marker;
72
+ }
73
+ }
74
+ function assertProjectId(value) {
75
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {
76
+ throw new Error('Engineering Memory project identifier must be a UUID');
77
+ }
78
+ }
79
+ //# sourceMappingURL=repository.js.map