@ngockhoale/ukit 3.0.7 → 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 +18 -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,144 @@
|
|
|
1
|
+
// MemoryHit provenance API (M05-01/M05-03, SPEC §5 FR-001/FR-002, §8).
|
|
2
|
+
// Pure module: no I/O, deterministic. buildMemoryHit carries provenance
|
|
3
|
+
// (citation, as-of, freshness slot, reason codes) plus backward-compat
|
|
4
|
+
// fields; rankHits applies eligibility-first dual-score ranking.
|
|
5
|
+
// The record's stored certainty hint is NEVER read for ranking —
|
|
6
|
+
// eligibility and the composite score are the only gates.
|
|
7
|
+
|
|
8
|
+
import { eligible } from './policy.js';
|
|
9
|
+
import { scoreRecordTokens } from './recordIndex.js';
|
|
10
|
+
import { LEGACY_SCOPE_MATCH, RECORD_TYPE_WEIGHTS } from './retrieval.js';
|
|
11
|
+
|
|
12
|
+
const SNIPPET_MAX = 160;
|
|
13
|
+
const TYPE_WEIGHT_SCALE = 10; // typeWeight bounded to ≤0.7 bonus
|
|
14
|
+
const RECENCY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000; // bounded decay on created_at
|
|
15
|
+
|
|
16
|
+
function firstCitation(evidence) {
|
|
17
|
+
if (!Array.isArray(evidence)) return null;
|
|
18
|
+
for (const entry of evidence) {
|
|
19
|
+
if (entry != null && typeof entry === 'object'
|
|
20
|
+
&& typeof entry.locator === 'string' && entry.locator.length > 0) {
|
|
21
|
+
// Locator is an opaque passthrough — never resolved, never rendered
|
|
22
|
+
// as an instruction.
|
|
23
|
+
return {
|
|
24
|
+
kind: entry.kind ?? null,
|
|
25
|
+
locator: entry.locator,
|
|
26
|
+
observedAt: entry.observed_at ?? null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function maxObservedAt(evidence, fallback) {
|
|
34
|
+
let max = null;
|
|
35
|
+
if (Array.isArray(evidence)) {
|
|
36
|
+
for (const entry of evidence) {
|
|
37
|
+
if (entry != null && typeof entry.observed_at === 'number'
|
|
38
|
+
&& (max == null || entry.observed_at > max)) {
|
|
39
|
+
max = entry.observed_at;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return max ?? fallback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// buildMemoryHit(record, {freshness?, score?, reasons?}) → MemoryHit
|
|
47
|
+
// MemoryHit = {id, type, scope, projectId, snippet, citation, observedAt,
|
|
48
|
+
// validFrom, validUntil, revision, freshness, reasons, relevanceScore,
|
|
49
|
+
// summary, source, timestamp}
|
|
50
|
+
export function buildMemoryHit(record, { freshness = null, score = 0, reasons = [] } = {}) {
|
|
51
|
+
const text = String(record?.text ?? '');
|
|
52
|
+
const snippet = text.length > SNIPPET_MAX ? text.slice(0, SNIPPET_MAX) : text;
|
|
53
|
+
const createdAt = record?.created_at ?? 0;
|
|
54
|
+
return {
|
|
55
|
+
id: record?.id ?? null,
|
|
56
|
+
type: record?.type ?? null,
|
|
57
|
+
scope: record?.scope ?? null,
|
|
58
|
+
projectId: record?.project_id ?? null,
|
|
59
|
+
snippet,
|
|
60
|
+
citation: firstCitation(record?.evidence),
|
|
61
|
+
observedAt: maxObservedAt(record?.evidence, createdAt),
|
|
62
|
+
validFrom: record?.valid_from ?? null,
|
|
63
|
+
validUntil: record?.valid_until ?? null,
|
|
64
|
+
revision: record?.revision ?? 1,
|
|
65
|
+
freshness,
|
|
66
|
+
reasons: Array.isArray(reasons) ? [...reasons] : [],
|
|
67
|
+
relevanceScore: score,
|
|
68
|
+
// Backward-compat fields for existing search-result consumers.
|
|
69
|
+
summary: snippet,
|
|
70
|
+
source: record?.type ?? null,
|
|
71
|
+
timestamp: createdAt,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Requested-scope narrowing only — eligibility is owned by policy.js.
|
|
76
|
+
// Delegates to the shared LEGACY_SCOPE_MATCH table from retrieval.js.
|
|
77
|
+
function matchesRequestedScope(record, scope) {
|
|
78
|
+
if (scope === 'all') return true;
|
|
79
|
+
const matcher = LEGACY_SCOPE_MATCH[scope];
|
|
80
|
+
return matcher ? matcher(record) : (record.scope === scope || record.type === scope);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hasLocatorEvidence(record) {
|
|
84
|
+
return Array.isArray(record.evidence)
|
|
85
|
+
&& record.evidence.some((entry) => entry != null
|
|
86
|
+
&& typeof entry.locator === 'string' && entry.locator.length > 0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// rankHits(records, {queryTokens, eligibleCtx, policy, scope?, limit?, now?})
|
|
90
|
+
// → MemoryHit[]
|
|
91
|
+
// Eligibility first (eligible() + requested-scope narrowing), then composite
|
|
92
|
+
// score = lexical + typeBonus + evidenceBonus + recencyBonus. Dedup by id.
|
|
93
|
+
export function rankHits(records, {
|
|
94
|
+
queryTokens = [],
|
|
95
|
+
eligibleCtx = {},
|
|
96
|
+
policy = {},
|
|
97
|
+
scope = 'all',
|
|
98
|
+
limit = 10,
|
|
99
|
+
now,
|
|
100
|
+
} = {}) {
|
|
101
|
+
if (!Array.isArray(records) || records.length === 0) return [];
|
|
102
|
+
if (!Array.isArray(queryTokens) || queryTokens.length === 0) return [];
|
|
103
|
+
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
104
|
+
const ctx = { ...eligibleCtx, now: nowMs };
|
|
105
|
+
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
const scored = [];
|
|
108
|
+
for (const record of records) {
|
|
109
|
+
if (record == null || seen.has(record.id)) continue;
|
|
110
|
+
seen.add(record.id);
|
|
111
|
+
if (!eligible(record, ctx, policy).ok) continue;
|
|
112
|
+
if (!matchesRequestedScope(record, scope)) continue;
|
|
113
|
+
|
|
114
|
+
const lexical = scoreRecordTokens(record, queryTokens);
|
|
115
|
+
const typeWeight = RECORD_TYPE_WEIGHTS[record.type] ?? 1;
|
|
116
|
+
const typeBonus = typeWeight / TYPE_WEIGHT_SCALE;
|
|
117
|
+
const evidenceBonus = hasLocatorEvidence(record) ? 1 : 0;
|
|
118
|
+
const age = nowMs - (record.created_at ?? 0);
|
|
119
|
+
const recencyBonus = record.created_at > 0 && age >= 0
|
|
120
|
+
? Math.exp(-Math.LN2 * age / RECENCY_HALF_LIFE_MS)
|
|
121
|
+
: 0;
|
|
122
|
+
const score = lexical + typeBonus + evidenceBonus + recencyBonus;
|
|
123
|
+
|
|
124
|
+
const reasons = [];
|
|
125
|
+
if (lexical > 0) reasons.push('lexical');
|
|
126
|
+
reasons.push(`type:${record.type}`);
|
|
127
|
+
if (evidenceBonus > 0) reasons.push('evidence');
|
|
128
|
+
if (recencyBonus > 0) reasons.push('recency');
|
|
129
|
+
|
|
130
|
+
scored.push({ record, score, reasons });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return scored
|
|
134
|
+
.sort((left, right) => (
|
|
135
|
+
right.score - left.score
|
|
136
|
+
|| (right.record.created_at ?? 0) - (left.record.created_at ?? 0)
|
|
137
|
+
|| String(left.record.text ?? '').localeCompare(String(right.record.text ?? ''))
|
|
138
|
+
))
|
|
139
|
+
.slice(0, limit)
|
|
140
|
+
.map((entry) => buildMemoryHit(entry.record, {
|
|
141
|
+
score: entry.score,
|
|
142
|
+
reasons: entry.reasons,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
// Memory v1 → v2 migration — SPEC §4.
|
|
1
|
+
// Memory v1 → v2 migration — SPEC §4 / §5 FR-001..003.
|
|
2
2
|
// Idempotent: marker v2/migrated-from-v1.json; re-run is a no-op.
|
|
3
3
|
// Backup-first: legacy memory dir copied to v1-backup-<ts>/ before writing.
|
|
4
4
|
// Legacy files are never mutated.
|
|
5
|
+
// Record mapping lives in migrateMapping.js (pure, deterministic); this file
|
|
6
|
+
// only collects legacy sources, resolves identities/tombstones, and runs the
|
|
7
|
+
// guarded store merge.
|
|
5
8
|
|
|
6
|
-
import crypto from 'node:crypto';
|
|
7
9
|
import fs from 'node:fs/promises';
|
|
8
10
|
import path from 'node:path';
|
|
9
11
|
import { buildRuntimePaths } from '../runtimePaths.js';
|
|
10
12
|
import { readJsonIfExists, writeJson } from '../fileOps.js';
|
|
11
13
|
import { loadRuntimeConfig } from '../runtimeConfig.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
+
import { mapLegacySources } from './migrateMapping.js';
|
|
15
|
+
import { readRecordStore } from './recordStore.js';
|
|
16
|
+
import { lookupRegisteredProjectId } from './projectIdentity.js';
|
|
14
17
|
|
|
15
18
|
const MARKER_NAME = 'migrated-from-v1.json';
|
|
16
19
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -62,176 +65,38 @@ async function copyDirRecursive(srcDir, destDir) {
|
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Per-run skip counter — threaded explicitly so concurrent runMigration calls
|
|
71
|
-
// can never cross-report each other's skips (a module-level counter did).
|
|
72
|
-
|
|
73
|
-
function makeRecord({ type, scope, text, confidence, provenance, projectId = null, validUntil = null, createdAt = null, meta = {}, status = null }, skip) {
|
|
74
|
-
try {
|
|
75
|
-
const record = createRecord({
|
|
76
|
-
type,
|
|
77
|
-
scope,
|
|
78
|
-
text,
|
|
79
|
-
provenance,
|
|
80
|
-
sourceFingerprint: fingerprint(type, text),
|
|
81
|
-
confidence,
|
|
82
|
-
createdBy: 'migration',
|
|
83
|
-
projectId,
|
|
84
|
-
validUntil,
|
|
85
|
-
meta,
|
|
86
|
-
});
|
|
87
|
-
if (createdAt != null && Number.isFinite(createdAt)) record.created_at = createdAt;
|
|
88
|
-
if (status) record.status = status;
|
|
89
|
-
return record;
|
|
90
|
-
} catch {
|
|
91
|
-
skip.count += 1;
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function migrateUserMemory(user, records, skip) {
|
|
97
|
-
if (!user || typeof user !== 'object') return;
|
|
98
|
-
for (const rule of Array.isArray(user.rules) ? user.rules : []) {
|
|
99
|
-
if (typeof rule !== 'string' || !rule) continue;
|
|
100
|
-
const rec = makeRecord({
|
|
101
|
-
type: 'project_rule', scope: 'user', text: rule,
|
|
102
|
-
confidence: 1.0, provenance: 'legacy:user-rules',
|
|
103
|
-
createdAt: user.updatedAt,
|
|
104
|
-
}, skip);
|
|
105
|
-
if (rec) records.push(rec);
|
|
106
|
-
}
|
|
107
|
-
const prefs = user.preferences && typeof user.preferences === 'object' ? user.preferences : {};
|
|
108
|
-
for (const [key, value] of Object.entries(prefs)) {
|
|
109
|
-
const text = `${key} = ${typeof value === 'string' ? value : JSON.stringify(value)}`;
|
|
110
|
-
const rec = makeRecord({
|
|
111
|
-
type: 'derived_fact', scope: 'user', text,
|
|
112
|
-
confidence: 1.0, provenance: 'legacy:user-preferences',
|
|
113
|
-
createdAt: user.updatedAt,
|
|
114
|
-
}, skip);
|
|
115
|
-
if (rec) records.push(rec);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function migrateProjectMemory(project, projectId, records, skip) {
|
|
120
|
-
if (!project || typeof project !== 'object') return;
|
|
121
|
-
const createdAt = project.updatedAt;
|
|
122
|
-
|
|
123
|
-
for (const text of Array.isArray(project.conventions) ? project.conventions : []) {
|
|
124
|
-
if (typeof text !== 'string' || !text) continue;
|
|
125
|
-
const rec = makeRecord({
|
|
126
|
-
type: 'project_rule', scope: 'repo', text,
|
|
127
|
-
confidence: 0.9, provenance: 'legacy:conventions',
|
|
128
|
-
projectId, createdAt,
|
|
129
|
-
}, skip);
|
|
130
|
-
if (rec) records.push(rec);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
for (const text of Array.isArray(project.activeRules) ? project.activeRules : []) {
|
|
134
|
-
if (typeof text !== 'string' || !text) continue;
|
|
135
|
-
const rec = makeRecord({
|
|
136
|
-
type: 'project_rule', scope: 'repo', text,
|
|
137
|
-
confidence: 1.0, provenance: 'legacy:activeRules',
|
|
138
|
-
projectId, createdAt,
|
|
139
|
-
}, skip);
|
|
140
|
-
if (rec) records.push(rec);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
for (const decision of Array.isArray(project.decisions) ? project.decisions : []) {
|
|
144
|
-
if (!decision || typeof decision !== 'object') continue;
|
|
145
|
-
const text = decision.why ? `${decision.what} — ${decision.why}` : String(decision.what ?? '');
|
|
146
|
-
if (!text) continue;
|
|
147
|
-
const rec = makeRecord({
|
|
148
|
-
type: 'derived_fact', scope: 'repo', text,
|
|
149
|
-
confidence: 0.9, provenance: 'legacy:decisions',
|
|
150
|
-
projectId, createdAt: decision.when ?? createdAt,
|
|
151
|
-
}, skip);
|
|
152
|
-
if (rec) records.push(rec);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const profileEntries = [
|
|
156
|
-
...(typeof project.architecture === 'string' && project.architecture ? [project.architecture] : []),
|
|
157
|
-
...(Array.isArray(project.techStack) ? project.techStack.filter((t) => typeof t === 'string' && t) : []),
|
|
158
|
-
];
|
|
159
|
-
for (const text of profileEntries) {
|
|
160
|
-
const rec = makeRecord({
|
|
161
|
-
type: 'derived_fact', scope: 'repo', text,
|
|
162
|
-
confidence: 0.8, provenance: 'legacy:project-profile',
|
|
163
|
-
projectId, createdAt,
|
|
164
|
-
}, skip);
|
|
165
|
-
if (rec) records.push(rec);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
for (const pc of Array.isArray(project.patternCandidates) ? project.patternCandidates : []) {
|
|
169
|
-
if (!pc || typeof pc !== 'object' || typeof pc.text !== 'string' || !pc.text) continue;
|
|
170
|
-
const legacyStatus = pc.status ?? 'pending';
|
|
171
|
-
const meta = { ...pc, legacyStatus };
|
|
172
|
-
const base = {
|
|
173
|
-
text: pc.text,
|
|
174
|
-
scope: 'task', confidence: 0.4, provenance: 'legacy:pattern-candidate',
|
|
175
|
-
projectId, createdAt: pc.detectedAt ?? createdAt, meta,
|
|
176
|
-
};
|
|
177
|
-
let rec = null;
|
|
178
|
-
if (legacyStatus === 'approved') {
|
|
179
|
-
rec = makeRecord({ ...base, type: 'project_rule', scope: 'repo', confidence: 0.9 }, skip);
|
|
180
|
-
} else if (legacyStatus === 'rejected') {
|
|
181
|
-
rec = makeRecord({ ...base, type: 'derived_fact', status: 'archived' }, skip);
|
|
182
|
-
} else {
|
|
183
|
-
rec = makeRecord({ ...base, type: 'derived_fact' }, skip);
|
|
184
|
-
}
|
|
185
|
-
if (rec) records.push(rec);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function sessionText(session) {
|
|
190
|
-
return [
|
|
191
|
-
session.taskDescription,
|
|
192
|
-
session.outcome ? `outcome: ${session.outcome}` : null,
|
|
193
|
-
...(Array.isArray(session.keyActions) ? session.keyActions : []),
|
|
194
|
-
].filter(Boolean).join(' — ');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function migrateSession(session, episodeTtlMs, records, skip) {
|
|
198
|
-
if (!session || typeof session !== 'object') return;
|
|
199
|
-
const text = sessionText(session);
|
|
200
|
-
if (!text) return;
|
|
201
|
-
const createdAt = session.startedAt ?? session.endedAt ?? null;
|
|
202
|
-
const validUntil = createdAt != null ? createdAt + episodeTtlMs : null;
|
|
203
|
-
const rec = makeRecord({
|
|
204
|
-
type: 'episode', scope: 'session', text,
|
|
205
|
-
confidence: 0.7, provenance: 'legacy:session',
|
|
206
|
-
projectId: typeof session.projectId === 'string' ? session.projectId : null,
|
|
207
|
-
createdAt, validUntil,
|
|
208
|
-
meta: { legacyId: session.id ?? null },
|
|
209
|
-
}, skip);
|
|
210
|
-
if (rec) records.push(rec);
|
|
211
|
-
}
|
|
212
|
-
|
|
68
|
+
// Source shape consumed by mapLegacySources:
|
|
69
|
+
// { user, userFile, projects:[{id,file,content}],
|
|
70
|
+
// sessions:[{file,session}], corruptFiles, corrupt:[fileName] }
|
|
213
71
|
async function collectLegacySources(paths) {
|
|
214
|
-
const sources = { user: null, projects: [], sessions: [], corruptFiles: 0 };
|
|
72
|
+
const sources = { user: null, userFile: null, projects: [], sessions: [], corruptFiles: 0, corrupt: [] };
|
|
215
73
|
|
|
216
74
|
const user = await readJsonTolerant(paths.userMemoryPath);
|
|
217
|
-
if (user.corrupt)
|
|
218
|
-
|
|
75
|
+
if (user.corrupt) {
|
|
76
|
+
sources.corruptFiles += 1;
|
|
77
|
+
sources.corrupt.push(path.basename(paths.userMemoryPath));
|
|
78
|
+
}
|
|
79
|
+
if (user.value) {
|
|
80
|
+
sources.user = user.value;
|
|
81
|
+
sources.userFile = path.basename(paths.userMemoryPath);
|
|
82
|
+
}
|
|
219
83
|
|
|
220
84
|
for (const name of await listJsonFiles(paths.projectsDir)) {
|
|
221
85
|
const filePath = path.join(paths.projectsDir, name);
|
|
222
86
|
const { value: content, corrupt } = await readJsonTolerant(filePath);
|
|
223
87
|
if (corrupt) {
|
|
224
88
|
sources.corruptFiles += 1;
|
|
89
|
+
sources.corrupt.push(name);
|
|
225
90
|
continue;
|
|
226
91
|
}
|
|
227
92
|
if (!content) continue;
|
|
228
93
|
if (name.endsWith('.archive.json')) {
|
|
229
94
|
for (const s of Array.isArray(content.sessions) ? content.sessions : []) {
|
|
230
|
-
sources.sessions.push(s);
|
|
95
|
+
sources.sessions.push({ file: name, session: s });
|
|
231
96
|
}
|
|
232
97
|
continue;
|
|
233
98
|
}
|
|
234
|
-
sources.projects.push({ id: content.id ?? name.replace(/\.json$/, ''), content });
|
|
99
|
+
sources.projects.push({ id: content.id ?? name.replace(/\.json$/, ''), file: name, content });
|
|
235
100
|
}
|
|
236
101
|
|
|
237
102
|
for (const name of await listJsonFiles(paths.sessionsDir)) {
|
|
@@ -239,13 +104,14 @@ async function collectLegacySources(paths) {
|
|
|
239
104
|
const { value: content, corrupt } = await readJsonTolerant(filePath);
|
|
240
105
|
if (corrupt) {
|
|
241
106
|
sources.corruptFiles += 1;
|
|
107
|
+
sources.corrupt.push(name);
|
|
242
108
|
continue;
|
|
243
109
|
}
|
|
244
110
|
if (!content) continue;
|
|
245
111
|
if (name.endsWith('.archive.json') && Array.isArray(content.sessions)) {
|
|
246
|
-
sources.sessions.push(
|
|
112
|
+
for (const s of content.sessions) sources.sessions.push({ file: name, session: s });
|
|
247
113
|
} else {
|
|
248
|
-
sources.sessions.push(content);
|
|
114
|
+
sources.sessions.push({ file: name, session: content });
|
|
249
115
|
}
|
|
250
116
|
}
|
|
251
117
|
|
|
@@ -257,18 +123,12 @@ function hasLegacyContent(sources) {
|
|
|
257
123
|
|| sources.sessions.length > 0 || sources.corruptFiles > 0;
|
|
258
124
|
}
|
|
259
125
|
|
|
260
|
-
async function
|
|
126
|
+
async function episodeTtlMsFor(projectRoot) {
|
|
261
127
|
const config = await loadRuntimeConfig(projectRoot);
|
|
262
128
|
const ttlDays = Number.isFinite(config?.memoryV2?.episodeTtlDays) && config.memoryV2.episodeTtlDays > 0
|
|
263
129
|
? config.memoryV2.episodeTtlDays
|
|
264
130
|
: 90;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const records = [];
|
|
268
|
-
if (sources.user) migrateUserMemory(sources.user, records, skip);
|
|
269
|
-
for (const { id, content } of sources.projects) migrateProjectMemory(content, id, records, skip);
|
|
270
|
-
for (const session of sources.sessions) migrateSession(session, episodeTtlMs, records, skip);
|
|
271
|
-
return records;
|
|
131
|
+
return ttlDays * DAY_MS;
|
|
272
132
|
}
|
|
273
133
|
|
|
274
134
|
/**
|
|
@@ -285,25 +145,41 @@ export async function needsMigration(projectRoot) {
|
|
|
285
145
|
|
|
286
146
|
/**
|
|
287
147
|
* runMigration(projectRoot, { dryRun? } = {})
|
|
288
|
-
* → { migrated, skipped, markerPath, backupDir }
|
|
289
|
-
* No-op when the marker exists or no legacy files exist. dryRun
|
|
290
|
-
*
|
|
148
|
+
* → { migrated, skipped, plan, markerPath, backupDir }
|
|
149
|
+
* No-op when the marker exists or no legacy files exist. dryRun returns the
|
|
150
|
+
* full per-record plan ({id, source, action, reason}) and writes nothing.
|
|
291
151
|
*/
|
|
292
152
|
export async function runMigration(projectRoot, { dryRun = false } = {}) {
|
|
293
153
|
const paths = buildRuntimePaths(projectRoot);
|
|
294
154
|
const markerPath = path.join(paths.memoryV2Dir, MARKER_NAME);
|
|
295
|
-
const empty = { migrated: 0, skipped: 0, markerPath, backupDir: null };
|
|
155
|
+
const empty = { migrated: 0, skipped: 0, plan: [], markerPath, backupDir: null };
|
|
296
156
|
|
|
297
157
|
if (await pathExists(markerPath)) return empty;
|
|
298
158
|
|
|
299
159
|
const sources = await collectLegacySources(paths);
|
|
300
160
|
if (!hasLegacyContent(sources)) return empty;
|
|
301
161
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
162
|
+
// Tombstones + alias resolution feed the pure mapper — tombstone
|
|
163
|
+
// precedence is planned here and enforced again inside mutateMemory.
|
|
164
|
+
// Registry lookups are async, so legacy ids are pre-resolved and handed
|
|
165
|
+
// to the mapper as a synchronous closure.
|
|
166
|
+
const prior = await readRecordStore(paths.memoryV2RecordsPath);
|
|
167
|
+
const resolvedIds = new Map();
|
|
168
|
+
for (const p of sources.projects) {
|
|
169
|
+
if (p.id && !resolvedIds.has(p.id)) {
|
|
170
|
+
resolvedIds.set(p.id, await lookupRegisteredProjectId(p.id));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const { records, plan, skipped } = mapLegacySources(sources, {
|
|
174
|
+
projectId: null,
|
|
175
|
+
now: Date.now(),
|
|
176
|
+
episodeTtlMs: await episodeTtlMsFor(projectRoot),
|
|
177
|
+
tombstones: prior.tombstones,
|
|
178
|
+
resolveProjectId: (legacyId) => resolvedIds.get(legacyId) ?? null,
|
|
179
|
+
});
|
|
180
|
+
|
|
305
181
|
if (dryRun) {
|
|
306
|
-
return { migrated: records.length, skipped, markerPath, backupDir: null };
|
|
182
|
+
return { migrated: records.length, skipped, plan, markerPath, backupDir: null };
|
|
307
183
|
}
|
|
308
184
|
|
|
309
185
|
// Backup-first: copy legacy memory dir (excluding v2 + prior backups) before writing.
|
|
@@ -321,20 +197,24 @@ export async function runMigration(projectRoot, { dryRun = false } = {}) {
|
|
|
321
197
|
}
|
|
322
198
|
}
|
|
323
199
|
|
|
324
|
-
// Merge into the existing store
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
200
|
+
// Merge into the existing store through mutateMemory's restore op — the
|
|
201
|
+
// single guarded write entry. Tombstone precedence lives there, so a
|
|
202
|
+
// purged record can never resurrect via re-migration; source_fingerprint
|
|
203
|
+
// + deterministic-id dedupe keeps the merge idempotent. A non-ok status
|
|
204
|
+
// (corrupt/disabled/rejected) is fail-closed: surface it as an error
|
|
205
|
+
// rather than writing the marker over a store that never received the
|
|
206
|
+
// records.
|
|
207
|
+
const { mutateMemory } = await import('./mutateMemory.js');
|
|
208
|
+
const restore = await mutateMemory(
|
|
209
|
+
{ kind: 'project', projectRoot },
|
|
210
|
+
{ op: 'restore', payload: { records } },
|
|
211
|
+
);
|
|
212
|
+
if (restore.status !== 'ok') {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`runMigration: restore rejected — ${restore.status}${restore.reason ? `:${restore.reason}` : ''}`,
|
|
335
215
|
);
|
|
336
|
-
|
|
337
|
-
|
|
216
|
+
}
|
|
217
|
+
const migrated = restore.added ?? 0;
|
|
338
218
|
|
|
339
219
|
const counts = {
|
|
340
220
|
total: records.length,
|
|
@@ -347,5 +227,5 @@ export async function runMigration(projectRoot, { dryRun = false } = {}) {
|
|
|
347
227
|
backupDir,
|
|
348
228
|
});
|
|
349
229
|
|
|
350
|
-
return { migrated
|
|
230
|
+
return { migrated, skipped, plan, markerPath, backupDir };
|
|
351
231
|
}
|