fluffy-context 0.3.1 → 0.5.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 (34) hide show
  1. package/README.md +39 -4
  2. package/dist/src/agent/api.js +20 -9
  3. package/dist/src/cli/main.js +96 -2
  4. package/dist/src/cognition/git-observer.d.ts +13 -0
  5. package/dist/src/cognition/git-observer.js +61 -0
  6. package/dist/src/cognition/journal.d.ts +24 -0
  7. package/dist/src/cognition/journal.js +113 -0
  8. package/dist/src/cognition/migration-v1.d.ts +7 -0
  9. package/dist/src/cognition/migration-v1.js +199 -0
  10. package/dist/src/cognition/orient.d.ts +9 -0
  11. package/dist/src/cognition/orient.js +85 -0
  12. package/dist/src/cognition/projections.d.ts +7 -0
  13. package/dist/src/cognition/projections.js +127 -0
  14. package/dist/src/cognition/readiness.d.ts +2 -0
  15. package/dist/src/cognition/readiness.js +81 -0
  16. package/dist/src/cognition/runtime.d.ts +12 -0
  17. package/dist/src/cognition/runtime.js +295 -0
  18. package/dist/src/cognition/types.d.ts +132 -0
  19. package/dist/src/cognition/types.js +1 -0
  20. package/dist/src/cognition/v1-bridge.d.ts +7 -0
  21. package/dist/src/cognition/v1-bridge.js +74 -0
  22. package/dist/src/git/git-adapter.d.ts +7 -0
  23. package/dist/src/git/git-adapter.js +16 -6
  24. package/dist/src/hooks/claude-code.js +53 -0
  25. package/dist/src/runtime/diagnostics.js +6 -0
  26. package/dist/src/runtime/knowledge.js +5 -0
  27. package/dist/src/runtime/notes.js +8 -1
  28. package/dist/src/runtime/runtime.d.ts +1 -0
  29. package/dist/src/runtime/runtime.js +28 -0
  30. package/dist/src/storage/layout.d.ts +18 -0
  31. package/dist/src/storage/layout.js +54 -0
  32. package/dist/src/version.d.ts +1 -1
  33. package/dist/src/version.js +1 -1
  34. package/package.json +1 -1
@@ -0,0 +1,199 @@
1
+ import crypto from 'node:crypto';
2
+ import { mkdir, readFile, readdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { resolveProjectRoot } from '../project/project-resolver.js';
5
+ import { loadManifest } from '../runtime/init.js';
6
+ import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
7
+ import { listNotes } from '../runtime/notes.js';
8
+ import { rebuildSnapshot } from '../runtime/runtime.js';
9
+ import { atomicWriteJson } from '../storage/atomic-write.js';
10
+ import { withLock } from '../storage/lock.js';
11
+ import { cognitionManifestPath, cognitionPolicyPath, cognitionQuarantineDirectory, cognitionV1ImportPath, cognitionV1SyncPath, contextMetadataPath, contextsRoot, locksDirectory, manifestPath, snapshotPath, snapshotsDirectory, } from '../storage/layout.js';
12
+ import { readJson } from '../storage/json-store.js';
13
+ import { appendEvent } from './journal.js';
14
+ import { rebuildProjections } from './projections.js';
15
+ import { v1Fingerprint } from './v1-bridge.js';
16
+ import { migrationReadiness } from './readiness.js';
17
+ import { COGNITION_LAYOUT_VERSION } from './types.js';
18
+ function sha256(value) {
19
+ return crypto.createHash('sha256').update(value).digest('hex');
20
+ }
21
+ function isNotFound(error) {
22
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
23
+ }
24
+ async function readOptional(filePath) {
25
+ try {
26
+ return await readFile(filePath, 'utf8');
27
+ }
28
+ catch (error) {
29
+ if (isNotFound(error))
30
+ return null;
31
+ throw error;
32
+ }
33
+ }
34
+ async function sourceRecords(projectRoot) {
35
+ const sources = [];
36
+ const records = [];
37
+ const snapshotAnchors = new Map();
38
+ const v1ManifestPath = manifestPath(projectRoot);
39
+ sources.push({ filePath: v1ManifestPath, content: await readFile(v1ManifestPath, 'utf8') });
40
+ const contextIds = (await readdir(contextsRoot(projectRoot))).sort();
41
+ for (const contextId of contextIds) {
42
+ const contextFile = contextMetadataPath(projectRoot, contextId);
43
+ const contextContent = await readFile(contextFile, 'utf8');
44
+ sources.push({ filePath: contextFile, content: contextContent });
45
+ const context = await readJson(contextFile);
46
+ const snapshots = (await readdir(snapshotsDirectory(projectRoot, contextId))).filter((file) => file.endsWith('.json')).sort();
47
+ for (const file of snapshots) {
48
+ const snapshotId = file.slice(0, -'.json'.length);
49
+ const filePath = snapshotPath(projectRoot, contextId, snapshotId);
50
+ const content = await readFile(filePath, 'utf8');
51
+ sources.push({ filePath, content });
52
+ const snapshot = await readJson(filePath);
53
+ await rebuildSnapshot(projectRoot, contextId, snapshotId);
54
+ const git = { branch: snapshot.branch, head: snapshot.commit };
55
+ snapshotAnchors.set(`${contextId}:${snapshotId}`, git);
56
+ records.push({
57
+ kind: 'context',
58
+ id: `${contextId}:${snapshotId}`,
59
+ occurredAt: snapshot.createdAt,
60
+ git,
61
+ record: { context, snapshot },
62
+ sourcePath: filePath,
63
+ sourceHash: sha256(content),
64
+ });
65
+ }
66
+ }
67
+ const noteFile = path.join(projectRoot, '.context', 'notes.json');
68
+ const knowledgeFile = path.join(projectRoot, '.context', 'knowledge.json');
69
+ const deadendFile = path.join(projectRoot, '.context', 'deadends.json');
70
+ for (const [kind, filePath] of [['note', noteFile], ['knowledge', knowledgeFile], ['deadend', deadendFile]]) {
71
+ const content = await readOptional(filePath);
72
+ if (content === null)
73
+ continue;
74
+ sources.push({ filePath, content });
75
+ const sourceHash = sha256(content);
76
+ const items = kind === 'note' ? await listNotes(projectRoot, { limit: Number.MAX_SAFE_INTEGER, maxChars: Number.MAX_SAFE_INTEGER })
77
+ : kind === 'knowledge' ? await listKnowledge(projectRoot, true)
78
+ : await listDeadends(projectRoot, true);
79
+ for (const item of items) {
80
+ const record = item;
81
+ const id = typeof record.noteId === 'string' ? record.noteId : typeof record.knowledgeId === 'string' ? record.knowledgeId : record.deadendId;
82
+ const occurredAt = item.createdAt;
83
+ const contextId = 'contextId' in item ? item.contextId : 'sourceContextId' in item ? item.sourceContextId : null;
84
+ const snapshotId = 'snapshotId' in item ? item.snapshotId : 'sourceSnapshotId' in item ? item.sourceSnapshotId : null;
85
+ if (snapshotId && !contextId)
86
+ throw new Error(`${kind} ${id} has a snapshot without a context`);
87
+ if (contextId && snapshotId)
88
+ await rebuildSnapshot(projectRoot, contextId, snapshotId);
89
+ records.push({ kind, id, occurredAt, git: contextId && snapshotId ? snapshotAnchors.get(`${contextId}:${snapshotId}`) ?? { branch: null, head: null } : { branch: null, head: null }, record: item, sourcePath: filePath, sourceHash });
90
+ }
91
+ }
92
+ const sourceHash = sha256(sources.sort((left, right) => left.filePath.localeCompare(right.filePath)).map((source) => `${source.filePath}\0${sha256(source.content)}`).join('\n'));
93
+ return { records: records.sort((left, right) => left.occurredAt.localeCompare(right.occurredAt) || left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id)), sourceHash };
94
+ }
95
+ async function writeV2Metadata(projectRoot) {
96
+ const v1 = await loadManifest(projectRoot);
97
+ const manifest = { schemaVersion: 2, layoutVersion: COGNITION_LAYOUT_VERSION, projectId: v1.projectId, createdAt: new Date().toISOString(), policyVersion: 1 };
98
+ const policy = { schemaVersion: 2, enabled: false, allowHookStartup: false, inactivityTimeoutMs: 300_000, promptIntentMaxChars: 500, filesystemEventsEnabled: false, retentionMaxBytes: 64 * 1024 * 1024 };
99
+ await Promise.all([atomicWriteJson(cognitionManifestPath(projectRoot), manifest), atomicWriteJson(cognitionPolicyPath(projectRoot), policy)]);
100
+ }
101
+ async function quarantineReadiness(projectRoot, readiness) {
102
+ await mkdir(cognitionQuarantineDirectory(projectRoot), { recursive: true });
103
+ await atomicWriteJson(path.join(cognitionQuarantineDirectory(projectRoot), 'v1-readiness.json'), {
104
+ schemaVersion: 2,
105
+ recordedAt: new Date().toISOString(),
106
+ eligible: readiness.eligible,
107
+ issues: readiness.issues,
108
+ });
109
+ }
110
+ export async function importV1(startPath) {
111
+ const projectRoot = await resolveProjectRoot(startPath);
112
+ return withLock(`${locksDirectory(projectRoot)}/cognition-migration.lock`, async () => {
113
+ const readiness = await migrationReadiness(projectRoot);
114
+ if (!readiness.eligible) {
115
+ const report = { ...readiness, applied: false, importedEventIds: [] };
116
+ await quarantineReadiness(projectRoot, report);
117
+ return report;
118
+ }
119
+ const { records, sourceHash } = await sourceRecords(projectRoot);
120
+ const existingReceipt = await readOptional(cognitionV1ImportPath(projectRoot));
121
+ if (existingReceipt) {
122
+ const receipt = JSON.parse(existingReceipt);
123
+ if (receipt.schemaVersion !== 2 || receipt.sourceHash !== sourceHash)
124
+ throw new Error('v1 source changed after import; v2 historical import is immutable');
125
+ await rebuildProjections(projectRoot);
126
+ await atomicWriteJson(cognitionV1SyncPath(projectRoot), { schemaVersion: 2, fingerprint: await v1Fingerprint(projectRoot), updatedAt: new Date().toISOString() });
127
+ return { ...readiness, applied: true, importedEventIds: receipt.eventIds };
128
+ }
129
+ await writeV2Metadata(projectRoot);
130
+ const importedEventIds = [];
131
+ for (const record of records) {
132
+ const status = record.record.status;
133
+ const event = await appendEvent(projectRoot, {
134
+ type: record.kind === 'context' ? 'context.checkpoint.imported'
135
+ : record.kind === 'note' ? 'note.recorded'
136
+ : record.kind === 'knowledge' ? status === 'verified' ? 'knowledge.verified' : 'knowledge.proposed'
137
+ : status === 'verified' ? 'deadend.verified' : 'deadend.proposed',
138
+ occurredAt: record.occurredAt,
139
+ source: { kind: 'migration' },
140
+ idempotencyKey: `v1:${record.sourceHash}:${record.kind}:${record.id}`,
141
+ git: { worktreeId: null, branch: record.git.branch, ref: record.git.branch, head: record.git.head },
142
+ payload: { record: record.record, sourcePath: record.sourcePath, sourceHash: record.sourceHash, anchorProvenance: record.git.branch === null && record.git.head === null ? 'global' : 'snapshot' },
143
+ });
144
+ importedEventIds.push(event.event.eventId);
145
+ }
146
+ await rebuildProjections(projectRoot);
147
+ await atomicWriteJson(cognitionV1SyncPath(projectRoot), { schemaVersion: 2, fingerprint: await v1Fingerprint(projectRoot), updatedAt: new Date().toISOString() });
148
+ const receipt = { schemaVersion: 2, sourceHash, importedAt: new Date().toISOString(), eventIds: importedEventIds, counts: readiness.counts };
149
+ await atomicWriteJson(cognitionV1ImportPath(projectRoot), receipt);
150
+ return { ...readiness, v2Present: true, applied: true, importedEventIds };
151
+ });
152
+ }
153
+ export async function reconcileV1State(startPath) {
154
+ const projectRoot = await resolveProjectRoot(startPath);
155
+ await withLock(`${locksDirectory(projectRoot)}/cognition-migration.lock`, async () => {
156
+ const readiness = await migrationReadiness(projectRoot);
157
+ if (!readiness.eligible)
158
+ return;
159
+ const { records, sourceHash } = await sourceRecords(projectRoot);
160
+ for (const record of records) {
161
+ const status = record.record.status;
162
+ await appendEvent(projectRoot, {
163
+ type: record.kind === 'context' ? 'context.state.recorded'
164
+ : record.kind === 'note' ? record.record.absorbedBySnapshotId ? 'note.absorbed' : record.record.status === 'resolved' ? 'note.resolved' : 'note.recorded'
165
+ : record.kind === 'knowledge' ? status === 'verified' ? 'knowledge.verified' : 'knowledge.proposed'
166
+ : status === 'verified' ? 'deadend.verified' : 'deadend.proposed',
167
+ occurredAt: record.occurredAt,
168
+ source: { kind: 'migration' },
169
+ idempotencyKey: `v1-live-sync:${sourceHash}:${record.kind}:${record.id}`,
170
+ git: { worktreeId: null, branch: record.git.branch, ref: record.git.branch, head: record.git.head },
171
+ payload: { record: record.record, sourcePath: record.sourcePath, sourceHash: record.sourceHash, anchorProvenance: record.git.branch === null && record.git.head === null ? 'global' : 'snapshot' },
172
+ });
173
+ }
174
+ await rebuildProjections(projectRoot);
175
+ await atomicWriteJson(cognitionV1SyncPath(projectRoot), { schemaVersion: 2, fingerprint: await v1Fingerprint(projectRoot), updatedAt: new Date().toISOString() });
176
+ });
177
+ }
178
+ export async function verifyV1Import(startPath) {
179
+ const projectRoot = await resolveProjectRoot(startPath);
180
+ try {
181
+ const readiness = await migrationReadiness(projectRoot);
182
+ if (!readiness.eligible)
183
+ return { verified: false, issues: readiness.issues };
184
+ const receipt = JSON.parse(await readFile(cognitionV1ImportPath(projectRoot), 'utf8'));
185
+ const { sourceHash } = await sourceRecords(projectRoot);
186
+ if (receipt.sourceHash !== sourceHash)
187
+ return { verified: false, issues: ['v1 source changed after import'] };
188
+ const { projection, cursor } = await rebuildProjections(projectRoot);
189
+ const actual = { contexts: Object.keys(projection.contexts).length, notes: Object.keys(projection.notes).length, knowledge: Object.keys(projection.knowledge).length, deadends: Object.keys(projection.deadends).length };
190
+ const expected = { contexts: readiness.counts.snapshots, notes: readiness.counts.notes, knowledge: readiness.counts.knowledge, deadends: readiness.counts.deadends };
191
+ const issues = Object.entries(expected).flatMap(([key, count]) => actual[key] === count ? [] : [`${key} projection count mismatch`]);
192
+ if (!cursor.lastEventId && receipt.eventIds.length > 0)
193
+ issues.push('projection cursor is missing');
194
+ return { verified: issues.length === 0, issues };
195
+ }
196
+ catch (error) {
197
+ return { verified: false, issues: [error instanceof Error ? error.message : 'invalid v1 import'] };
198
+ }
199
+ }
@@ -0,0 +1,9 @@
1
+ export interface V2OrientationPlan {
2
+ contextId: string;
3
+ snapshotId: string;
4
+ knowledgeIds: ReadonlySet<string>;
5
+ deadendIds: ReadonlySet<string>;
6
+ noteIds: ReadonlySet<string>;
7
+ }
8
+ export declare function selectV2Orientation(projectRoot: string, contextId?: string): Promise<V2OrientationPlan | null>;
9
+ export declare function v2Enabled(projectRoot: string): Promise<boolean>;
@@ -0,0 +1,85 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { readGitWorktreeState } from '../git/git-adapter.js';
3
+ import { cognitionManifestPath, cognitionPolicyPath } from '../storage/layout.js';
4
+ import { readJson } from '../storage/json-store.js';
5
+ import { loadManifest } from '../runtime/init.js';
6
+ import { readJournal } from './journal.js';
7
+ import { projectEvents, workspaceKey } from './projections.js';
8
+ import { v1Synced } from './v1-bridge.js';
9
+ function contextReference(value) {
10
+ const boundary = value.lastIndexOf(':');
11
+ if (boundary <= 0 || boundary === value.length - 1)
12
+ return null;
13
+ return { contextId: value.slice(0, boundary), snapshotId: value.slice(boundary + 1) };
14
+ }
15
+ function record(event) {
16
+ const payload = event.payload;
17
+ return payload.record && typeof payload.record === 'object' && !Array.isArray(payload.record) ? payload.record : null;
18
+ }
19
+ function applicable(event, current) {
20
+ if (event.git.ref === null && event.git.branch === null)
21
+ return true;
22
+ return event.git.ref === current.ref && event.git.branch === current.branch && (event.git.worktreeId === null || event.git.worktreeId === current.worktreeId);
23
+ }
24
+ function latestRecords(events, current, contextId) {
25
+ const knowledge = new Map();
26
+ const deadends = new Map();
27
+ const notes = new Map();
28
+ for (const event of events) {
29
+ const item = record(event);
30
+ if (!item)
31
+ continue;
32
+ if (['knowledge.proposed', 'knowledge.verified'].includes(event.type) && typeof item.knowledgeId === 'string')
33
+ knowledge.set(item.knowledgeId, { event, record: item });
34
+ if (['deadend.proposed', 'deadend.verified'].includes(event.type) && typeof item.deadendId === 'string')
35
+ deadends.set(item.deadendId, { event, record: item });
36
+ if (['note.recorded', 'note.resolved', 'note.absorbed'].includes(event.type) && typeof item.noteId === 'string')
37
+ notes.set(item.noteId, { event, record: item });
38
+ }
39
+ return {
40
+ knowledgeIds: new Set([...knowledge].flatMap(([id, item]) => item.record.status === 'verified' && applicable(item.event, current) ? [id] : [])),
41
+ deadendIds: new Set([...deadends].flatMap(([id, item]) => item.record.status === 'verified' && applicable(item.event, current) ? [id] : [])),
42
+ noteIds: new Set([...notes].flatMap(([id, item]) => item.record.contextId === contextId && item.record.status === 'open' && item.record.absorbedBySnapshotId === null && applicable(item.event, current) ? [id] : [])),
43
+ };
44
+ }
45
+ export async function selectV2Orientation(projectRoot, contextId) {
46
+ try {
47
+ const [manifest, cognition, policy] = await Promise.all([
48
+ loadManifest(projectRoot),
49
+ readJson(cognitionManifestPath(projectRoot)),
50
+ readJson(cognitionPolicyPath(projectRoot)),
51
+ ]);
52
+ if (cognition.schemaVersion !== 2 || cognition.projectId !== manifest.projectId || !policy.enabled || !await v1Synced(projectRoot))
53
+ return null;
54
+ const [events, current] = await Promise.all([readJournal(projectRoot), readGitWorktreeState(projectRoot)]);
55
+ if (events.length === 0)
56
+ return null;
57
+ const projection = projectEvents(events);
58
+ let selected = null;
59
+ if (contextId) {
60
+ const matching = [...Object.keys(projection.contexts)].reverse().find((reference) => contextReference(reference)?.contextId === contextId);
61
+ selected = matching ? contextReference(matching) : null;
62
+ }
63
+ else {
64
+ const exact = projection.workspaces[workspaceKey(current)];
65
+ const legacy = Object.values(projection.workspaces).find((workspace) => workspace.worktreeId === null && (workspace.ref === current.ref || workspace.branch === current.branch));
66
+ const reference = (exact?.activeContextRef ? exact : legacy)?.activeContextRef;
67
+ selected = reference ? contextReference(reference) : null;
68
+ }
69
+ if (!selected || !projection.contexts[`${selected.contextId}:${selected.snapshotId}`])
70
+ return null;
71
+ return { ...selected, ...latestRecords(events, current, selected.contextId) };
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ export async function v2Enabled(projectRoot) {
78
+ try {
79
+ await access(cognitionManifestPath(projectRoot));
80
+ return (await readJson(cognitionPolicyPath(projectRoot))).enabled;
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
@@ -0,0 +1,7 @@
1
+ import type { CognitionEvent, CognitionGitAnchor, CognitionProjection, ProjectionCursor } from './types.js';
2
+ export declare function workspaceKey(anchor: Pick<CognitionGitAnchor, 'worktreeId' | 'branch' | 'ref'>): string;
3
+ export declare function projectEvents(events: CognitionEvent[]): CognitionProjection;
4
+ export declare function rebuildProjections(projectRoot: string): Promise<{
5
+ projection: CognitionProjection;
6
+ cursor: ProjectionCursor;
7
+ }>;
@@ -0,0 +1,127 @@
1
+ import crypto from 'node:crypto';
2
+ import { readdir, rm } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { atomicWriteJson } from '../storage/atomic-write.js';
5
+ import { cognitionBranchesDirectory, cognitionContextsProjectionPath, cognitionDeadendsProjectionPath, cognitionKnowledgeProjectionPath, cognitionNotesProjectionPath, cognitionProjectionCursorPath, } from '../storage/layout.js';
6
+ import { readJournal } from './journal.js';
7
+ export function workspaceKey(anchor) {
8
+ return crypto.createHash('sha256')
9
+ .update(`${anchor.worktreeId ?? 'default'}\0${anchor.ref ?? anchor.branch ?? 'detached'}`)
10
+ .digest('hex')
11
+ .slice(0, 16);
12
+ }
13
+ function workspaceFor(event, existing) {
14
+ const key = workspaceKey(event.git);
15
+ return {
16
+ schemaVersion: 2,
17
+ workspaceKey: key,
18
+ worktreeId: event.git.worktreeId,
19
+ branch: event.git.branch,
20
+ ref: event.git.ref,
21
+ head: event.git.head,
22
+ activeContextRef: existing?.activeContextRef ?? null,
23
+ openObservationEventIds: existing?.openObservationEventIds ?? [],
24
+ pathFocus: existing?.pathFocus ?? [],
25
+ recentEventIds: [...(existing?.recentEventIds ?? []), event.eventId].slice(-50),
26
+ lastActivatedAt: event.occurredAt,
27
+ };
28
+ }
29
+ function recordPayload(event) {
30
+ const payload = event.payload;
31
+ if (!payload.record || typeof payload.record !== 'object' || Array.isArray(payload.record))
32
+ return null;
33
+ return payload;
34
+ }
35
+ function recordId(record, kind) {
36
+ const field = kind === 'note' ? 'noteId' : kind === 'knowledge' ? 'knowledgeId' : 'deadendId';
37
+ return typeof record[field] === 'string' ? record[field] : null;
38
+ }
39
+ function latestOpenObservationIds(events, notes) {
40
+ const latest = new Map();
41
+ for (const event of events) {
42
+ if (!['note.recorded', 'note.resolved', 'note.absorbed'].includes(event.type))
43
+ continue;
44
+ const payload = recordPayload(event);
45
+ const note = payload?.record;
46
+ if (!note)
47
+ continue;
48
+ const noteId = recordId(note, 'note');
49
+ if (!noteId)
50
+ continue;
51
+ notes[noteId] = payload;
52
+ latest.set(noteId, { eventId: event.eventId, record: note });
53
+ }
54
+ return new Set([...latest.values()].flatMap(({ eventId, record }) => record.status === 'open' && record.absorbedBySnapshotId === null ? [eventId] : []));
55
+ }
56
+ export function projectEvents(events) {
57
+ const projection = { schemaVersion: 2, contexts: {}, notes: {}, knowledge: {}, deadends: {}, workspaces: {} };
58
+ const openObservationIds = latestOpenObservationIds(events, projection.notes);
59
+ for (const event of events) {
60
+ const key = workspaceKey(event.git);
61
+ const workspace = workspaceFor(event, projection.workspaces[key]);
62
+ const payload = recordPayload(event);
63
+ const record = payload?.record;
64
+ if ((event.type === 'context.checkpoint.imported' || event.type === 'context.state.recorded') && record) {
65
+ const snapshot = record.snapshot;
66
+ if (snapshot && typeof snapshot.contextId === 'string' && typeof snapshot.snapshotId === 'string') {
67
+ const contextRef = `${snapshot.contextId}:${snapshot.snapshotId}`;
68
+ projection.contexts[contextRef] = payload;
69
+ workspace.activeContextRef = contextRef;
70
+ }
71
+ }
72
+ if (event.type === 'note.recorded' && record) {
73
+ const noteId = recordId(record, 'note');
74
+ if (noteId)
75
+ projection.notes[noteId] = payload;
76
+ }
77
+ if ((event.type === 'knowledge.proposed' || event.type === 'knowledge.verified') && record) {
78
+ const knowledgeId = recordId(record, 'knowledge');
79
+ if (knowledgeId)
80
+ projection.knowledge[knowledgeId] = payload;
81
+ }
82
+ if ((event.type === 'deadend.proposed' || event.type === 'deadend.verified') && record) {
83
+ const deadendId = recordId(record, 'deadend');
84
+ if (deadendId)
85
+ projection.deadends[deadendId] = payload;
86
+ }
87
+ if (event.type === 'note.recorded' && openObservationIds.has(event.eventId)) {
88
+ workspace.openObservationEventIds = [...workspace.openObservationEventIds, event.eventId].slice(-50);
89
+ }
90
+ projection.workspaces[key] = workspace;
91
+ }
92
+ return projection;
93
+ }
94
+ async function removeObsoleteWorkspaceFiles(projectRoot, workspaceKeys) {
95
+ try {
96
+ const files = await readdir(cognitionBranchesDirectory(projectRoot));
97
+ await Promise.all(files
98
+ .filter((file) => file.endsWith('.json') && !workspaceKeys.has(file.slice(0, -'.json'.length)))
99
+ .map((file) => rm(path.join(cognitionBranchesDirectory(projectRoot), file), { force: true })));
100
+ }
101
+ catch (error) {
102
+ if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
103
+ return;
104
+ throw error;
105
+ }
106
+ }
107
+ export async function rebuildProjections(projectRoot) {
108
+ const events = await readJournal(projectRoot);
109
+ const projection = projectEvents(events);
110
+ const last = events.at(-1);
111
+ const cursor = {
112
+ schemaVersion: 2,
113
+ lastEventId: last?.eventId ?? null,
114
+ lastEventHash: last?.hash ?? null,
115
+ updatedAt: last?.observedAt ?? null,
116
+ };
117
+ await removeObsoleteWorkspaceFiles(projectRoot, new Set(Object.keys(projection.workspaces)));
118
+ await Promise.all([
119
+ atomicWriteJson(cognitionContextsProjectionPath(projectRoot), projection.contexts),
120
+ atomicWriteJson(cognitionNotesProjectionPath(projectRoot), projection.notes),
121
+ atomicWriteJson(cognitionKnowledgeProjectionPath(projectRoot), projection.knowledge),
122
+ atomicWriteJson(cognitionDeadendsProjectionPath(projectRoot), projection.deadends),
123
+ ...Object.values(projection.workspaces).map((workspace) => atomicWriteJson(`${cognitionBranchesDirectory(projectRoot)}/${workspace.workspaceKey}.json`, workspace)),
124
+ ]);
125
+ await atomicWriteJson(cognitionProjectionCursorPath(projectRoot), cursor);
126
+ return { projection, cursor };
127
+ }
@@ -0,0 +1,2 @@
1
+ import type { MigrationReadiness } from './types.js';
2
+ export declare function migrationReadiness(startPath?: string): Promise<MigrationReadiness>;
@@ -0,0 +1,81 @@
1
+ import { access, readdir } from 'node:fs/promises';
2
+ import { resolveProjectRoot } from '../project/project-resolver.js';
3
+ import { contextMetadataPath, contextsRoot, cognitionManifestPath, manifestPath, snapshotPath, snapshotsDirectory } from '../storage/layout.js';
4
+ import { readJson } from '../storage/json-store.js';
5
+ import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
6
+ import { listNotes } from '../runtime/notes.js';
7
+ import { rebuildSnapshot } from '../runtime/runtime.js';
8
+ async function exists(filePath) {
9
+ try {
10
+ await access(filePath);
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ export async function migrationReadiness(startPath) {
18
+ const projectRoot = await resolveProjectRoot(startPath);
19
+ const counts = { contexts: 0, snapshots: 0, notes: 0, knowledge: 0, deadends: 0 };
20
+ const issues = [];
21
+ const v1Present = await exists(manifestPath(projectRoot));
22
+ const v2Present = await exists(cognitionManifestPath(projectRoot));
23
+ if (!v1Present) {
24
+ issues.push('v1 manifest is missing');
25
+ return { projectRoot, eligible: false, v1Present, v2Present, counts, issues };
26
+ }
27
+ try {
28
+ const manifest = await readJson(manifestPath(projectRoot));
29
+ if (manifest.schemaVersion !== 1 || manifest.layoutVersion !== 'compact-json-v1' || manifest.projectRoot !== projectRoot) {
30
+ issues.push('v1 manifest is invalid');
31
+ }
32
+ }
33
+ catch (error) {
34
+ issues.push(`v1 manifest: ${error instanceof Error ? error.message : 'invalid'}`);
35
+ }
36
+ let contextIds = [];
37
+ try {
38
+ contextIds = await readdir(contextsRoot(projectRoot));
39
+ }
40
+ catch (error) {
41
+ issues.push(`v1 contexts: ${error instanceof Error ? error.message : 'unavailable'}`);
42
+ }
43
+ for (const contextId of contextIds) {
44
+ try {
45
+ const context = await readJson(contextMetadataPath(projectRoot, contextId));
46
+ if (context.schemaVersion !== 1 || context.id !== contextId || context.projectRoot !== projectRoot) {
47
+ throw new Error('invalid context metadata');
48
+ }
49
+ counts.contexts += 1;
50
+ const snapshots = (await readdir(snapshotsDirectory(projectRoot, contextId))).filter((entry) => entry.endsWith('.json'));
51
+ counts.snapshots += snapshots.length;
52
+ for (const snapshotFile of snapshots) {
53
+ const snapshotId = snapshotFile.slice(0, -'.json'.length);
54
+ await readJson(snapshotPath(projectRoot, contextId, snapshotId));
55
+ await rebuildSnapshot(projectRoot, contextId, snapshotId);
56
+ }
57
+ }
58
+ catch (error) {
59
+ issues.push(`context ${contextId}: ${error instanceof Error ? error.message : 'invalid'}`);
60
+ }
61
+ }
62
+ try {
63
+ counts.notes = (await listNotes(projectRoot, { limit: Number.MAX_SAFE_INTEGER, maxChars: Number.MAX_SAFE_INTEGER })).length;
64
+ }
65
+ catch (error) {
66
+ issues.push(`notes: ${error instanceof Error ? error.message : 'invalid'}`);
67
+ }
68
+ try {
69
+ counts.knowledge = (await listKnowledge(projectRoot, true)).length;
70
+ }
71
+ catch (error) {
72
+ issues.push(`knowledge: ${error instanceof Error ? error.message : 'invalid'}`);
73
+ }
74
+ try {
75
+ counts.deadends = (await listDeadends(projectRoot, true)).length;
76
+ }
77
+ catch (error) {
78
+ issues.push(`deadends: ${error instanceof Error ? error.message : 'invalid'}`);
79
+ }
80
+ return { projectRoot, eligible: issues.length === 0, v1Present, v2Present, counts, issues };
81
+ }
@@ -0,0 +1,12 @@
1
+ import type { RuntimeLease, RuntimeStatus } from './types.js';
2
+ export interface RuntimeServer {
3
+ lease: RuntimeLease;
4
+ close(reason?: string): Promise<void>;
5
+ }
6
+ export declare function serveRuntime(startPath?: string): Promise<RuntimeServer>;
7
+ export declare function runtimeStatus(startPath?: string): Promise<RuntimeStatus>;
8
+ export declare function enableRuntime(startPath?: string): Promise<RuntimeStatus>;
9
+ export declare function disableRuntime(startPath?: string): Promise<RuntimeStatus>;
10
+ export declare function startRuntime(startPath?: string): Promise<RuntimeStatus>;
11
+ export declare function stopRuntime(startPath?: string): Promise<RuntimeStatus>;
12
+ export declare function observeRuntime(startPath?: string): Promise<boolean>;