@ngockhoale/ukit 3.0.8 → 3.0.9
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 +14 -1
- package/manifests/documentation.yaml +11 -0
- package/package.json +1 -1
- package/scripts/audit/decision-coverage.mjs +29 -2
- package/scripts/bench/data-foundation.mjs +52 -3
- package/scripts/bench/decision-runtime-baseline.mjs +427 -0
- package/scripts/bench/decision-runtime-metrics.mjs +67 -0
- package/scripts/bench/decision-runtime-variant.mjs +626 -0
- package/scripts/bench/memory-ablation.mjs +495 -0
- package/scripts/bench/memory-baseline.mjs +596 -0
- package/scripts/bench/memory-bench.mjs +661 -0
- package/scripts/bench/memory-canary.mjs +321 -0
- package/scripts/bench/memory-corpus.mjs +354 -0
- package/scripts/bench/memory-gate.mjs +389 -0
- package/scripts/bench/memory-metrics.mjs +179 -0
- package/scripts/bench/parallel-agents.mjs +33 -11
- package/scripts/bench/recorder-overhead.mjs +204 -0
- package/scripts/bench/sqlite-spike.mjs +451 -0
- package/scripts/measure-decision-gateway.mjs +306 -0
- package/scripts/perf/audit-perf.mjs +35 -17
- package/src/bug/triageBug.js +4 -3
- package/src/cli/commands/memory.js +357 -63
- package/src/context/detectProjectContext.js +11 -1
- package/src/core/agentRuntime/adapters.js +254 -0
- package/src/core/agentRuntime/artifacts.js +192 -0
- package/src/core/agentRuntime/completionGate.js +176 -0
- package/src/core/agentRuntime/context.js +149 -0
- package/src/core/agentRuntime/contract.js +247 -0
- package/src/core/agentRuntime/diagnostics.js +244 -0
- package/src/core/agentRuntime/evaluation.js +163 -0
- package/src/core/agentRuntime/eventStore.js +404 -0
- package/src/core/agentRuntime/liveness.js +60 -0
- package/src/core/agentRuntime/planCompiler.js +322 -0
- package/src/core/agentRuntime/promotion.js +53 -0
- package/src/core/agentRuntime/qualityComparison.js +112 -0
- package/src/core/agentRuntime/recovery.js +266 -0
- package/src/core/agentRuntime/resourcePolicy.js +78 -0
- package/src/core/agentRuntime/runtimeSupport.js +237 -0
- package/src/core/agentRuntime/supervisor.js +565 -0
- package/src/core/agentRuntime/vmEngine.js +621 -0
- package/src/core/codeintel/analogy.js +3 -2
- package/src/core/experiments/dynamicWorkflow.js +17 -2
- package/src/core/fileOps.js +21 -3
- package/src/core/memory/deltaOverlays.js +75 -30
- package/src/core/memory/learningCandidates.js +93 -48
- package/src/core/memory/memoryFlags.js +83 -0
- package/src/core/memory/memoryFreshness.js +190 -0
- package/src/core/memory/memoryHit.js +144 -0
- package/src/core/memory/migrate.js +69 -189
- package/src/core/memory/migrateMapping.js +232 -0
- package/src/core/memory/mutateMemory.js +323 -0
- package/src/core/memory/policy.js +96 -0
- package/src/core/memory/projectIdentity.js +266 -0
- package/src/core/memory/recordIndex.js +178 -0
- package/src/core/memory/recordStore.js +133 -20
- package/src/core/memory/records.js +144 -6
- package/src/core/memory/retrieval.js +259 -125
- package/src/core/memory/store.js +16 -5
- package/src/core/memory/storeBackup.js +226 -0
- package/src/core/memory/storeV2.js +63 -26
- package/src/core/memory/storeV2Loader.js +30 -12
- package/src/core/memory/userMemory.js +38 -20
- package/src/core/memory/writeClassification.js +161 -0
- package/src/core/memory/writeGuard.js +129 -0
- package/src/core/observability/adapters/hookTelemetryAdapter.js +90 -0
- package/src/core/observability/analytics/cohorts.js +148 -0
- package/src/core/observability/analytics/storeDigest.js +163 -0
- package/src/core/observability/evaluation/experimentPlan.js +95 -0
- package/src/core/observability/evaluation/findings.js +99 -0
- package/src/core/observability/evaluation/optimizationKnowledge.js +10 -1
- package/src/core/observability/evaluation/perturbation.js +273 -0
- package/src/core/observability/evaluation/replay.js +7 -1
- package/src/core/observability/evaluation/scorecard.js +23 -3
- package/src/core/observability/rollout.js +11 -7
- package/src/core/observability/schema/compatibility.js +135 -0
- package/src/core/observability/schema/registry.js +99 -0
- package/src/core/observability/schema/validate.js +7 -0
- package/src/core/observability/support/import.js +53 -9
- package/src/core/observability/support/paths.js +13 -3
- package/src/core/observability/support/projector.js +148 -12
- package/src/core/output/index.js +12 -2
- package/src/core/runtimeConfig.js +83 -0
- package/src/core/runtimePaths.js +3 -0
- package/src/core/sensitiveValueScanner.js +40 -0
- package/src/core/token/index.js +40 -3
- package/src/decision/client.js +37 -13
- package/src/decision/protocol.js +1 -1
- package/src/decision/registry.js +5 -3
- package/src/decision/runtimeDecide.js +242 -0
- package/src/decision/runtimeFilter.js +150 -0
- package/src/decision/runtimeScheduler.js +239 -0
- package/src/index/buildIndex.js +13 -12
- package/src/index/queryIndex.js +35 -14
- package/src/index/relatedTests.js +50 -8
- package/src/index/resolveContext.js +9 -4
- package/src/manifest/selectItems.js +7 -3
- package/src/render/instructionRenderer.js +17 -5
- package/template_project/.claude/ukit/index/lib/index-core.mjs +94 -39
- package/template_project/.claude/ukit/index/route-task.mjs +121 -19
- package/template_project/.claude/ukit/index/unic-decision.mjs +28 -13
- package/template_project/.claude/ukit/runtime/memory-flags.mjs +51 -0
- package/template_project/.claude/ukit/runtime/memory-freshness.mjs +155 -0
- package/template_project/.claude/ukit/runtime/memory-policy.mjs +286 -0
- package/template_project/.claude/ukit/runtime/output-compression.mjs +3 -0
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +145 -14
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// storeBackup — operator lane over whole v2 store docs (SPEC §5 FR-008/FR-009).
|
|
2
|
+
// createBackup snapshots records.json copies + a sha256 manifest into a
|
|
3
|
+
// timestamped dir under <memoryRoot>/backups/ (or explicit outDir). Every
|
|
4
|
+
// restore write still commits through mutateMemory op 'restore', so guard
|
|
5
|
+
// redaction, fingerprint dedupe, and tombstone precedence are never
|
|
6
|
+
// duplicated here. diagnoseStore is strictly read-only — it never triggers
|
|
7
|
+
// the quarantine path that a mutation would.
|
|
8
|
+
//
|
|
9
|
+
// Backup copies may contain secrets → mode 0600, same boundary as the
|
|
10
|
+
// canonical store and quarantine copies (SPEC §14).
|
|
11
|
+
|
|
12
|
+
import crypto from 'node:crypto';
|
|
13
|
+
import fs from 'node:fs/promises';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { ensureDir, pathExists, readJsonIfExists, writeJson } from '../fileOps.js';
|
|
17
|
+
import { buildRuntimePaths } from '../runtimePaths.js';
|
|
18
|
+
import { buildUserPaths } from '../userPaths.js';
|
|
19
|
+
import { mutateMemory } from './mutateMemory.js';
|
|
20
|
+
import { readRecordStore } from './recordStore.js';
|
|
21
|
+
|
|
22
|
+
const MANIFEST_VERSION = 1;
|
|
23
|
+
const COPY_MODE = 0o600;
|
|
24
|
+
|
|
25
|
+
function sha256(buf) {
|
|
26
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveStores({ projectRoot, homeDir, includeUser } = {}) {
|
|
30
|
+
const stores = [];
|
|
31
|
+
if (projectRoot) {
|
|
32
|
+
const paths = buildRuntimePaths(projectRoot);
|
|
33
|
+
stores.push({
|
|
34
|
+
kind: 'project',
|
|
35
|
+
paths,
|
|
36
|
+
recordsPath: paths.memoryV2RecordsPath,
|
|
37
|
+
target: { kind: 'project', projectRoot },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (includeUser || (homeDir && !projectRoot)) {
|
|
41
|
+
const paths = buildUserPaths({ homeDir });
|
|
42
|
+
stores.push({
|
|
43
|
+
kind: 'user',
|
|
44
|
+
paths,
|
|
45
|
+
recordsPath: paths.memoryV2RecordsPath,
|
|
46
|
+
target: { kind: 'user', homeDir },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return stores;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function backupFileName(kind) {
|
|
53
|
+
return `${kind}-records.json`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function stateHint(state) {
|
|
57
|
+
switch (state) {
|
|
58
|
+
case 'missing': return 'no v2 store yet — records appear after the first memory write or migration';
|
|
59
|
+
case 'empty': return 'store exists but holds no usable records';
|
|
60
|
+
case 'corrupt': return 'records.json failed to parse — restore from a backup; a mutation will quarantine the file first';
|
|
61
|
+
case 'unsupported-schema': return 'written by a newer UKit build — upgrade before mutating';
|
|
62
|
+
default: return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* createBackup({projectRoot, homeDir?, outDir?, includeUser?}) → {backupDir, manifest}
|
|
68
|
+
* Copies each store's records.json byte-identical (corrupt bytes included —
|
|
69
|
+
* the manifest marks state:'corrupt' so restore skips it). Default location
|
|
70
|
+
* `<projectMemoryRoot>/backups/<ts>/`; outDir is the explicit escape.
|
|
71
|
+
*/
|
|
72
|
+
export async function createBackup({ projectRoot, homeDir, outDir, includeUser } = {}) {
|
|
73
|
+
const stores = resolveStores({ projectRoot, homeDir, includeUser });
|
|
74
|
+
if (stores.length === 0) throw new Error('createBackup: projectRoot or homeDir required');
|
|
75
|
+
|
|
76
|
+
const stamp = `${new Date().toISOString().replace(/[:.]/g, '-')}-${crypto.randomBytes(3).toString('hex')}`;
|
|
77
|
+
const backupDir = outDir ?? path.join(stores[0].paths.memoryRoot, 'backups', stamp);
|
|
78
|
+
await ensureDir(backupDir);
|
|
79
|
+
|
|
80
|
+
const manifest = { version: MANIFEST_VERSION, createdAt: new Date().toISOString(), stores: [] };
|
|
81
|
+
for (const store of stores) {
|
|
82
|
+
const doc = await readRecordStore(store.recordsPath);
|
|
83
|
+
const entry = {
|
|
84
|
+
kind: store.kind,
|
|
85
|
+
state: doc.state,
|
|
86
|
+
generation: doc.generation,
|
|
87
|
+
recordCount: doc.records.length,
|
|
88
|
+
tombstoneCount: doc.tombstones.length,
|
|
89
|
+
sha256: null,
|
|
90
|
+
file: null,
|
|
91
|
+
};
|
|
92
|
+
if (doc.state !== 'missing' && await pathExists(store.recordsPath)) {
|
|
93
|
+
const bytes = await fs.readFile(store.recordsPath);
|
|
94
|
+
const dest = path.join(backupDir, backupFileName(store.kind));
|
|
95
|
+
await fs.writeFile(dest, bytes, { mode: COPY_MODE });
|
|
96
|
+
await fs.chmod(dest, COPY_MODE).catch(() => {});
|
|
97
|
+
entry.sha256 = sha256(bytes);
|
|
98
|
+
entry.file = backupFileName(store.kind);
|
|
99
|
+
}
|
|
100
|
+
manifest.stores.push(entry);
|
|
101
|
+
}
|
|
102
|
+
const manifestPath = path.join(backupDir, 'manifest.json');
|
|
103
|
+
await writeJson(manifestPath, manifest, { fsync: true });
|
|
104
|
+
await fs.chmod(manifestPath, COPY_MODE).catch(() => {});
|
|
105
|
+
return { backupDir, manifest };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* restoreBackup(backupDir, {projectRoot, homeDir?, dryRun?}) →
|
|
110
|
+
* {restored, skipped, plan[]}
|
|
111
|
+
* Each store file is verified against the manifest sha256 and parsed before
|
|
112
|
+
* any write; a tampered/malformed backup file is a plan 'skip', never a
|
|
113
|
+
* partial merge. Merges go through mutateMemory op 'restore' — a corrupt
|
|
114
|
+
* target is quarantined inside that path (first call returns 'corrupt' after
|
|
115
|
+
* the quarantine copy, the retry merges onto the now-missing doc).
|
|
116
|
+
*/
|
|
117
|
+
export async function restoreBackup(backupDir, { projectRoot, homeDir, dryRun } = {}) {
|
|
118
|
+
const plan = [];
|
|
119
|
+
let restored = 0;
|
|
120
|
+
let skipped = 0;
|
|
121
|
+
|
|
122
|
+
const manifest = await readJsonIfExists(path.join(backupDir, 'manifest.json'));
|
|
123
|
+
if (!manifest || !Array.isArray(manifest.stores)) {
|
|
124
|
+
return { restored, skipped, plan: [{ kind: 'manifest', action: 'skip', reason: 'manifest-missing-or-invalid' }] };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const stores = resolveStores({ projectRoot, homeDir, includeUser: true });
|
|
128
|
+
const byKind = new Map(stores.map((s) => [s.kind, s]));
|
|
129
|
+
|
|
130
|
+
for (const entry of manifest.stores) {
|
|
131
|
+
const store = byKind.get(entry.kind);
|
|
132
|
+
if (!store) {
|
|
133
|
+
skipped += 1;
|
|
134
|
+
plan.push({ kind: entry.kind, action: 'skip', reason: 'no-target' });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!entry.file) {
|
|
138
|
+
skipped += 1;
|
|
139
|
+
plan.push({ kind: entry.kind, action: 'skip', reason: `backup-state:${entry.state ?? 'missing'}` });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// Boundary (SPEC §14): a manifest file entry must resolve inside
|
|
143
|
+
// backupDir — a crafted '..' path is a skip, never a read outside.
|
|
144
|
+
const backupRoot = path.resolve(backupDir);
|
|
145
|
+
const filePath = path.resolve(backupRoot, String(entry.file));
|
|
146
|
+
if (filePath !== backupRoot && !filePath.startsWith(backupRoot + path.sep)) {
|
|
147
|
+
skipped += 1;
|
|
148
|
+
plan.push({ kind: entry.kind, action: 'skip', reason: 'backup-file-out-of-boundary' });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
let bytes;
|
|
152
|
+
try {
|
|
153
|
+
bytes = await fs.readFile(filePath);
|
|
154
|
+
} catch {
|
|
155
|
+
skipped += 1;
|
|
156
|
+
plan.push({ kind: entry.kind, action: 'skip', reason: 'backup-file-missing' });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (entry.sha256 && sha256(bytes) !== entry.sha256) {
|
|
160
|
+
skipped += 1;
|
|
161
|
+
plan.push({ kind: entry.kind, action: 'skip', reason: 'sha256-mismatch' });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
let doc;
|
|
165
|
+
try {
|
|
166
|
+
doc = JSON.parse(bytes.toString('utf8'));
|
|
167
|
+
} catch {
|
|
168
|
+
doc = null;
|
|
169
|
+
}
|
|
170
|
+
if (!doc || typeof doc !== 'object' || !Array.isArray(doc.records)) {
|
|
171
|
+
skipped += 1;
|
|
172
|
+
plan.push({ kind: entry.kind, action: 'skip', reason: 'corrupt-backup' });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (dryRun) {
|
|
176
|
+
plan.push({ kind: entry.kind, action: 'dry-run', records: doc.records.length });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let res = await mutateMemory(store.target, { op: 'restore', payload: { records: doc.records } });
|
|
181
|
+
if (res?.status === 'corrupt') {
|
|
182
|
+
// Quarantine already happened inside the locked RMW; retry merges onto
|
|
183
|
+
// the now-missing doc. Never loop — a second 'corrupt' is a real failure.
|
|
184
|
+
res = await mutateMemory(store.target, { op: 'restore', payload: { records: doc.records } });
|
|
185
|
+
}
|
|
186
|
+
if (res?.status === 'ok') {
|
|
187
|
+
restored += res.added ?? 0;
|
|
188
|
+
plan.push({ kind: entry.kind, action: (res.added ?? 0) > 0 ? 'restored' : 'noop', added: res.added ?? 0 });
|
|
189
|
+
} else {
|
|
190
|
+
skipped += 1;
|
|
191
|
+
plan.push({ kind: entry.kind, action: 'skip', reason: `restore-status:${res?.status ?? 'null'}` });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { restored, skipped, plan };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* diagnoseStore({projectRoot, homeDir?}) → {stores:[StoreDiag]}
|
|
199
|
+
* Read-only doctor: typed state + counts + quarantine listing + hint.
|
|
200
|
+
* Never mutates — uses readRecordStore directly, not the mutate path.
|
|
201
|
+
*/
|
|
202
|
+
export async function diagnoseStore({ projectRoot, homeDir } = {}) {
|
|
203
|
+
const stores = resolveStores({ projectRoot, homeDir, includeUser: Boolean(homeDir) });
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const store of stores) {
|
|
206
|
+
const doc = await readRecordStore(store.recordsPath);
|
|
207
|
+
const quarantineDir = path.join(path.dirname(store.recordsPath), 'quarantine');
|
|
208
|
+
let quarantine = [];
|
|
209
|
+
try {
|
|
210
|
+
quarantine = (await fs.readdir(quarantineDir)).map((n) => path.join(quarantineDir, n));
|
|
211
|
+
} catch {
|
|
212
|
+
quarantine = [];
|
|
213
|
+
}
|
|
214
|
+
out.push({
|
|
215
|
+
kind: store.kind,
|
|
216
|
+
state: doc.state,
|
|
217
|
+
generation: doc.generation,
|
|
218
|
+
recordCount: doc.records.length,
|
|
219
|
+
tombstoneCount: doc.tombstones.length,
|
|
220
|
+
invalidSkipped: doc.invalidSkipped,
|
|
221
|
+
quarantine,
|
|
222
|
+
hint: stateHint(doc.state),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return { stores: out };
|
|
226
|
+
}
|
|
@@ -5,19 +5,17 @@
|
|
|
5
5
|
// v1→v2 migration (SPEC §4); migrate.js is lazy-imported to break the
|
|
6
6
|
// storeV2↔migrate import cycle. The record-document read/write core lives in
|
|
7
7
|
// recordStore.js (FR-007) so the user-level store (userMemory.js) shares it
|
|
8
|
-
// without the migration flow; mutations
|
|
9
|
-
//
|
|
8
|
+
// without the migration flow; all public mutations route through
|
|
9
|
+
// mutateMemory (mutateMemory.js) — the single guarded write entry.
|
|
10
10
|
|
|
11
11
|
import { buildRuntimePaths } from '../runtimePaths.js';
|
|
12
12
|
import { loadRuntimeConfig } from '../runtimeConfig.js';
|
|
13
13
|
import { createRecord, normalizeRecord } from './records.js';
|
|
14
14
|
import {
|
|
15
15
|
readRecordStore,
|
|
16
|
-
writeRecordStore,
|
|
17
|
-
applyRecordPatch,
|
|
18
16
|
isRawRecord,
|
|
19
|
-
mutateRecordStore,
|
|
20
17
|
} from './recordStore.js';
|
|
18
|
+
import { mutateMemory } from './mutateMemory.js';
|
|
21
19
|
|
|
22
20
|
// Per-process memoization for ensureMigrated — one in-flight migration per
|
|
23
21
|
// projectRoot. The PROMISE is cached (not a done-flag): concurrent callers
|
|
@@ -31,9 +29,13 @@ const migrationPromises = new Map();
|
|
|
31
29
|
async function ensureMigrated(projectRoot) {
|
|
32
30
|
const key = String(projectRoot);
|
|
33
31
|
const existing = migrationPromises.get(key);
|
|
34
|
-
if (existing)
|
|
32
|
+
if (existing) {
|
|
33
|
+
await existing.promise;
|
|
34
|
+
return { inFlight: true, failed: existing.failed };
|
|
35
|
+
}
|
|
35
36
|
|
|
36
|
-
const
|
|
37
|
+
const entry = { promise: null, failed: false };
|
|
38
|
+
entry.promise = (async () => {
|
|
37
39
|
try {
|
|
38
40
|
const config = await loadRuntimeConfig(projectRoot);
|
|
39
41
|
if (config?.memoryV2?.autoMigrate === false) return;
|
|
@@ -47,22 +49,48 @@ async function ensureMigrated(projectRoot) {
|
|
|
47
49
|
}
|
|
48
50
|
} catch {
|
|
49
51
|
// Lazy migration is best-effort: a failed auto-run must not break reads.
|
|
52
|
+
entry.failed = true;
|
|
50
53
|
}
|
|
51
54
|
})();
|
|
52
|
-
migrationPromises.set(key,
|
|
55
|
+
migrationPromises.set(key, entry);
|
|
53
56
|
try {
|
|
54
|
-
await promise;
|
|
57
|
+
await entry.promise;
|
|
55
58
|
} finally {
|
|
56
59
|
migrationPromises.delete(key);
|
|
57
60
|
}
|
|
61
|
+
return { inFlight: false, failed: entry.failed };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* loadRecordsDetailed(projectRoot) → { state, records }
|
|
66
|
+
* Typed loader states (M01-03): 'disabled' (memoryV2.enabled===false),
|
|
67
|
+
* 'migrating' (auto-migration in-flight or failed), otherwise the
|
|
68
|
+
* readRecordStore state ('ok'|'empty'|'missing'|'corrupt').
|
|
69
|
+
*/
|
|
70
|
+
export async function loadRecordsDetailed(projectRoot) {
|
|
71
|
+
try {
|
|
72
|
+
const config = await loadRuntimeConfig(projectRoot);
|
|
73
|
+
if (config?.memoryV2?.enabled === false) {
|
|
74
|
+
return { state: 'disabled', records: [] };
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// unreadable config → default enabled, keep going
|
|
78
|
+
}
|
|
79
|
+
const migration = await ensureMigrated(projectRoot);
|
|
80
|
+
if (migration.failed) {
|
|
81
|
+
return { state: 'migrating', records: [] };
|
|
82
|
+
}
|
|
83
|
+
// inFlight callers awaited the same migration promise — it has completed,
|
|
84
|
+
// so fall through and read the store like a normal caller.
|
|
85
|
+
const { records, state, generation } = await readStore(projectRoot);
|
|
86
|
+
return { state, records, generation };
|
|
58
87
|
}
|
|
59
88
|
|
|
60
89
|
/**
|
|
61
90
|
* loadRecords(projectRoot) → record[] — tolerant: invalid entries skipped.
|
|
62
91
|
*/
|
|
63
92
|
export async function loadRecords(projectRoot) {
|
|
64
|
-
await
|
|
65
|
-
const { records } = await readStore(projectRoot);
|
|
93
|
+
const { records } = await loadRecordsDetailed(projectRoot);
|
|
66
94
|
return records;
|
|
67
95
|
}
|
|
68
96
|
|
|
@@ -72,11 +100,18 @@ async function readStore(projectRoot) {
|
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
/**
|
|
75
|
-
* saveRecords(projectRoot, records) → void —
|
|
103
|
+
* saveRecords(projectRoot, records) → void — explicit restore path
|
|
104
|
+
* (mutateMemory op:'restore'); drops unrecoverable entries.
|
|
76
105
|
*/
|
|
77
106
|
export async function saveRecords(projectRoot, records) {
|
|
78
|
-
|
|
79
|
-
await
|
|
107
|
+
await ensureMigrated(projectRoot);
|
|
108
|
+
const res = await mutateMemory(
|
|
109
|
+
{ kind: 'project', projectRoot },
|
|
110
|
+
{ op: 'restore', payload: { records } },
|
|
111
|
+
);
|
|
112
|
+
if (res.status === 'rejected' || res.status === 'conflict' || res.status === 'corrupt') {
|
|
113
|
+
throw new Error(`saveRecords: restore ${res.status} — ${res.reason ?? 'no detail'}`);
|
|
114
|
+
}
|
|
80
115
|
}
|
|
81
116
|
|
|
82
117
|
/**
|
|
@@ -93,12 +128,13 @@ export async function addRecord(projectRoot, recordInput) {
|
|
|
93
128
|
throw new Error('addRecord: input could not be normalized into a valid record');
|
|
94
129
|
}
|
|
95
130
|
await ensureMigrated(projectRoot);
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
131
|
+
const res = await mutateMemory(
|
|
132
|
+
{ kind: 'project', projectRoot },
|
|
133
|
+
{ op: 'add', payload: record },
|
|
134
|
+
);
|
|
135
|
+
if (res.status === 'ok' || res.status === 'duplicate') return res.record ?? record;
|
|
136
|
+
if (res.status === 'disabled') return record;
|
|
137
|
+
throw new Error(`addRecord: ${res.status} — ${res.reason ?? 'no detail'}`);
|
|
102
138
|
}
|
|
103
139
|
|
|
104
140
|
/**
|
|
@@ -106,12 +142,13 @@ export async function addRecord(projectRoot, recordInput) {
|
|
|
106
142
|
*/
|
|
107
143
|
export async function updateRecord(projectRoot, id, patch = {}) {
|
|
108
144
|
await ensureMigrated(projectRoot);
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
const res = await mutateMemory(
|
|
146
|
+
{ kind: 'project', projectRoot },
|
|
147
|
+
{ op: 'update', payload: { id, patch } },
|
|
148
|
+
);
|
|
149
|
+
if (res.status === 'ok') return res.record;
|
|
150
|
+
if (res.status === 'not-found' || res.status === 'disabled') return null;
|
|
151
|
+
throw new Error(`updateRecord: ${res.status} — ${res.reason ?? 'no detail'}`);
|
|
115
152
|
}
|
|
116
153
|
|
|
117
154
|
/**
|
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
// Lazy/defensive bridge to storeV2.js (owned by TASK-002).
|
|
2
|
-
// Real storeV2
|
|
3
|
-
// consumers must never call migration themselves; just call this and treat
|
|
4
|
-
//
|
|
2
|
+
// Real storeV2 performs ensureMigrated auto-migration internally —
|
|
3
|
+
// consumers must never call migration themselves; just call this and treat a
|
|
4
|
+
// non-'ok' state as "fall back to the legacy memory path".
|
|
5
|
+
import { resolveMemoryStage } from './memoryFlags.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* loadV2Records(projectRoot) →
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* loadV2Records(projectRoot, filter, { config, projectId } = {}) → { state, records }
|
|
9
|
+
* Typed loader states (M01-03): 'ok'|'empty'|'missing'|'corrupt'|'disabled'|
|
|
10
|
+
* 'migrating'. Dynamic-import failure or a missing storeV2 API → 'missing'.
|
|
11
|
+
* Callers decide fallback on non-'ok' states.
|
|
12
|
+
*
|
|
13
|
+
* M06 rollout flag (SPEC §5 FR-003): when a config is supplied, the
|
|
14
|
+
* eligibility stage gates the v2 lane — 'off' (incl. killSwitch or an
|
|
15
|
+
* unlisted canary project) reports 'missing' so callers take the legacy
|
|
16
|
+
* lane exactly as if the v2 store were absent.
|
|
10
17
|
*/
|
|
11
|
-
export async function loadV2Records(projectRoot, filter = {}) {
|
|
18
|
+
export async function loadV2Records(projectRoot, filter = {}, { config, projectId } = {}) {
|
|
19
|
+
if (config && resolveMemoryStage(config, 'eligibility', { projectId }) === 'off') {
|
|
20
|
+
return { state: 'missing', records: [] };
|
|
21
|
+
}
|
|
12
22
|
try {
|
|
13
23
|
const storeV2 = await import('./storeV2.js');
|
|
14
|
-
if (typeof storeV2?.queryRecords !== 'function'
|
|
15
|
-
|
|
24
|
+
if (typeof storeV2?.queryRecords !== 'function'
|
|
25
|
+
|| typeof storeV2?.loadRecordsDetailed !== 'function') {
|
|
26
|
+
return { state: 'missing', records: [] };
|
|
16
27
|
}
|
|
17
|
-
const records = await storeV2.
|
|
18
|
-
|
|
28
|
+
const { state, records, generation } = await storeV2.loadRecordsDetailed(projectRoot);
|
|
29
|
+
const { type, scope, status, projectId } = filter ?? {};
|
|
30
|
+
const filtered = (Array.isArray(records) ? records : []).filter((r) => (
|
|
31
|
+
(type == null || r.type === type)
|
|
32
|
+
&& (scope == null || r.scope === scope)
|
|
33
|
+
&& (status == null || r.status === status)
|
|
34
|
+
&& (projectId == null || r.project_id === projectId)
|
|
35
|
+
));
|
|
36
|
+
return { state, records: filtered, generation: generation ?? 0 };
|
|
19
37
|
} catch {
|
|
20
|
-
return [];
|
|
38
|
+
return { state: 'missing', records: [] };
|
|
21
39
|
}
|
|
22
40
|
}
|
|
@@ -8,11 +8,9 @@ import { buildUserPaths } from '../userPaths.js';
|
|
|
8
8
|
import { createRecord, normalizeRecord } from './records.js';
|
|
9
9
|
import {
|
|
10
10
|
readRecordStore,
|
|
11
|
-
writeRecordStore,
|
|
12
|
-
applyRecordPatch,
|
|
13
11
|
isRawRecord,
|
|
14
|
-
mutateRecordStore,
|
|
15
12
|
} from './recordStore.js';
|
|
13
|
+
import { mutateMemory } from './mutateMemory.js';
|
|
16
14
|
import { loadRecords } from './storeV2.js';
|
|
17
15
|
|
|
18
16
|
function userRecordsPath(homeDir) {
|
|
@@ -27,11 +25,27 @@ export async function loadUserRecords({ homeDir } = {}) {
|
|
|
27
25
|
return records;
|
|
28
26
|
}
|
|
29
27
|
|
|
28
|
+
/**
|
|
29
|
+
* loadUserRecordsDetailed({homeDir}={}) → { state, records }
|
|
30
|
+
* Typed loader states (M01-03) via readRecordStore: 'ok'|'empty'|'missing'|
|
|
31
|
+
* 'corrupt'. No 'disabled'/'migrating' — the user layer has no v1 migration.
|
|
32
|
+
*/
|
|
33
|
+
export async function loadUserRecordsDetailed({ homeDir } = {}) {
|
|
34
|
+
const { records, state } = await readRecordStore(userRecordsPath(homeDir));
|
|
35
|
+
return { state, records };
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
/**
|
|
31
39
|
* saveUserRecords(records, {homeDir}={}) → void — atomic write.
|
|
32
40
|
*/
|
|
33
41
|
export async function saveUserRecords(records, { homeDir } = {}) {
|
|
34
|
-
await
|
|
42
|
+
const res = await mutateMemory(
|
|
43
|
+
{ kind: 'user', homeDir },
|
|
44
|
+
{ op: 'restore', payload: { records } },
|
|
45
|
+
);
|
|
46
|
+
if (res.status === 'rejected' || res.status === 'conflict' || res.status === 'corrupt') {
|
|
47
|
+
throw new Error(`saveUserRecords: restore ${res.status} — ${res.reason ?? 'no detail'}`);
|
|
48
|
+
}
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
/**
|
|
@@ -45,22 +59,25 @@ export async function addUserRecord(recordInput, { homeDir } = {}) {
|
|
|
45
59
|
if (!record) {
|
|
46
60
|
throw new Error('addUserRecord: input could not be normalized into a valid record');
|
|
47
61
|
}
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
62
|
+
const res = await mutateMemory(
|
|
63
|
+
{ kind: 'user', homeDir },
|
|
64
|
+
{ op: 'add', payload: record },
|
|
65
|
+
);
|
|
66
|
+
if (res.status === 'ok' || res.status === 'duplicate') return res.record ?? record;
|
|
67
|
+
throw new Error(`addUserRecord: ${res.status} — ${res.reason ?? 'no detail'}`);
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
/**
|
|
56
71
|
* updateUserRecord(id, patch, {homeDir}={}) → record | null
|
|
57
72
|
*/
|
|
58
73
|
export async function updateUserRecord(id, patch = {}, { homeDir } = {}) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
const res = await mutateMemory(
|
|
75
|
+
{ kind: 'user', homeDir },
|
|
76
|
+
{ op: 'update', payload: { id, patch } },
|
|
77
|
+
);
|
|
78
|
+
if (res.status === 'ok') return res.record;
|
|
79
|
+
if (res.status === 'not-found') return null;
|
|
80
|
+
throw new Error(`updateUserRecord: ${res.status} — ${res.reason ?? 'no detail'}`);
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
/**
|
|
@@ -70,12 +87,13 @@ export async function updateUserRecord(id, patch = {}, { homeDir } = {}) {
|
|
|
70
87
|
* or null when the id is absent.
|
|
71
88
|
*/
|
|
72
89
|
export async function removeUserRecord(id, { homeDir } = {}) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
const res = await mutateMemory(
|
|
91
|
+
{ kind: 'user', homeDir },
|
|
92
|
+
{ op: 'purge', payload: { id }, confirmScope: 'user' },
|
|
93
|
+
);
|
|
94
|
+
if (res.status === 'ok') return res.record;
|
|
95
|
+
if (res.status === 'not-found') return null;
|
|
96
|
+
throw new Error(`removeUserRecord: ${res.status} — ${res.reason ?? 'no detail'}`);
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
/**
|