@ngockhoale/ukit 3.0.11 → 3.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.
- package/CHANGELOG.md +17 -1
- package/README.md +1 -0
- package/manifests/documentation.yaml +12 -0
- package/package.json +1 -1
- package/scripts/bench/data-foundation.mjs +368 -50
- package/src/cli/commands/doctor.js +232 -3
- package/src/cli/commands/feedback.js +64 -1
- package/src/cli/commands/install.js +18 -0
- package/src/cli/commands/memory.js +42 -37
- package/src/cli/commands/telemetry.js +460 -0
- package/src/cli/index.js +7 -0
- package/src/core/agentRuntime/adapters.js +83 -2
- package/src/core/agentRuntime/diagnostics.js +104 -0
- package/src/core/agentRuntime/supervisor.js +137 -0
- package/src/core/agentRuntime/telemetry.js +204 -0
- package/src/core/memory/memoryEmit.js +131 -0
- package/src/core/memory/memoryHit.js +1 -1
- package/src/core/memory/migrate.js +18 -11
- package/src/core/memory/migrateMapping.js +15 -7
- package/src/core/memory/mutateMemory.js +22 -4
- package/src/core/memory/recordIndex.js +10 -3
- package/src/core/memory/recordStore.js +28 -3
- package/src/core/memory/retrieval.js +79 -38
- package/src/core/memory/store.js +37 -37
- package/src/core/memory/storeV2.js +28 -25
- package/src/core/memory/storeV2Loader.js +2 -2
- package/src/core/observability/adapters/ingest.js +576 -0
- package/src/core/observability/analytics/anomalies.js +415 -0
- package/src/core/observability/analytics/summary.js +16 -1
- package/src/core/observability/emit/config.js +69 -1
- package/src/core/observability/emit/crash.js +434 -0
- package/src/core/observability/emit/lifecycle.js +349 -0
- package/src/core/observability/emit/recorder.js +135 -9
- package/src/core/observability/evaluation/aiPacket.js +52 -10
- package/src/core/observability/evaluation/outcomes.js +95 -0
- package/src/core/observability/evaluation/runner.js +225 -0
- package/src/core/observability/privacy/allowlist.js +23 -3
- package/src/core/observability/schema/compatibility.js +48 -3
- package/src/core/observability/schema/constants.js +5 -0
- package/src/core/observability/schema/registry.js +57 -0
- package/src/core/observability/schema/validate.js +68 -6
- package/src/core/observability/segments/internal.js +42 -8
- package/src/core/observability/segments/readSegments.js +35 -1
- package/src/core/observability/segments/recovery.js +3 -2
- package/src/core/observability/segments/retention.js +137 -33
- package/src/core/observability/support/projector.js +88 -18
- package/src/core/observability/support/provision.js +160 -0
- package/src/core/observability/support/renderer.js +2 -2
- package/src/core/observability/support/schedule.js +174 -0
- package/template_project/.claude/hooks/auto-allow-bash.sh +7 -1
- package/template_project/.claude/hooks/auto-prune-bash.sh +16 -7
- package/template_project/.claude/hooks/verification-guard.sh +13 -4
- package/template_project/.claude/ukit/runtime/async-lock.mjs +26 -0
package/src/core/memory/store.js
CHANGED
|
@@ -19,9 +19,9 @@ async function storeV2() {
|
|
|
19
19
|
return import('./storeV2.js');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
async function v2Active(projectRoot) {
|
|
22
|
+
async function v2Active(projectRoot, { homeDir } = {}) {
|
|
23
23
|
try {
|
|
24
|
-
const config = await loadRuntimeConfig(projectRoot);
|
|
24
|
+
const config = await loadRuntimeConfig(projectRoot, { homeDir });
|
|
25
25
|
if (config?.memoryV2?.enabled === false) return false;
|
|
26
26
|
} catch {
|
|
27
27
|
// unreadable config → default enabled, keep going
|
|
@@ -30,7 +30,7 @@ async function v2Active(projectRoot) {
|
|
|
30
30
|
const v2 = await storeV2();
|
|
31
31
|
// loadRecords triggers ensureMigrated (lazy migration) when autoMigrate is
|
|
32
32
|
// on; records.json existing afterwards — or already — means v2 mode.
|
|
33
|
-
await v2.loadRecords(projectRoot);
|
|
33
|
+
await v2.loadRecords(projectRoot, { homeDir });
|
|
34
34
|
try {
|
|
35
35
|
await fs.access(buildRuntimePaths(projectRoot).memoryV2RecordsPath);
|
|
36
36
|
return true;
|
|
@@ -271,15 +271,15 @@ function createSearchableText(type, content) {
|
|
|
271
271
|
].filter(Boolean).join('\n');
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
export async function exportMemory(projectRoot) {
|
|
274
|
+
export async function exportMemory(projectRoot, { homeDir } = {}) {
|
|
275
275
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
276
276
|
// FR-023/025: export is a bulk read lane — redact at the store boundary so
|
|
277
277
|
// every caller (CLI, future lanes) gets defense-in-depth, not just the CLI.
|
|
278
|
-
const config = await loadRuntimeConfig(projectRoot);
|
|
278
|
+
const config = await loadRuntimeConfig(projectRoot, { homeDir });
|
|
279
279
|
const redact = (data) => redactWritePayload({ meta: data }, { config }).payload.meta;
|
|
280
|
-
if (await v2Active(projectRoot)) {
|
|
280
|
+
if (await v2Active(projectRoot, { homeDir })) {
|
|
281
281
|
const v2 = await storeV2();
|
|
282
|
-
const items = recordsToMemoryItems(await v2.loadRecords(projectRoot), runtimePaths);
|
|
282
|
+
const items = recordsToMemoryItems(await v2.loadRecords(projectRoot, { homeDir }), runtimePaths);
|
|
283
283
|
const user = items.find((item) => item.type === 'user')?.content ?? defaultUserMemory();
|
|
284
284
|
return redact({
|
|
285
285
|
user,
|
|
@@ -314,11 +314,11 @@ async function warnOnCorruptLegacyFiles(runtimePaths) {
|
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
export async function listMemoryItems(projectRoot) {
|
|
317
|
+
export async function listMemoryItems(projectRoot, { homeDir } = {}) {
|
|
318
318
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
319
|
-
if (await v2Active(projectRoot)) {
|
|
319
|
+
if (await v2Active(projectRoot, { homeDir })) {
|
|
320
320
|
const v2 = await storeV2();
|
|
321
|
-
const records = await v2.loadRecords(projectRoot);
|
|
321
|
+
const records = await v2.loadRecords(projectRoot, { homeDir });
|
|
322
322
|
await warnOnCorruptLegacyFiles(runtimePaths);
|
|
323
323
|
return recordsToMemoryItems(records, runtimePaths);
|
|
324
324
|
}
|
|
@@ -355,7 +355,7 @@ export async function listMemoryItems(projectRoot) {
|
|
|
355
355
|
];
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
async function forgetMemoryItemV2(projectRoot, memoryId, runtimePaths) {
|
|
358
|
+
async function forgetMemoryItemV2(projectRoot, memoryId, runtimePaths, { homeDir } = {}) {
|
|
359
359
|
const v2 = await storeV2();
|
|
360
360
|
const { mutateMemory } = await import('./mutateMemory.js');
|
|
361
361
|
const archiveAll = async (records) => {
|
|
@@ -363,7 +363,7 @@ async function forgetMemoryItemV2(projectRoot, memoryId, runtimePaths) {
|
|
|
363
363
|
for (const record of records) {
|
|
364
364
|
if (record.status !== 'archived') {
|
|
365
365
|
const res = await mutateMemory(
|
|
366
|
-
{ kind: 'project', projectRoot },
|
|
366
|
+
{ kind: 'project', projectRoot, homeDir },
|
|
367
367
|
{ op: 'forget', payload: { id: record.id } },
|
|
368
368
|
);
|
|
369
369
|
if (res.status === 'ok') archived += 1;
|
|
@@ -373,7 +373,7 @@ async function forgetMemoryItemV2(projectRoot, memoryId, runtimePaths) {
|
|
|
373
373
|
};
|
|
374
374
|
|
|
375
375
|
if (memoryId === 'user:user' || memoryId === 'user') {
|
|
376
|
-
const userRecords = await v2.queryRecords(projectRoot, { scope: 'user' });
|
|
376
|
+
const userRecords = await v2.queryRecords(projectRoot, { scope: 'user', homeDir });
|
|
377
377
|
await archiveAll(userRecords);
|
|
378
378
|
await writeJson(runtimePaths.userMemoryPath, defaultUserMemory());
|
|
379
379
|
// The reset above always removes user memory — `removed` reports that the
|
|
@@ -392,7 +392,7 @@ async function forgetMemoryItemV2(projectRoot, memoryId, runtimePaths) {
|
|
|
392
392
|
return { removed: false, type, path: null };
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
const records = await v2.loadRecords(projectRoot);
|
|
395
|
+
const records = await v2.loadRecords(projectRoot, { homeDir });
|
|
396
396
|
const matched = type === 'session'
|
|
397
397
|
? records.filter((r) => r.type === 'episode' && (r.meta?.legacyId === rawId || r.id === rawId))
|
|
398
398
|
: type === 'project'
|
|
@@ -420,10 +420,10 @@ async function forgetMemoryItemV2(projectRoot, memoryId, runtimePaths) {
|
|
|
420
420
|
return { removed: archived > 0 || legacyRemoved, type: baseDir ? type : null, path: null };
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
-
export async function forgetMemoryItem(projectRoot, memoryId) {
|
|
423
|
+
export async function forgetMemoryItem(projectRoot, memoryId, { homeDir } = {}) {
|
|
424
424
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
425
|
-
if (await v2Active(projectRoot)) {
|
|
426
|
-
return forgetMemoryItemV2(projectRoot, memoryId, runtimePaths);
|
|
425
|
+
if (await v2Active(projectRoot, { homeDir })) {
|
|
426
|
+
return forgetMemoryItemV2(projectRoot, memoryId, runtimePaths, { homeDir });
|
|
427
427
|
}
|
|
428
428
|
if (memoryId === 'user:user' || memoryId === 'user') {
|
|
429
429
|
await writeJson(runtimePaths.userMemoryPath, defaultUserMemory());
|
|
@@ -576,14 +576,14 @@ function patternCandidateFromRecord(record) {
|
|
|
576
576
|
};
|
|
577
577
|
}
|
|
578
578
|
|
|
579
|
-
async function proposePatternCandidateV2(projectRoot, projectId, candidate) {
|
|
579
|
+
async function proposePatternCandidateV2(projectRoot, projectId, candidate, { homeDir } = {}) {
|
|
580
580
|
const v2 = await storeV2();
|
|
581
581
|
const normalizedText = normalizePatternText(candidate?.text);
|
|
582
582
|
if (!normalizedText) {
|
|
583
583
|
throw new Error('proposePatternCandidate requires candidate.text');
|
|
584
584
|
}
|
|
585
585
|
|
|
586
|
-
const existing = (await v2.queryRecords(projectRoot, { projectId }))
|
|
586
|
+
const existing = (await v2.queryRecords(projectRoot, { projectId, homeDir }))
|
|
587
587
|
.filter((record) => record.provenance === 'pattern-candidate'
|
|
588
588
|
|| String(record.provenance ?? '').includes('pattern-candidate'))
|
|
589
589
|
.find((record) => record.meta?.legacyStatus === 'pending'
|
|
@@ -612,13 +612,13 @@ async function proposePatternCandidateV2(projectRoot, projectId, candidate) {
|
|
|
612
612
|
createdBy: 'propose',
|
|
613
613
|
projectId,
|
|
614
614
|
meta: { ...entry, legacyStatus: 'pending' },
|
|
615
|
-
});
|
|
615
|
+
}, { homeDir });
|
|
616
616
|
return entry;
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
-
async function listPendingPatternCandidatesV2(projectRoot, projectId) {
|
|
619
|
+
async function listPendingPatternCandidatesV2(projectRoot, projectId, { homeDir } = {}) {
|
|
620
620
|
const v2 = await storeV2();
|
|
621
|
-
return (await v2.queryRecords(projectRoot, { projectId }))
|
|
621
|
+
return (await v2.queryRecords(projectRoot, { projectId, homeDir }))
|
|
622
622
|
.filter((record) => record.type === 'derived_fact'
|
|
623
623
|
&& record.status !== 'archived'
|
|
624
624
|
&& (record.meta?.legacyStatus === 'pending'
|
|
@@ -628,9 +628,9 @@ async function listPendingPatternCandidatesV2(projectRoot, projectId) {
|
|
|
628
628
|
.map(patternCandidateFromRecord);
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
-
async function resolvePatternCandidateV2(projectRoot, projectId, candidateId, decision) {
|
|
631
|
+
async function resolvePatternCandidateV2(projectRoot, projectId, candidateId, decision, { homeDir } = {}) {
|
|
632
632
|
const v2 = await storeV2();
|
|
633
|
-
const records = await v2.queryRecords(projectRoot, { projectId });
|
|
633
|
+
const records = await v2.queryRecords(projectRoot, { projectId, homeDir });
|
|
634
634
|
// Only candidate records are resolvable — matching every record by meta.id
|
|
635
635
|
// let a delta-overlay (meta.id 'ov_*') be "approved" into a project_rule or
|
|
636
636
|
// archived, corrupting the overlay store.
|
|
@@ -658,7 +658,7 @@ async function resolvePatternCandidateV2(projectRoot, projectId, candidateId, de
|
|
|
658
658
|
// Keep the C11 status field in sync for learning candidates.
|
|
659
659
|
...(isLearningCandidate ? { status: 'approved' } : {}),
|
|
660
660
|
},
|
|
661
|
-
});
|
|
661
|
+
}, { homeDir });
|
|
662
662
|
} else {
|
|
663
663
|
updated = await v2.updateRecord(projectRoot, target.id, {
|
|
664
664
|
status: 'archived',
|
|
@@ -667,15 +667,15 @@ async function resolvePatternCandidateV2(projectRoot, projectId, candidateId, de
|
|
|
667
667
|
legacyStatus: 'rejected',
|
|
668
668
|
...(isLearningCandidate ? { status: 'rejected' } : {}),
|
|
669
669
|
},
|
|
670
|
-
});
|
|
670
|
+
}, { homeDir });
|
|
671
671
|
}
|
|
672
672
|
return patternCandidateFromRecord(updated ?? target);
|
|
673
673
|
}
|
|
674
674
|
|
|
675
|
-
export async function proposePatternCandidate(projectRoot, projectId, candidate) {
|
|
675
|
+
export async function proposePatternCandidate(projectRoot, projectId, candidate, { homeDir } = {}) {
|
|
676
676
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
677
|
-
if (await v2Active(projectRoot)) {
|
|
678
|
-
const entry = await proposePatternCandidateV2(projectRoot, projectId, candidate);
|
|
677
|
+
if (await v2Active(projectRoot, { homeDir })) {
|
|
678
|
+
const entry = await proposePatternCandidateV2(projectRoot, projectId, candidate, { homeDir });
|
|
679
679
|
// Mirror into the legacy project file (hygiene + conventions) so the
|
|
680
680
|
// untouched-on-disk legacy format stays read-compatible.
|
|
681
681
|
const { filePath, memory } = await readProjectMemoryForId(runtimePaths, projectId);
|
|
@@ -719,10 +719,10 @@ export async function proposePatternCandidate(projectRoot, projectId, candidate)
|
|
|
719
719
|
return entry;
|
|
720
720
|
}
|
|
721
721
|
|
|
722
|
-
export async function listPendingPatternCandidates(projectRoot, projectId) {
|
|
722
|
+
export async function listPendingPatternCandidates(projectRoot, projectId, { homeDir } = {}) {
|
|
723
723
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
724
|
-
if (await v2Active(projectRoot)) {
|
|
725
|
-
return listPendingPatternCandidatesV2(projectRoot, projectId);
|
|
724
|
+
if (await v2Active(projectRoot, { homeDir })) {
|
|
725
|
+
return listPendingPatternCandidatesV2(projectRoot, projectId, { homeDir });
|
|
726
726
|
}
|
|
727
727
|
|
|
728
728
|
const { memory } = await readProjectMemoryForId(runtimePaths, projectId);
|
|
@@ -730,14 +730,14 @@ export async function listPendingPatternCandidates(projectRoot, projectId) {
|
|
|
730
730
|
return patternCandidates.filter((entry) => entry.status === 'pending');
|
|
731
731
|
}
|
|
732
732
|
|
|
733
|
-
export async function resolvePatternCandidate(projectRoot, projectId, candidateId, decision) {
|
|
733
|
+
export async function resolvePatternCandidate(projectRoot, projectId, candidateId, decision, { homeDir } = {}) {
|
|
734
734
|
if (decision !== 'approve' && decision !== 'reject') {
|
|
735
735
|
throw new Error(`Unknown decision: ${decision}`);
|
|
736
736
|
}
|
|
737
737
|
|
|
738
738
|
const runtimePaths = buildRuntimePaths(projectRoot);
|
|
739
|
-
if (await v2Active(projectRoot)) {
|
|
740
|
-
const resolved = await resolvePatternCandidateV2(projectRoot, projectId, candidateId, decision);
|
|
739
|
+
if (await v2Active(projectRoot, { homeDir })) {
|
|
740
|
+
const resolved = await resolvePatternCandidateV2(projectRoot, projectId, candidateId, decision, { homeDir });
|
|
741
741
|
// Mirror the resolution into the legacy project file.
|
|
742
742
|
const { filePath, memory } = await readProjectMemoryForId(runtimePaths, projectId);
|
|
743
743
|
const patternCandidates = Array.isArray(memory.patternCandidates) ? memory.patternCandidates : [];
|
|
@@ -781,8 +781,8 @@ export async function resolvePatternCandidate(projectRoot, projectId, candidateI
|
|
|
781
781
|
return target;
|
|
782
782
|
}
|
|
783
783
|
|
|
784
|
-
export async function countMemoryItems(projectRoot) {
|
|
785
|
-
const exported = await exportMemory(projectRoot);
|
|
784
|
+
export async function countMemoryItems(projectRoot, { homeDir } = {}) {
|
|
785
|
+
const exported = await exportMemory(projectRoot, { homeDir });
|
|
786
786
|
return {
|
|
787
787
|
projectCount: exported.projects.length,
|
|
788
788
|
sessionCount: exported.sessions.length,
|
|
@@ -26,8 +26,11 @@ const migrationPromises = new Map();
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
async function ensureMigrated(projectRoot) {
|
|
30
|
-
|
|
29
|
+
async function ensureMigrated(projectRoot, { homeDir } = {}) {
|
|
30
|
+
// Keyed on root+home: the registry consulted for alias-collision checks is
|
|
31
|
+
// per-home, so identical roots under different homes must not share an
|
|
32
|
+
// in-flight migration.
|
|
33
|
+
const key = `${String(projectRoot)}::${String(homeDir ?? '')}`;
|
|
31
34
|
const existing = migrationPromises.get(key);
|
|
32
35
|
if (existing) {
|
|
33
36
|
await existing.promise;
|
|
@@ -37,7 +40,7 @@ async function ensureMigrated(projectRoot) {
|
|
|
37
40
|
const entry = { promise: null, failed: false };
|
|
38
41
|
entry.promise = (async () => {
|
|
39
42
|
try {
|
|
40
|
-
const config = await loadRuntimeConfig(projectRoot);
|
|
43
|
+
const config = await loadRuntimeConfig(projectRoot, { homeDir });
|
|
41
44
|
if (config?.memoryV2?.autoMigrate === false) return;
|
|
42
45
|
|
|
43
46
|
const migrate = await import('./migrate.js');
|
|
@@ -45,7 +48,7 @@ async function ensureMigrated(projectRoot) {
|
|
|
45
48
|
// records.json stranded v1 data forever whenever the file existed
|
|
46
49
|
// without the marker (a pre-migration write, a crashed first run).
|
|
47
50
|
if (await migrate.needsMigration(projectRoot)) {
|
|
48
|
-
await migrate.runMigration(projectRoot);
|
|
51
|
+
await migrate.runMigration(projectRoot, { homeDir });
|
|
49
52
|
}
|
|
50
53
|
} catch {
|
|
51
54
|
// Lazy migration is best-effort: a failed auto-run must not break reads.
|
|
@@ -67,16 +70,16 @@ async function ensureMigrated(projectRoot) {
|
|
|
67
70
|
* 'migrating' (auto-migration in-flight or failed), otherwise the
|
|
68
71
|
* readRecordStore state ('ok'|'empty'|'missing'|'corrupt').
|
|
69
72
|
*/
|
|
70
|
-
export async function loadRecordsDetailed(projectRoot) {
|
|
73
|
+
export async function loadRecordsDetailed(projectRoot, { homeDir } = {}) {
|
|
71
74
|
try {
|
|
72
|
-
const config = await loadRuntimeConfig(projectRoot);
|
|
75
|
+
const config = await loadRuntimeConfig(projectRoot, { homeDir });
|
|
73
76
|
if (config?.memoryV2?.enabled === false) {
|
|
74
77
|
return { state: 'disabled', records: [] };
|
|
75
78
|
}
|
|
76
79
|
} catch {
|
|
77
80
|
// unreadable config → default enabled, keep going
|
|
78
81
|
}
|
|
79
|
-
const migration = await ensureMigrated(projectRoot);
|
|
82
|
+
const migration = await ensureMigrated(projectRoot, { homeDir });
|
|
80
83
|
if (migration.failed) {
|
|
81
84
|
return { state: 'migrating', records: [] };
|
|
82
85
|
}
|
|
@@ -89,8 +92,8 @@ export async function loadRecordsDetailed(projectRoot) {
|
|
|
89
92
|
/**
|
|
90
93
|
* loadRecords(projectRoot) → record[] — tolerant: invalid entries skipped.
|
|
91
94
|
*/
|
|
92
|
-
export async function loadRecords(projectRoot) {
|
|
93
|
-
const { records } = await loadRecordsDetailed(projectRoot);
|
|
95
|
+
export async function loadRecords(projectRoot, { homeDir } = {}) {
|
|
96
|
+
const { records } = await loadRecordsDetailed(projectRoot, { homeDir });
|
|
94
97
|
return records;
|
|
95
98
|
}
|
|
96
99
|
|
|
@@ -103,10 +106,10 @@ async function readStore(projectRoot) {
|
|
|
103
106
|
* saveRecords(projectRoot, records) → void — explicit restore path
|
|
104
107
|
* (mutateMemory op:'restore'); drops unrecoverable entries.
|
|
105
108
|
*/
|
|
106
|
-
export async function saveRecords(projectRoot, records) {
|
|
107
|
-
await ensureMigrated(projectRoot);
|
|
109
|
+
export async function saveRecords(projectRoot, records, { homeDir } = {}) {
|
|
110
|
+
await ensureMigrated(projectRoot, { homeDir });
|
|
108
111
|
const res = await mutateMemory(
|
|
109
|
-
{ kind: 'project', projectRoot },
|
|
112
|
+
{ kind: 'project', projectRoot, homeDir },
|
|
110
113
|
{ op: 'restore', payload: { records } },
|
|
111
114
|
);
|
|
112
115
|
if (res.status === 'rejected' || res.status === 'conflict' || res.status === 'corrupt') {
|
|
@@ -120,16 +123,16 @@ export async function saveRecords(projectRoot, records) {
|
|
|
120
123
|
* Runs ensureMigrated first — a first v2 write must not create records.json
|
|
121
124
|
* ahead of the v1→v2 migration and strand legacy data.
|
|
122
125
|
*/
|
|
123
|
-
export async function addRecord(projectRoot, recordInput) {
|
|
126
|
+
export async function addRecord(projectRoot, recordInput, { homeDir } = {}) {
|
|
124
127
|
const record = isRawRecord(recordInput)
|
|
125
128
|
? normalizeRecord(recordInput)
|
|
126
129
|
: createRecord(recordInput);
|
|
127
130
|
if (!record) {
|
|
128
131
|
throw new Error('addRecord: input could not be normalized into a valid record');
|
|
129
132
|
}
|
|
130
|
-
await ensureMigrated(projectRoot);
|
|
133
|
+
await ensureMigrated(projectRoot, { homeDir });
|
|
131
134
|
const res = await mutateMemory(
|
|
132
|
-
{ kind: 'project', projectRoot },
|
|
135
|
+
{ kind: 'project', projectRoot, homeDir },
|
|
133
136
|
{ op: 'add', payload: record },
|
|
134
137
|
);
|
|
135
138
|
if (res.status === 'ok' || res.status === 'duplicate') return res.record ?? record;
|
|
@@ -140,10 +143,10 @@ export async function addRecord(projectRoot, recordInput) {
|
|
|
140
143
|
/**
|
|
141
144
|
* updateRecord(projectRoot, id, patch) → record | null
|
|
142
145
|
*/
|
|
143
|
-
export async function updateRecord(projectRoot, id, patch = {}) {
|
|
144
|
-
await ensureMigrated(projectRoot);
|
|
146
|
+
export async function updateRecord(projectRoot, id, patch = {}, { homeDir } = {}) {
|
|
147
|
+
await ensureMigrated(projectRoot, { homeDir });
|
|
145
148
|
const res = await mutateMemory(
|
|
146
|
-
{ kind: 'project', projectRoot },
|
|
149
|
+
{ kind: 'project', projectRoot, homeDir },
|
|
147
150
|
{ op: 'update', payload: { id, patch } },
|
|
148
151
|
);
|
|
149
152
|
if (res.status === 'ok') return res.record;
|
|
@@ -154,17 +157,17 @@ export async function updateRecord(projectRoot, id, patch = {}) {
|
|
|
154
157
|
/**
|
|
155
158
|
* getRecord(projectRoot, id) → record | null
|
|
156
159
|
*/
|
|
157
|
-
export async function getRecord(projectRoot, id) {
|
|
158
|
-
await ensureMigrated(projectRoot);
|
|
160
|
+
export async function getRecord(projectRoot, id, { homeDir } = {}) {
|
|
161
|
+
await ensureMigrated(projectRoot, { homeDir });
|
|
159
162
|
const { records } = await readStore(projectRoot);
|
|
160
163
|
return records.find((r) => r.id === id) ?? null;
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
/**
|
|
164
|
-
* queryRecords(projectRoot, { type?, scope?, status?, projectId? }) → record[]
|
|
167
|
+
* queryRecords(projectRoot, { type?, scope?, status?, projectId?, homeDir? }) → record[]
|
|
165
168
|
*/
|
|
166
|
-
export async function queryRecords(projectRoot, { type, scope, status, projectId } = {}) {
|
|
167
|
-
const records = await loadRecords(projectRoot);
|
|
169
|
+
export async function queryRecords(projectRoot, { type, scope, status, projectId, homeDir } = {}) {
|
|
170
|
+
const records = await loadRecords(projectRoot, { homeDir });
|
|
168
171
|
return records.filter((r) => (type == null || r.type === type)
|
|
169
172
|
&& (scope == null || r.scope === scope)
|
|
170
173
|
&& (status == null || r.status === status)
|
|
@@ -174,8 +177,8 @@ export async function queryRecords(projectRoot, { type, scope, status, projectId
|
|
|
174
177
|
/**
|
|
175
178
|
* stats(projectRoot) → { total, byType, byStatus, invalidSkipped }
|
|
176
179
|
*/
|
|
177
|
-
export async function stats(projectRoot) {
|
|
178
|
-
await ensureMigrated(projectRoot);
|
|
180
|
+
export async function stats(projectRoot, { homeDir } = {}) {
|
|
181
|
+
await ensureMigrated(projectRoot, { homeDir });
|
|
179
182
|
const { records, invalidSkipped } = await readStore(projectRoot);
|
|
180
183
|
const byType = {};
|
|
181
184
|
const byStatus = {};
|
|
@@ -15,7 +15,7 @@ import { resolveMemoryStage } from './memoryFlags.js';
|
|
|
15
15
|
* unlisted canary project) reports 'missing' so callers take the legacy
|
|
16
16
|
* lane exactly as if the v2 store were absent.
|
|
17
17
|
*/
|
|
18
|
-
export async function loadV2Records(projectRoot, filter = {}, { config, projectId } = {}) {
|
|
18
|
+
export async function loadV2Records(projectRoot, filter = {}, { config, projectId, homeDir } = {}) {
|
|
19
19
|
if (config && resolveMemoryStage(config, 'eligibility', { projectId }) === 'off') {
|
|
20
20
|
return { state: 'missing', records: [] };
|
|
21
21
|
}
|
|
@@ -25,7 +25,7 @@ export async function loadV2Records(projectRoot, filter = {}, { config, projectI
|
|
|
25
25
|
|| typeof storeV2?.loadRecordsDetailed !== 'function') {
|
|
26
26
|
return { state: 'missing', records: [] };
|
|
27
27
|
}
|
|
28
|
-
const { state, records, generation } = await storeV2.loadRecordsDetailed(projectRoot);
|
|
28
|
+
const { state, records, generation } = await storeV2.loadRecordsDetailed(projectRoot, { homeDir });
|
|
29
29
|
const { type, scope, status, projectId } = filter ?? {};
|
|
30
30
|
const filtered = (Array.isArray(records) ? records : []).filter((r) => (
|
|
31
31
|
(type == null || r.type === type)
|