@prmichaelsen/remember-mcp 3.0.0 → 3.13.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/AGENT.md +296 -250
- package/CHANGELOG.md +358 -0
- package/README.md +68 -45
- package/agent/commands/acp.clarification-create.md +382 -0
- package/agent/commands/acp.project-info.md +309 -0
- package/agent/commands/acp.project-remove.md +379 -0
- package/agent/commands/acp.project-update.md +296 -0
- package/agent/commands/acp.task-create.md +17 -9
- package/agent/commands/git.commit.md +13 -1
- package/agent/design/comment-memory-type.md +2 -2
- package/agent/design/local.collaborative-memory-sync.md +265 -0
- package/agent/design/local.content-flags.md +210 -0
- package/agent/design/local.ghost-persona-system.md +273 -0
- package/agent/design/local.group-acl-integration.md +338 -0
- package/agent/design/local.memory-acl-schema.md +352 -0
- package/agent/design/local.memory-collection-pattern-v2.md +348 -0
- package/agent/design/local.moderation-and-space-config.md +257 -0
- package/agent/design/local.v2-api-reference.md +621 -0
- package/agent/design/local.v2-migration-guide.md +191 -0
- package/agent/design/local.v2-usage-examples.md +265 -0
- package/agent/design/permissions-storage-architecture.md +11 -3
- package/agent/design/trust-escalation-prevention.md +9 -2
- package/agent/design/trust-system-implementation.md +12 -3
- package/agent/milestones/milestone-14-memory-collection-v2.md +182 -0
- package/agent/milestones/milestone-15-moderation-space-config.md +126 -0
- package/agent/progress.yaml +628 -49
- package/agent/scripts/acp.common.sh +2 -0
- package/agent/scripts/acp.install.sh +11 -1
- package/agent/scripts/acp.package-install-optimized.sh +454 -0
- package/agent/scripts/acp.package-install.sh +247 -300
- package/agent/scripts/acp.project-info.sh +218 -0
- package/agent/scripts/acp.project-remove.sh +302 -0
- package/agent/scripts/acp.project-update.sh +296 -0
- package/agent/scripts/acp.yaml-parser.sh +128 -10
- package/agent/tasks/milestone-14-memory-collection-v2/task-165-core-infrastructure-setup.md +171 -0
- package/agent/tasks/milestone-14-memory-collection-v2/task-166-update-remember-publish.md +191 -0
- package/agent/tasks/milestone-14-memory-collection-v2/task-167-update-remember-retract.md +186 -0
- package/agent/tasks/milestone-14-memory-collection-v2/task-168-implement-remember-revise.md +184 -0
- package/agent/tasks/milestone-14-memory-collection-v2/task-169-update-remember-search-space.md +179 -0
- package/agent/tasks/milestone-14-memory-collection-v2/task-170-update-remember-create-update.md +139 -0
- package/agent/tasks/milestone-14-memory-collection-v2/task-172-performance-testing-optimization.md +161 -0
- package/agent/tasks/milestone-14-memory-collection-v2/task-173-documentation-examples.md +258 -0
- package/agent/tasks/milestone-15-moderation-space-config/task-174-add-moderation-schema-fields.md +57 -0
- package/agent/tasks/milestone-15-moderation-space-config/task-175-create-space-config-service.md +64 -0
- package/agent/tasks/milestone-15-moderation-space-config/task-176-wire-moderation-publish-flow.md +45 -0
- package/agent/tasks/milestone-15-moderation-space-config/task-177-add-moderation-search-filters.md +70 -0
- package/agent/tasks/milestone-15-moderation-space-config/task-178-create-remember-moderate-tool.md +69 -0
- package/agent/tasks/milestone-15-moderation-space-config/task-179-documentation-integration-tests.md +58 -0
- package/agent/tasks/milestone-16-ghost-system/task-187-ghost-config-firestore.md +41 -0
- package/agent/tasks/milestone-16-ghost-system/task-188-trust-filter-integration.md +44 -0
- package/agent/tasks/milestone-16-ghost-system/task-189-ghost-memory-filtering.md +43 -0
- package/agent/tasks/milestone-16-ghost-system/task-190-ghost-config-tools.md +45 -0
- package/agent/tasks/milestone-16-ghost-system/task-191-escalation-firestore.md +38 -0
- package/agent/tasks/milestone-16-ghost-system/task-192-documentation-verification.md +39 -0
- package/agent/tasks/milestone-7-trust-permissions/task-180-access-result-permission-types.md +69 -0
- package/agent/tasks/milestone-7-trust-permissions/task-181-firestore-permissions-access-logs.md +56 -0
- package/agent/tasks/milestone-7-trust-permissions/task-182-trust-enforcement-service.md +68 -0
- package/agent/tasks/milestone-7-trust-permissions/task-183-access-control-service.md +70 -0
- package/agent/tasks/milestone-7-trust-permissions/task-184-permission-tools.md +79 -0
- package/agent/tasks/milestone-7-trust-permissions/task-185-wire-trust-into-search-query.md +55 -0
- package/agent/tasks/milestone-7-trust-permissions/task-186-documentation-verification.md +56 -0
- package/agent/tasks/task-76-fix-indexnullstate-schema-bug.md +197 -0
- package/dist/collections/composite-ids.d.ts +106 -0
- package/dist/collections/core-infrastructure.spec.d.ts +11 -0
- package/dist/collections/dot-notation.d.ts +106 -0
- package/dist/collections/tracking-arrays.d.ts +176 -0
- package/dist/constants/content-types.d.ts +1 -0
- package/dist/schema/v2-collections-comments.spec.d.ts +8 -0
- package/dist/schema/v2-collections.d.ts +210 -0
- package/dist/server-factory.d.ts +15 -0
- package/dist/server-factory.js +2798 -1029
- package/dist/server.js +2526 -1012
- package/dist/services/access-control.d.ts +103 -0
- package/dist/services/access-control.spec.d.ts +2 -0
- package/dist/services/credentials-provider.d.ts +24 -0
- package/dist/services/credentials-provider.spec.d.ts +2 -0
- package/dist/services/escalation.service.d.ts +22 -0
- package/dist/services/escalation.service.spec.d.ts +2 -0
- package/dist/services/ghost-config.service.d.ts +55 -0
- package/dist/services/ghost-config.service.spec.d.ts +2 -0
- package/dist/services/space-config.service.d.ts +23 -0
- package/dist/services/space-config.service.spec.d.ts +2 -0
- package/dist/services/trust-enforcement.d.ts +83 -0
- package/dist/services/trust-enforcement.spec.d.ts +2 -0
- package/dist/services/trust-validator.d.ts +43 -0
- package/dist/services/trust-validator.spec.d.ts +2 -0
- package/dist/tools/confirm-publish-moderation.spec.d.ts +8 -0
- package/dist/tools/confirm.d.ts +8 -1
- package/dist/tools/create-memory.d.ts +2 -1
- package/dist/tools/create-memory.spec.d.ts +10 -0
- package/dist/tools/create-relationship.d.ts +2 -1
- package/dist/tools/delete-memory.d.ts +2 -1
- package/dist/tools/delete-relationship.d.ts +2 -1
- package/dist/tools/deny.d.ts +2 -1
- package/dist/tools/find-similar.d.ts +2 -1
- package/dist/tools/get-preferences.d.ts +2 -1
- package/dist/tools/ghost-config.d.ts +27 -0
- package/dist/tools/ghost-config.spec.d.ts +2 -0
- package/dist/tools/moderate.d.ts +20 -0
- package/dist/tools/moderate.spec.d.ts +5 -0
- package/dist/tools/publish.d.ts +11 -3
- package/dist/tools/query-memory.d.ts +3 -1
- package/dist/tools/query-space.d.ts +4 -1
- package/dist/tools/retract.d.ts +29 -0
- package/dist/tools/revise.d.ts +45 -0
- package/dist/tools/revise.spec.d.ts +8 -0
- package/dist/tools/search-memory.d.ts +2 -1
- package/dist/tools/search-relationship.d.ts +2 -1
- package/dist/tools/search-space.d.ts +25 -5
- package/dist/tools/search-space.spec.d.ts +9 -0
- package/dist/tools/set-preference.d.ts +2 -1
- package/dist/tools/update-memory.d.ts +2 -1
- package/dist/tools/update-relationship.d.ts +2 -1
- package/dist/types/access-result.d.ts +48 -0
- package/dist/types/access-result.spec.d.ts +2 -0
- package/dist/types/auth.d.ts +46 -0
- package/dist/types/ghost-config.d.ts +36 -0
- package/dist/types/memory.d.ts +3 -1
- package/dist/types/preferences.d.ts +1 -1
- package/dist/utils/auth-helpers.d.ts +14 -0
- package/dist/utils/auth-helpers.spec.d.ts +2 -0
- package/dist/utils/test-data-generator.d.ts +124 -0
- package/dist/utils/test-data-generator.spec.d.ts +12 -0
- package/dist/v2-performance.e2e.d.ts +17 -0
- package/dist/v2-smoke.e2e.d.ts +14 -0
- package/dist/weaviate/client.d.ts +5 -8
- package/dist/weaviate/space-schema.d.ts +2 -2
- package/docs/performance/v2-benchmarks.md +80 -0
- package/jest.e2e.config.js +14 -3
- package/package.json +1 -1
- package/scripts/.collection-recreation-state.yaml +16 -0
- package/scripts/.gitkeep +5 -0
- package/scripts/README-collection-recreation.md +224 -0
- package/scripts/README.md +51 -0
- package/scripts/backup-collections.ts +543 -0
- package/scripts/delete-collection.ts +137 -0
- package/scripts/migrate-recreate-collections.ts +578 -0
- package/scripts/migrate-v1-to-v2.ts +1094 -0
- package/scripts/package-lock.json +1113 -0
- package/scripts/package.json +27 -0
- package/src/collections/composite-ids.ts +193 -0
- package/src/collections/core-infrastructure.spec.ts +353 -0
- package/src/collections/dot-notation.ts +212 -0
- package/src/collections/tracking-arrays.ts +298 -0
- package/src/constants/content-types.ts +20 -0
- package/src/schema/v2-collections-comments.spec.ts +141 -0
- package/src/schema/v2-collections.ts +433 -0
- package/src/server-factory.ts +89 -20
- package/src/server.ts +45 -17
- package/src/services/access-control.spec.ts +383 -0
- package/src/services/access-control.ts +291 -0
- package/src/services/credentials-provider.spec.ts +22 -0
- package/src/services/credentials-provider.ts +34 -0
- package/src/services/escalation.service.spec.ts +183 -0
- package/src/services/escalation.service.ts +150 -0
- package/src/services/ghost-config.service.spec.ts +339 -0
- package/src/services/ghost-config.service.ts +219 -0
- package/src/services/space-config.service.spec.ts +102 -0
- package/src/services/space-config.service.ts +79 -0
- package/src/services/trust-enforcement.spec.ts +309 -0
- package/src/services/trust-enforcement.ts +197 -0
- package/src/services/trust-validator.spec.ts +108 -0
- package/src/services/trust-validator.ts +105 -0
- package/src/tools/confirm-publish-moderation.spec.ts +240 -0
- package/src/tools/confirm.ts +869 -135
- package/src/tools/create-memory.spec.ts +126 -0
- package/src/tools/create-memory.ts +20 -27
- package/src/tools/create-relationship.ts +17 -8
- package/src/tools/delete-memory.ts +13 -6
- package/src/tools/delete-relationship.ts +15 -6
- package/src/tools/deny.ts +8 -1
- package/src/tools/find-similar.ts +21 -8
- package/src/tools/get-preferences.ts +10 -1
- package/src/tools/ghost-config.spec.ts +180 -0
- package/src/tools/ghost-config.ts +230 -0
- package/src/tools/moderate.spec.ts +277 -0
- package/src/tools/moderate.ts +219 -0
- package/src/tools/publish.ts +99 -41
- package/src/tools/query-memory.ts +28 -6
- package/src/tools/query-space.ts +39 -4
- package/src/tools/retract.ts +292 -0
- package/src/tools/revise.spec.ts +146 -0
- package/src/tools/revise.ts +283 -0
- package/src/tools/search-memory.ts +30 -7
- package/src/tools/search-relationship.ts +11 -2
- package/src/tools/search-space.spec.ts +341 -0
- package/src/tools/search-space.ts +323 -99
- package/src/tools/set-preference.ts +10 -1
- package/src/tools/update-memory.ts +16 -5
- package/src/tools/update-relationship.ts +10 -1
- package/src/types/access-result.spec.ts +193 -0
- package/src/types/access-result.ts +62 -0
- package/src/types/auth.ts +52 -0
- package/src/types/ghost-config.ts +46 -0
- package/src/types/memory.ts +9 -1
- package/src/types/preferences.ts +2 -2
- package/src/utils/auth-helpers.spec.ts +75 -0
- package/src/utils/auth-helpers.ts +25 -0
- package/src/utils/test-data-generator.spec.ts +317 -0
- package/src/utils/test-data-generator.ts +292 -0
- package/src/utils/weaviate-filters.ts +4 -4
- package/src/v2-performance.e2e.ts +173 -0
- package/src/v2-smoke.e2e.ts +401 -0
- package/src/weaviate/client.spec.ts +5 -5
- package/src/weaviate/client.ts +51 -36
- package/src/weaviate/schema.ts +11 -256
- package/src/weaviate/space-schema.spec.ts +24 -24
- package/src/weaviate/space-schema.ts +18 -6
|
@@ -0,0 +1,1094 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* V1 → V2 Collection Migration Script
|
|
4
|
+
*
|
|
5
|
+
* Migrates Weaviate collections from v1 naming to v2 naming:
|
|
6
|
+
* Memory_{SanitizedUserId} → Memory_users_{literalUserId}
|
|
7
|
+
* Memory_public → Memory_spaces_public
|
|
8
|
+
* Memory_{spaceId} → Memory_spaces_public (merged)
|
|
9
|
+
*
|
|
10
|
+
* Safety: V1 collections are NEVER modified or deleted.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npx tsx scripts/migrate-v1-to-v2.ts [options]
|
|
14
|
+
* --dry-run Preview changes without writing
|
|
15
|
+
* --skip-backup Skip backup step (if already backed up)
|
|
16
|
+
* --verify-only Only run verification checks
|
|
17
|
+
* --batch-size N Documents per batch (default: 100)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import weaviate, { WeaviateClient } from 'weaviate-client';
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as yaml from 'yaml';
|
|
23
|
+
import * as dotenv from 'dotenv';
|
|
24
|
+
|
|
25
|
+
// Load environment
|
|
26
|
+
dotenv.config();
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Types
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
interface MigrationConfig {
|
|
33
|
+
weaviate: {
|
|
34
|
+
url: string;
|
|
35
|
+
apiKey?: string;
|
|
36
|
+
openaiApiKey?: string;
|
|
37
|
+
};
|
|
38
|
+
options: {
|
|
39
|
+
batchSize: number;
|
|
40
|
+
dryRun: boolean;
|
|
41
|
+
skipBackup: boolean;
|
|
42
|
+
verifyOnly: boolean;
|
|
43
|
+
stateFile: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface CollectionClassification {
|
|
48
|
+
name: string;
|
|
49
|
+
type: 'user' | 'public' | 'space' | 'backup' | 'unknown';
|
|
50
|
+
v2Name?: string;
|
|
51
|
+
userId?: string; // literal userId from documents
|
|
52
|
+
spaceId?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface MigrationStep {
|
|
56
|
+
name: string;
|
|
57
|
+
status: 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed';
|
|
58
|
+
error?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface MigrationState {
|
|
62
|
+
migration: {
|
|
63
|
+
id: string;
|
|
64
|
+
started_at: string;
|
|
65
|
+
updated_at: string;
|
|
66
|
+
status: 'not_started' | 'discovering' | 'backing_up' | 'creating' | 'copying' | 'verifying' | 'completed' | 'failed';
|
|
67
|
+
};
|
|
68
|
+
collections: CollectionClassification[];
|
|
69
|
+
steps: MigrationStep[];
|
|
70
|
+
copy_progress: {
|
|
71
|
+
[collectionName: string]: {
|
|
72
|
+
total: number;
|
|
73
|
+
copied: number;
|
|
74
|
+
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
verification: {
|
|
78
|
+
passed: boolean;
|
|
79
|
+
checks: Array<{
|
|
80
|
+
name: string;
|
|
81
|
+
passed: boolean;
|
|
82
|
+
details?: string;
|
|
83
|
+
}>;
|
|
84
|
+
};
|
|
85
|
+
errors: Array<{
|
|
86
|
+
step: string;
|
|
87
|
+
error: string;
|
|
88
|
+
timestamp: string;
|
|
89
|
+
}>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* V1 → V2 property name mapping for data transformation during copy.
|
|
94
|
+
* Keys are v1 names, values are v2 names.
|
|
95
|
+
*/
|
|
96
|
+
const PROPERTY_RENAMES: Record<string, string> = {
|
|
97
|
+
type: 'content_type',
|
|
98
|
+
trust: 'trust_score',
|
|
99
|
+
location_gps_lat: 'location_lat',
|
|
100
|
+
location_gps_lng: 'location_lon',
|
|
101
|
+
relationships: 'relationship_ids',
|
|
102
|
+
memory_ids: 'related_memory_ids',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// State Manager
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
class StateManager {
|
|
110
|
+
private stateFile: string;
|
|
111
|
+
private state!: MigrationState;
|
|
112
|
+
|
|
113
|
+
constructor(stateFile: string) {
|
|
114
|
+
this.stateFile = stateFile;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async initialize(): Promise<void> {
|
|
118
|
+
if (fs.existsSync(this.stateFile)) {
|
|
119
|
+
this.load();
|
|
120
|
+
console.log(` Resuming migration from ${this.stateFile}\n`);
|
|
121
|
+
} else {
|
|
122
|
+
this.state = this.createInitialState();
|
|
123
|
+
await this.save();
|
|
124
|
+
console.log(` Created state file: ${this.stateFile}\n`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private createInitialState(): MigrationState {
|
|
129
|
+
const now = new Date().toISOString();
|
|
130
|
+
return {
|
|
131
|
+
migration: {
|
|
132
|
+
id: `v1-to-v2-${now.replace(/[:.]/g, '-').slice(0, 19)}`,
|
|
133
|
+
started_at: now,
|
|
134
|
+
updated_at: now,
|
|
135
|
+
status: 'not_started',
|
|
136
|
+
},
|
|
137
|
+
collections: [],
|
|
138
|
+
steps: [],
|
|
139
|
+
copy_progress: {},
|
|
140
|
+
verification: { passed: false, checks: [] },
|
|
141
|
+
errors: [],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private load(): void {
|
|
146
|
+
const content = fs.readFileSync(this.stateFile, 'utf8');
|
|
147
|
+
this.state = yaml.parse(content);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async save(): Promise<void> {
|
|
151
|
+
this.state.migration.updated_at = new Date().toISOString();
|
|
152
|
+
fs.writeFileSync(this.stateFile, yaml.stringify(this.state), 'utf8');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getState(): MigrationState {
|
|
156
|
+
return this.state;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
setStatus(status: MigrationState['migration']['status']): void {
|
|
160
|
+
this.state.migration.status = status;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setCollections(collections: CollectionClassification[]): void {
|
|
164
|
+
this.state.collections = collections;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
addStep(name: string, status: MigrationStep['status'] = 'pending'): void {
|
|
168
|
+
const existing = this.state.steps.find(s => s.name === name);
|
|
169
|
+
if (existing) {
|
|
170
|
+
existing.status = status;
|
|
171
|
+
} else {
|
|
172
|
+
this.state.steps.push({ name, status });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
updateStep(name: string, status: MigrationStep['status'], error?: string): void {
|
|
177
|
+
const step = this.state.steps.find(s => s.name === name);
|
|
178
|
+
if (step) {
|
|
179
|
+
step.status = status;
|
|
180
|
+
if (error) step.error = error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
updateCopyProgress(collection: string, total: number, copied: number, status: 'pending' | 'in_progress' | 'completed' | 'failed'): void {
|
|
185
|
+
this.state.copy_progress[collection] = { total, copied, status };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
addVerificationCheck(name: string, passed: boolean, details?: string): void {
|
|
189
|
+
this.state.verification.checks.push({ name, passed, details });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
setVerificationPassed(passed: boolean): void {
|
|
193
|
+
this.state.verification.passed = passed;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
addError(step: string, error: string): void {
|
|
197
|
+
this.state.errors.push({ step, error, timestamp: new Date().toISOString() });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async cleanup(): Promise<void> {
|
|
201
|
+
if (fs.existsSync(this.stateFile)) {
|
|
202
|
+
fs.unlinkSync(this.stateFile);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Migration Engine
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
class V1ToV2Migration {
|
|
212
|
+
private client!: WeaviateClient;
|
|
213
|
+
private config: MigrationConfig;
|
|
214
|
+
private state: StateManager;
|
|
215
|
+
|
|
216
|
+
constructor(config: MigrationConfig) {
|
|
217
|
+
this.config = config;
|
|
218
|
+
this.state = new StateManager(config.options.stateFile);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --------------------------------------------------------------------------
|
|
222
|
+
// Connection
|
|
223
|
+
// --------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
async connect(): Promise<void> {
|
|
226
|
+
console.log('Connecting to Weaviate...');
|
|
227
|
+
|
|
228
|
+
const clientConfig: any = {
|
|
229
|
+
authCredentials: this.config.weaviate.apiKey
|
|
230
|
+
? new weaviate.ApiKey(this.config.weaviate.apiKey)
|
|
231
|
+
: undefined,
|
|
232
|
+
};
|
|
233
|
+
if (this.config.weaviate.openaiApiKey) {
|
|
234
|
+
clientConfig.headers = { 'X-Openai-Api-Key': this.config.weaviate.openaiApiKey };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.client = await weaviate.connectToWeaviateCloud(
|
|
238
|
+
this.config.weaviate.url,
|
|
239
|
+
clientConfig,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
console.log(' Connected\n');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async disconnect(): Promise<void> {
|
|
246
|
+
await this.client?.close();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --------------------------------------------------------------------------
|
|
250
|
+
// Step 1: Discover
|
|
251
|
+
// --------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
async discover(): Promise<CollectionClassification[]> {
|
|
254
|
+
console.log('Step 1: Discovering collections...');
|
|
255
|
+
this.state.addStep('discover', 'in_progress');
|
|
256
|
+
this.state.setStatus('discovering');
|
|
257
|
+
await this.state.save();
|
|
258
|
+
|
|
259
|
+
const allCollections = await this.client.collections.listAll();
|
|
260
|
+
const memoryCollections = allCollections
|
|
261
|
+
.map(c => c.name)
|
|
262
|
+
.filter(name => name.startsWith('Memory_') && !name.startsWith('Backup_'));
|
|
263
|
+
|
|
264
|
+
console.log(` Found ${memoryCollections.length} Memory_ collections`);
|
|
265
|
+
|
|
266
|
+
const classified: CollectionClassification[] = [];
|
|
267
|
+
|
|
268
|
+
for (const name of memoryCollections) {
|
|
269
|
+
// Skip v2 collections that already exist
|
|
270
|
+
if (name.startsWith('Memory_users_') || name === 'Memory_spaces_public' || name.startsWith('Memory_groups_')) {
|
|
271
|
+
console.log(` [skip] ${name} (already v2 format)`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (name === 'Memory_public') {
|
|
276
|
+
classified.push({ name, type: 'public', v2Name: 'Memory_spaces_public' });
|
|
277
|
+
console.log(` [public] ${name} -> Memory_spaces_public`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Try to determine if this is a user collection or a space collection
|
|
282
|
+
// by reading a document and checking for user_id
|
|
283
|
+
const classification = await this.classifyCollection(name);
|
|
284
|
+
classified.push(classification);
|
|
285
|
+
console.log(` [${classification.type}] ${name} -> ${classification.v2Name || '(merged)'}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.state.setCollections(classified);
|
|
289
|
+
this.state.updateStep('discover', 'completed');
|
|
290
|
+
await this.state.save();
|
|
291
|
+
|
|
292
|
+
console.log(`\n Classified: ${classified.filter(c => c.type === 'user').length} user, ` +
|
|
293
|
+
`${classified.filter(c => c.type === 'public').length} public, ` +
|
|
294
|
+
`${classified.filter(c => c.type === 'space').length} space\n`);
|
|
295
|
+
|
|
296
|
+
return classified;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private async classifyCollection(name: string): Promise<CollectionClassification> {
|
|
300
|
+
const collection = this.client.collections.get(name);
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Fetch a sample document to inspect
|
|
304
|
+
const result = await collection.query.fetchObjects({ limit: 1 });
|
|
305
|
+
|
|
306
|
+
if (result.objects.length === 0) {
|
|
307
|
+
return { name, type: 'unknown' };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const doc = result.objects[0];
|
|
311
|
+
const props = doc.properties as Record<string, any>;
|
|
312
|
+
|
|
313
|
+
// Space collections have 'spaces' or 'author_id' fields
|
|
314
|
+
// User collections have 'user_id' without 'author_id'
|
|
315
|
+
if (props.author_id || props.spaces) {
|
|
316
|
+
// This is a space collection (per-space v1 collection)
|
|
317
|
+
const suffix = name.replace('Memory_', '');
|
|
318
|
+
return {
|
|
319
|
+
name,
|
|
320
|
+
type: 'space',
|
|
321
|
+
v2Name: 'Memory_spaces_public', // All spaces merge into single collection
|
|
322
|
+
spaceId: suffix,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// User collection — extract literal userId from user_id property
|
|
327
|
+
const literalUserId = props.user_id as string;
|
|
328
|
+
if (literalUserId) {
|
|
329
|
+
return {
|
|
330
|
+
name,
|
|
331
|
+
type: 'user',
|
|
332
|
+
v2Name: `Memory_users_${literalUserId}`,
|
|
333
|
+
userId: literalUserId,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { name, type: 'unknown' };
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.log(` Warning: Could not classify ${name}: ${(error as Error).message}`);
|
|
340
|
+
return { name, type: 'unknown' };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// --------------------------------------------------------------------------
|
|
345
|
+
// Step 2: Backup
|
|
346
|
+
// --------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
async backup(collections: CollectionClassification[]): Promise<void> {
|
|
349
|
+
if (this.config.options.skipBackup) {
|
|
350
|
+
console.log('Step 2: Backup (SKIPPED — --skip-backup)\n');
|
|
351
|
+
this.state.addStep('backup', 'skipped');
|
|
352
|
+
await this.state.save();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
console.log('Step 2: Backing up v1 collections...');
|
|
357
|
+
this.state.addStep('backup', 'in_progress');
|
|
358
|
+
this.state.setStatus('backing_up');
|
|
359
|
+
await this.state.save();
|
|
360
|
+
|
|
361
|
+
for (const col of collections) {
|
|
362
|
+
const backupName = `Backup_${col.name}`;
|
|
363
|
+
|
|
364
|
+
const backupExists = await this.client.collections.exists(backupName);
|
|
365
|
+
if (backupExists) {
|
|
366
|
+
console.log(` [exists] ${backupName}`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (this.config.options.dryRun) {
|
|
371
|
+
console.log(` [dry-run] Would create ${backupName}`);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
await this.copyCollection(col.name, backupName, false);
|
|
376
|
+
console.log(` [backed up] ${col.name} -> ${backupName}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.state.updateStep('backup', 'completed');
|
|
380
|
+
await this.state.save();
|
|
381
|
+
console.log('');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// --------------------------------------------------------------------------
|
|
385
|
+
// Step 3: Create v2 collections
|
|
386
|
+
// --------------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
async createV2Collections(collections: CollectionClassification[]): Promise<void> {
|
|
389
|
+
console.log('Step 3: Creating v2 collections...');
|
|
390
|
+
this.state.addStep('create_v2', 'in_progress');
|
|
391
|
+
this.state.setStatus('creating');
|
|
392
|
+
await this.state.save();
|
|
393
|
+
|
|
394
|
+
// Determine unique v2 collection names
|
|
395
|
+
const v2Names = new Set<string>();
|
|
396
|
+
for (const col of collections) {
|
|
397
|
+
if (col.v2Name) v2Names.add(col.v2Name);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
for (const v2Name of v2Names) {
|
|
401
|
+
const exists = await this.client.collections.exists(v2Name);
|
|
402
|
+
if (exists) {
|
|
403
|
+
console.log(` [exists] ${v2Name}`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (this.config.options.dryRun) {
|
|
408
|
+
console.log(` [dry-run] Would create ${v2Name}`);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Create with v2 schema
|
|
413
|
+
await this.createV2Collection(v2Name, collections);
|
|
414
|
+
console.log(` [created] ${v2Name}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
this.state.updateStep('create_v2', 'completed');
|
|
418
|
+
await this.state.save();
|
|
419
|
+
console.log('');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private async createV2Collection(v2Name: string, _collections: CollectionClassification[]): Promise<void> {
|
|
423
|
+
// Import v2 schema functions
|
|
424
|
+
const { createUserCollectionSchema, createSpaceCollectionSchema } =
|
|
425
|
+
await import('../src/schema/v2-collections.js');
|
|
426
|
+
|
|
427
|
+
if (v2Name === 'Memory_spaces_public') {
|
|
428
|
+
const schema = createSpaceCollectionSchema();
|
|
429
|
+
await this.client.collections.create(schema);
|
|
430
|
+
} else if (v2Name.startsWith('Memory_users_')) {
|
|
431
|
+
const userId = v2Name.replace('Memory_users_', '');
|
|
432
|
+
const schema = createUserCollectionSchema(userId);
|
|
433
|
+
await this.client.collections.create(schema);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// --------------------------------------------------------------------------
|
|
438
|
+
// Step 4: Copy user memories
|
|
439
|
+
// --------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
async copyUserMemories(collections: CollectionClassification[]): Promise<void> {
|
|
442
|
+
const userCollections = collections.filter(c => c.type === 'user');
|
|
443
|
+
if (userCollections.length === 0) {
|
|
444
|
+
console.log('Step 4: Copy user memories (no user collections found)\n');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
console.log(`Step 4: Copying ${userCollections.length} user collection(s)...`);
|
|
449
|
+
this.state.addStep('copy_users', 'in_progress');
|
|
450
|
+
this.state.setStatus('copying');
|
|
451
|
+
await this.state.save();
|
|
452
|
+
|
|
453
|
+
for (const col of userCollections) {
|
|
454
|
+
if (!col.v2Name || !col.userId) {
|
|
455
|
+
console.log(` [skip] ${col.name} (no userId resolved)`);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (this.config.options.dryRun) {
|
|
460
|
+
const srcCol = this.client.collections.get(col.name);
|
|
461
|
+
const aggregate = await srcCol.aggregate.overAll();
|
|
462
|
+
console.log(` [dry-run] ${col.name} -> ${col.v2Name} (${aggregate.totalCount || 0} docs)`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
await this.copyUserCollection(col);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
this.state.updateStep('copy_users', 'completed');
|
|
470
|
+
await this.state.save();
|
|
471
|
+
console.log('');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private async copyUserCollection(col: CollectionClassification): Promise<void> {
|
|
475
|
+
const srcCollection = this.client.collections.get(col.name);
|
|
476
|
+
const dstCollection = this.client.collections.get(col.v2Name!);
|
|
477
|
+
|
|
478
|
+
const aggregate = await srcCollection.aggregate.overAll();
|
|
479
|
+
const totalCount = aggregate.totalCount || 0;
|
|
480
|
+
console.log(` Copying ${col.name} -> ${col.v2Name} (${totalCount} docs)`);
|
|
481
|
+
|
|
482
|
+
this.state.updateCopyProgress(col.name, totalCount, 0, 'in_progress');
|
|
483
|
+
await this.state.save();
|
|
484
|
+
|
|
485
|
+
let offset = 0;
|
|
486
|
+
let copied = 0;
|
|
487
|
+
|
|
488
|
+
while (offset < totalCount) {
|
|
489
|
+
const result = await srcCollection.query.fetchObjects({
|
|
490
|
+
limit: this.config.options.batchSize,
|
|
491
|
+
offset,
|
|
492
|
+
includeVector: true,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
if (result.objects.length === 0) break;
|
|
496
|
+
|
|
497
|
+
const objects = result.objects.map(doc => {
|
|
498
|
+
const props = this.transformProperties(doc.properties as Record<string, any>);
|
|
499
|
+
|
|
500
|
+
// Add v2 tracking arrays if not present
|
|
501
|
+
if (!props.space_ids) props.space_ids = [];
|
|
502
|
+
if (!props.group_ids) props.group_ids = [];
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
properties: props,
|
|
506
|
+
vectors: doc.vectors,
|
|
507
|
+
uuid: doc.uuid,
|
|
508
|
+
};
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
await dstCollection.data.insertMany(objects);
|
|
512
|
+
|
|
513
|
+
copied += result.objects.length;
|
|
514
|
+
offset += result.objects.length;
|
|
515
|
+
|
|
516
|
+
this.state.updateCopyProgress(col.name, totalCount, copied, 'in_progress');
|
|
517
|
+
await this.state.save();
|
|
518
|
+
|
|
519
|
+
const pct = ((copied / totalCount) * 100).toFixed(1);
|
|
520
|
+
process.stdout.write(`\r ${pct}% (${copied}/${totalCount})`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
this.state.updateCopyProgress(col.name, totalCount, copied, 'completed');
|
|
524
|
+
await this.state.save();
|
|
525
|
+
console.log(`\n Done: ${copied} docs copied`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// --------------------------------------------------------------------------
|
|
529
|
+
// Step 5: Copy/merge published memories
|
|
530
|
+
// --------------------------------------------------------------------------
|
|
531
|
+
|
|
532
|
+
async copyPublishedMemories(collections: CollectionClassification[]): Promise<void> {
|
|
533
|
+
const publicCollections = collections.filter(c => c.type === 'public' || c.type === 'space');
|
|
534
|
+
if (publicCollections.length === 0) {
|
|
535
|
+
console.log('Step 5: Copy published memories (no public/space collections found)\n');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
console.log(`Step 5: Merging ${publicCollections.length} public/space collection(s) -> Memory_spaces_public...`);
|
|
540
|
+
this.state.addStep('copy_published', 'in_progress');
|
|
541
|
+
await this.state.save();
|
|
542
|
+
|
|
543
|
+
for (const col of publicCollections) {
|
|
544
|
+
if (this.config.options.dryRun) {
|
|
545
|
+
const srcCol = this.client.collections.get(col.name);
|
|
546
|
+
const aggregate = await srcCol.aggregate.overAll();
|
|
547
|
+
console.log(` [dry-run] ${col.name} -> Memory_spaces_public (${aggregate.totalCount || 0} docs)`);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
await this.copyPublicCollection(col);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
this.state.updateStep('copy_published', 'completed');
|
|
555
|
+
await this.state.save();
|
|
556
|
+
console.log('');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private async copyPublicCollection(col: CollectionClassification): Promise<void> {
|
|
560
|
+
const srcCollection = this.client.collections.get(col.name);
|
|
561
|
+
const dstCollection = this.client.collections.get('Memory_spaces_public');
|
|
562
|
+
|
|
563
|
+
const aggregate = await srcCollection.aggregate.overAll();
|
|
564
|
+
const totalCount = aggregate.totalCount || 0;
|
|
565
|
+
console.log(` Copying ${col.name} -> Memory_spaces_public (${totalCount} docs)`);
|
|
566
|
+
|
|
567
|
+
this.state.updateCopyProgress(col.name, totalCount, 0, 'in_progress');
|
|
568
|
+
await this.state.save();
|
|
569
|
+
|
|
570
|
+
let offset = 0;
|
|
571
|
+
let copied = 0;
|
|
572
|
+
|
|
573
|
+
while (offset < totalCount) {
|
|
574
|
+
const result = await srcCollection.query.fetchObjects({
|
|
575
|
+
limit: this.config.options.batchSize,
|
|
576
|
+
offset,
|
|
577
|
+
includeVector: true,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
if (result.objects.length === 0) break;
|
|
581
|
+
|
|
582
|
+
const objects = result.objects.map(doc => {
|
|
583
|
+
const props = this.transformProperties(doc.properties as Record<string, any>);
|
|
584
|
+
const authorId = (props.author_id || props.user_id || '') as string;
|
|
585
|
+
const originalId = doc.uuid;
|
|
586
|
+
|
|
587
|
+
// Generate composite ID: {authorId}.{originalUUID}
|
|
588
|
+
const compositeId = authorId ? `${authorId}.${originalId}` : originalId;
|
|
589
|
+
|
|
590
|
+
// Set space_ids from existing spaces field or from collection's spaceId
|
|
591
|
+
if (!props.space_ids || (props.space_ids as string[]).length === 0) {
|
|
592
|
+
if (props.spaces && Array.isArray(props.spaces) && props.spaces.length > 0) {
|
|
593
|
+
props.space_ids = props.spaces;
|
|
594
|
+
} else if (col.spaceId) {
|
|
595
|
+
props.space_ids = [col.spaceId];
|
|
596
|
+
} else {
|
|
597
|
+
props.space_ids = [];
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Ensure group_ids
|
|
602
|
+
if (!props.group_ids) props.group_ids = [];
|
|
603
|
+
|
|
604
|
+
// Add revision fields
|
|
605
|
+
if (props.revision_count === undefined) props.revision_count = 0;
|
|
606
|
+
if (props.revised_at === undefined) props.revised_at = null;
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
properties: props,
|
|
610
|
+
vectors: doc.vectors,
|
|
611
|
+
uuid: compositeId,
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
try {
|
|
616
|
+
await dstCollection.data.insertMany(objects);
|
|
617
|
+
} catch (insertError) {
|
|
618
|
+
// Some may already exist (if re-running), insert individually
|
|
619
|
+
for (const obj of objects) {
|
|
620
|
+
try {
|
|
621
|
+
await dstCollection.data.insert({
|
|
622
|
+
properties: obj.properties,
|
|
623
|
+
vectors: obj.vectors as any,
|
|
624
|
+
id: obj.uuid,
|
|
625
|
+
});
|
|
626
|
+
} catch {
|
|
627
|
+
// Already exists or other error — skip
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
copied += result.objects.length;
|
|
633
|
+
offset += result.objects.length;
|
|
634
|
+
|
|
635
|
+
this.state.updateCopyProgress(col.name, totalCount, copied, 'in_progress');
|
|
636
|
+
await this.state.save();
|
|
637
|
+
|
|
638
|
+
const pct = ((copied / totalCount) * 100).toFixed(1);
|
|
639
|
+
process.stdout.write(`\r ${pct}% (${copied}/${totalCount})`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
this.state.updateCopyProgress(col.name, totalCount, copied, 'completed');
|
|
643
|
+
await this.state.save();
|
|
644
|
+
console.log(`\n Done: ${copied} docs merged`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// --------------------------------------------------------------------------
|
|
648
|
+
// Step 6: Backfill tracking arrays
|
|
649
|
+
// --------------------------------------------------------------------------
|
|
650
|
+
|
|
651
|
+
async backfillTrackingArrays(collections: CollectionClassification[]): Promise<void> {
|
|
652
|
+
console.log('Step 6: Backfilling tracking arrays on source user memories...');
|
|
653
|
+
this.state.addStep('backfill_tracking', 'in_progress');
|
|
654
|
+
await this.state.save();
|
|
655
|
+
|
|
656
|
+
const userCollections = collections.filter(c => c.type === 'user' && c.v2Name);
|
|
657
|
+
|
|
658
|
+
if (this.config.options.dryRun) {
|
|
659
|
+
console.log(` [dry-run] Would backfill tracking arrays for ${userCollections.length} user collection(s)`);
|
|
660
|
+
this.state.updateStep('backfill_tracking', 'completed');
|
|
661
|
+
await this.state.save();
|
|
662
|
+
console.log('');
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// For each published memory in Memory_spaces_public, find the source memory
|
|
667
|
+
// and update its space_ids
|
|
668
|
+
const spacesCollectionExists = await this.client.collections.exists('Memory_spaces_public');
|
|
669
|
+
if (!spacesCollectionExists) {
|
|
670
|
+
console.log(' No Memory_spaces_public collection to backfill from');
|
|
671
|
+
this.state.updateStep('backfill_tracking', 'completed');
|
|
672
|
+
await this.state.save();
|
|
673
|
+
console.log('');
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const spacesCollection = this.client.collections.get('Memory_spaces_public');
|
|
678
|
+
const aggregate = await spacesCollection.aggregate.overAll();
|
|
679
|
+
const totalCount = aggregate.totalCount || 0;
|
|
680
|
+
|
|
681
|
+
let offset = 0;
|
|
682
|
+
let updated = 0;
|
|
683
|
+
|
|
684
|
+
while (offset < totalCount) {
|
|
685
|
+
const result = await spacesCollection.query.fetchObjects({
|
|
686
|
+
limit: this.config.options.batchSize,
|
|
687
|
+
offset,
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
if (result.objects.length === 0) break;
|
|
691
|
+
|
|
692
|
+
for (const doc of result.objects) {
|
|
693
|
+
const props = doc.properties as Record<string, any>;
|
|
694
|
+
const compositeId = doc.uuid;
|
|
695
|
+
|
|
696
|
+
// Parse composite ID: {userId}.{memoryId}
|
|
697
|
+
const dotIndex = compositeId.indexOf('.');
|
|
698
|
+
if (dotIndex === -1) continue;
|
|
699
|
+
|
|
700
|
+
const userId = compositeId.substring(0, dotIndex);
|
|
701
|
+
const memoryId = compositeId.substring(dotIndex + 1);
|
|
702
|
+
const spaceIds = (props.space_ids as string[]) || [];
|
|
703
|
+
|
|
704
|
+
if (spaceIds.length === 0) continue;
|
|
705
|
+
|
|
706
|
+
// Find the user's v2 collection
|
|
707
|
+
const userV2Name = `Memory_users_${userId}`;
|
|
708
|
+
const userColExists = await this.client.collections.exists(userV2Name);
|
|
709
|
+
if (!userColExists) continue;
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
const userCollection = this.client.collections.get(userV2Name);
|
|
713
|
+
await userCollection.data.update({
|
|
714
|
+
id: memoryId,
|
|
715
|
+
properties: {
|
|
716
|
+
space_ids: spaceIds,
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
updated++;
|
|
720
|
+
} catch {
|
|
721
|
+
// Memory might not exist in user collection — skip
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
offset += result.objects.length;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
console.log(` Updated ${updated} source memories with tracking arrays`);
|
|
729
|
+
this.state.updateStep('backfill_tracking', 'completed');
|
|
730
|
+
await this.state.save();
|
|
731
|
+
console.log('');
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// --------------------------------------------------------------------------
|
|
735
|
+
// Step 7: Verify
|
|
736
|
+
// --------------------------------------------------------------------------
|
|
737
|
+
|
|
738
|
+
async verify(collections: CollectionClassification[]): Promise<boolean> {
|
|
739
|
+
console.log('Step 7: Verifying migration...');
|
|
740
|
+
|
|
741
|
+
if (this.config.options.dryRun) {
|
|
742
|
+
console.log(' [dry-run] Skipping verification — v2 collections were not created');
|
|
743
|
+
this.state.addStep('verify', 'skipped');
|
|
744
|
+
await this.state.save();
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
this.state.addStep('verify', 'in_progress');
|
|
749
|
+
this.state.setStatus('verifying');
|
|
750
|
+
await this.state.save();
|
|
751
|
+
|
|
752
|
+
let allPassed = true;
|
|
753
|
+
|
|
754
|
+
// Check 1: Document count validation
|
|
755
|
+
for (const col of collections) {
|
|
756
|
+
if (!col.v2Name) continue;
|
|
757
|
+
|
|
758
|
+
const srcCol = this.client.collections.get(col.name);
|
|
759
|
+
const srcAggregate = await srcCol.aggregate.overAll();
|
|
760
|
+
const srcCount = srcAggregate.totalCount || 0;
|
|
761
|
+
|
|
762
|
+
if (col.type === 'user') {
|
|
763
|
+
const dstCol = this.client.collections.get(col.v2Name);
|
|
764
|
+
const dstAggregate = await dstCol.aggregate.overAll();
|
|
765
|
+
const dstCount = dstAggregate.totalCount || 0;
|
|
766
|
+
|
|
767
|
+
const passed = dstCount >= srcCount;
|
|
768
|
+
this.state.addVerificationCheck(
|
|
769
|
+
`count:${col.name}`,
|
|
770
|
+
passed,
|
|
771
|
+
`source=${srcCount}, dest=${dstCount}`,
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
if (!passed) {
|
|
775
|
+
console.log(` [FAIL] ${col.name}: source=${srcCount}, dest=${dstCount}`);
|
|
776
|
+
allPassed = false;
|
|
777
|
+
} else {
|
|
778
|
+
console.log(` [OK] ${col.name}: ${dstCount} docs (source: ${srcCount})`);
|
|
779
|
+
}
|
|
780
|
+
} else {
|
|
781
|
+
// Public/space collections merged — check that Memory_spaces_public exists
|
|
782
|
+
const dstExists = await this.client.collections.exists('Memory_spaces_public');
|
|
783
|
+
const passed = dstExists;
|
|
784
|
+
this.state.addVerificationCheck(
|
|
785
|
+
`exists:Memory_spaces_public`,
|
|
786
|
+
passed,
|
|
787
|
+
`exists=${dstExists}`,
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
if (!passed) {
|
|
791
|
+
console.log(` [FAIL] Memory_spaces_public does not exist`);
|
|
792
|
+
allPassed = false;
|
|
793
|
+
} else {
|
|
794
|
+
const dstCol = this.client.collections.get('Memory_spaces_public');
|
|
795
|
+
const dstAggregate = await dstCol.aggregate.overAll();
|
|
796
|
+
console.log(` [OK] Memory_spaces_public: ${dstAggregate.totalCount || 0} docs`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Check 2: Composite ID format validation (sample)
|
|
802
|
+
const spacesExists = await this.client.collections.exists('Memory_spaces_public');
|
|
803
|
+
if (spacesExists) {
|
|
804
|
+
const spacesCol = this.client.collections.get('Memory_spaces_public');
|
|
805
|
+
const sample = await spacesCol.query.fetchObjects({ limit: 10 });
|
|
806
|
+
|
|
807
|
+
let compositeValid = 0;
|
|
808
|
+
let compositeInvalid = 0;
|
|
809
|
+
|
|
810
|
+
for (const doc of sample.objects) {
|
|
811
|
+
if (doc.uuid.includes('.')) {
|
|
812
|
+
compositeValid++;
|
|
813
|
+
} else {
|
|
814
|
+
compositeInvalid++;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const passed = compositeInvalid === 0 || sample.objects.length === 0;
|
|
819
|
+
this.state.addVerificationCheck(
|
|
820
|
+
'composite_ids',
|
|
821
|
+
passed,
|
|
822
|
+
`valid=${compositeValid}, invalid=${compositeInvalid} (sample of ${sample.objects.length})`,
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
if (!passed) {
|
|
826
|
+
console.log(` [WARN] Composite IDs: ${compositeInvalid} of ${sample.objects.length} lack dot format`);
|
|
827
|
+
} else {
|
|
828
|
+
console.log(` [OK] Composite IDs: ${compositeValid} valid in sample`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Check 3: Tracking array consistency (sample)
|
|
833
|
+
for (const col of collections.filter(c => c.type === 'user' && c.v2Name)) {
|
|
834
|
+
const userCol = this.client.collections.get(col.v2Name!);
|
|
835
|
+
const sample = await userCol.query.fetchObjects({ limit: 5 });
|
|
836
|
+
|
|
837
|
+
let hasTrackingArrays = 0;
|
|
838
|
+
for (const doc of sample.objects) {
|
|
839
|
+
const props = doc.properties as Record<string, any>;
|
|
840
|
+
if (Array.isArray(props.space_ids) && Array.isArray(props.group_ids)) {
|
|
841
|
+
hasTrackingArrays++;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const passed = hasTrackingArrays === sample.objects.length || sample.objects.length === 0;
|
|
846
|
+
this.state.addVerificationCheck(
|
|
847
|
+
`tracking_arrays:${col.v2Name}`,
|
|
848
|
+
passed,
|
|
849
|
+
`${hasTrackingArrays}/${sample.objects.length} have tracking arrays`,
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
if (!passed) {
|
|
853
|
+
console.log(` [WARN] ${col.v2Name}: ${hasTrackingArrays}/${sample.objects.length} have tracking arrays`);
|
|
854
|
+
} else {
|
|
855
|
+
console.log(` [OK] ${col.v2Name}: tracking arrays present`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
this.state.setVerificationPassed(allPassed);
|
|
860
|
+
this.state.updateStep('verify', allPassed ? 'completed' : 'failed');
|
|
861
|
+
await this.state.save();
|
|
862
|
+
console.log(`\n Verification: ${allPassed ? 'PASSED' : 'FAILED'}\n`);
|
|
863
|
+
|
|
864
|
+
return allPassed;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// --------------------------------------------------------------------------
|
|
868
|
+
// Helpers
|
|
869
|
+
// --------------------------------------------------------------------------
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Transform v1 property names to v2 property names.
|
|
873
|
+
*/
|
|
874
|
+
private transformProperties(props: Record<string, any>): Record<string, any> {
|
|
875
|
+
const result: Record<string, any> = {};
|
|
876
|
+
|
|
877
|
+
for (const [key, value] of Object.entries(props)) {
|
|
878
|
+
if (key === '_additional') continue; // Skip Weaviate internals
|
|
879
|
+
|
|
880
|
+
const v2Key = PROPERTY_RENAMES[key] || key;
|
|
881
|
+
// Only write renamed key if the v2 key doesn't already have a value
|
|
882
|
+
if (v2Key !== key && result[v2Key] !== undefined) {
|
|
883
|
+
// v2 key already set, skip v1 value
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
result[v2Key] = value;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return result;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Copy all documents from one collection to another (for backup).
|
|
894
|
+
*/
|
|
895
|
+
private async copyCollection(srcName: string, dstName: string, transform: boolean): Promise<void> {
|
|
896
|
+
const srcCollection = this.client.collections.get(srcName);
|
|
897
|
+
|
|
898
|
+
// Get source schema and create destination with same schema
|
|
899
|
+
const srcSchema = await srcCollection.config.get();
|
|
900
|
+
await this.client.collections.create({
|
|
901
|
+
name: dstName,
|
|
902
|
+
vectorizers: weaviate.configure.vectorizer.text2VecOpenAI({
|
|
903
|
+
model: 'text-embedding-3-small',
|
|
904
|
+
sourceProperties: ['content', 'title', 'summary', 'observation'],
|
|
905
|
+
}),
|
|
906
|
+
invertedIndex: weaviate.configure.invertedIndex({
|
|
907
|
+
indexNullState: true,
|
|
908
|
+
}),
|
|
909
|
+
properties: srcSchema.properties as any,
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
const dstCollection = this.client.collections.get(dstName);
|
|
913
|
+
|
|
914
|
+
// Copy documents in batches
|
|
915
|
+
const aggregate = await srcCollection.aggregate.overAll();
|
|
916
|
+
const totalCount = aggregate.totalCount || 0;
|
|
917
|
+
let offset = 0;
|
|
918
|
+
|
|
919
|
+
while (offset < totalCount) {
|
|
920
|
+
const result = await srcCollection.query.fetchObjects({
|
|
921
|
+
limit: this.config.options.batchSize,
|
|
922
|
+
offset,
|
|
923
|
+
includeVector: true,
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
if (result.objects.length === 0) break;
|
|
927
|
+
|
|
928
|
+
const objects = result.objects.map(doc => ({
|
|
929
|
+
properties: transform
|
|
930
|
+
? this.transformProperties(doc.properties as Record<string, any>)
|
|
931
|
+
: doc.properties,
|
|
932
|
+
vectors: doc.vectors,
|
|
933
|
+
uuid: doc.uuid,
|
|
934
|
+
}));
|
|
935
|
+
|
|
936
|
+
await dstCollection.data.insertMany(objects);
|
|
937
|
+
offset += result.objects.length;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// --------------------------------------------------------------------------
|
|
942
|
+
// Main
|
|
943
|
+
// --------------------------------------------------------------------------
|
|
944
|
+
|
|
945
|
+
async run(): Promise<void> {
|
|
946
|
+
console.log('='.repeat(60));
|
|
947
|
+
console.log(' V1 -> V2 Weaviate Migration');
|
|
948
|
+
console.log('='.repeat(60));
|
|
949
|
+
console.log(` Weaviate URL: ${this.config.weaviate.url}`);
|
|
950
|
+
console.log(` Batch Size: ${this.config.options.batchSize}`);
|
|
951
|
+
console.log(` Dry Run: ${this.config.options.dryRun}`);
|
|
952
|
+
console.log(` Skip Backup: ${this.config.options.skipBackup}`);
|
|
953
|
+
console.log(` Verify Only: ${this.config.options.verifyOnly}`);
|
|
954
|
+
console.log('='.repeat(60));
|
|
955
|
+
console.log('');
|
|
956
|
+
|
|
957
|
+
await this.state.initialize();
|
|
958
|
+
|
|
959
|
+
try {
|
|
960
|
+
await this.connect();
|
|
961
|
+
|
|
962
|
+
// Step 1: Discover
|
|
963
|
+
const collections = await this.discover();
|
|
964
|
+
|
|
965
|
+
if (collections.length === 0) {
|
|
966
|
+
console.log('No v1 collections found to migrate. Done.\n');
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Verify-only mode
|
|
971
|
+
if (this.config.options.verifyOnly) {
|
|
972
|
+
await this.verify(collections);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Step 2: Backup
|
|
977
|
+
await this.backup(collections);
|
|
978
|
+
|
|
979
|
+
// Step 3: Create v2 collections
|
|
980
|
+
await this.createV2Collections(collections);
|
|
981
|
+
|
|
982
|
+
// Step 4: Copy user memories
|
|
983
|
+
await this.copyUserMemories(collections);
|
|
984
|
+
|
|
985
|
+
// Step 5: Copy/merge published memories
|
|
986
|
+
await this.copyPublishedMemories(collections);
|
|
987
|
+
|
|
988
|
+
// Step 6: Backfill tracking arrays
|
|
989
|
+
await this.backfillTrackingArrays(collections);
|
|
990
|
+
|
|
991
|
+
// Step 7: Verify
|
|
992
|
+
const passed = await this.verify(collections);
|
|
993
|
+
|
|
994
|
+
// Summary
|
|
995
|
+
console.log('='.repeat(60));
|
|
996
|
+
if (this.config.options.dryRun) {
|
|
997
|
+
console.log(' DRY RUN COMPLETE — no changes were made');
|
|
998
|
+
} else if (passed) {
|
|
999
|
+
this.state.setStatus('completed');
|
|
1000
|
+
await this.state.save();
|
|
1001
|
+
await this.state.cleanup();
|
|
1002
|
+
console.log(' MIGRATION COMPLETE');
|
|
1003
|
+
console.log('');
|
|
1004
|
+
console.log(' Next steps:');
|
|
1005
|
+
console.log(' 1. Apply code switch (Deliverable 3)');
|
|
1006
|
+
console.log(' 2. Run full test suite');
|
|
1007
|
+
console.log(' 3. Deploy');
|
|
1008
|
+
console.log(' 4. Verify production functionality');
|
|
1009
|
+
console.log(' 5. Delete v1 and backup collections');
|
|
1010
|
+
} else {
|
|
1011
|
+
this.state.setStatus('failed');
|
|
1012
|
+
await this.state.save();
|
|
1013
|
+
console.log(' MIGRATION COMPLETED WITH WARNINGS');
|
|
1014
|
+
console.log(' Review verification results above');
|
|
1015
|
+
}
|
|
1016
|
+
console.log('='.repeat(60));
|
|
1017
|
+
console.log('');
|
|
1018
|
+
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
this.state.setStatus('failed');
|
|
1021
|
+
this.state.addError('global', (error as Error).message);
|
|
1022
|
+
await this.state.save();
|
|
1023
|
+
throw error;
|
|
1024
|
+
} finally {
|
|
1025
|
+
await this.disconnect();
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// ============================================================================
|
|
1031
|
+
// Configuration
|
|
1032
|
+
// ============================================================================
|
|
1033
|
+
|
|
1034
|
+
function loadConfig(): MigrationConfig {
|
|
1035
|
+
const args = process.argv.slice(2);
|
|
1036
|
+
const cliArgs: Record<string, string> = {};
|
|
1037
|
+
|
|
1038
|
+
for (let i = 0; i < args.length; i++) {
|
|
1039
|
+
if (args[i].startsWith('--')) {
|
|
1040
|
+
const key = args[i].slice(2);
|
|
1041
|
+
const value = args[i + 1];
|
|
1042
|
+
if (value && !value.startsWith('--')) {
|
|
1043
|
+
cliArgs[key] = value;
|
|
1044
|
+
i++;
|
|
1045
|
+
} else {
|
|
1046
|
+
cliArgs[key] = 'true';
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const config: MigrationConfig = {
|
|
1052
|
+
weaviate: {
|
|
1053
|
+
url: cliArgs['weaviate-url'] || process.env.WEAVIATE_REST_URL || '',
|
|
1054
|
+
apiKey: cliArgs['weaviate-key'] || process.env.WEAVIATE_API_KEY,
|
|
1055
|
+
openaiApiKey: cliArgs['openai-key'] || process.env.OPENAI_EMBEDDINGS_API_KEY,
|
|
1056
|
+
},
|
|
1057
|
+
options: {
|
|
1058
|
+
batchSize: parseInt(cliArgs['batch-size'] || '100'),
|
|
1059
|
+
dryRun: cliArgs['dry-run'] === 'true',
|
|
1060
|
+
skipBackup: cliArgs['skip-backup'] === 'true',
|
|
1061
|
+
verifyOnly: cliArgs['verify-only'] === 'true',
|
|
1062
|
+
stateFile: cliArgs['state-file'] || '.v1-to-v2-migration-state.yaml',
|
|
1063
|
+
},
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
if (!config.weaviate.url) {
|
|
1067
|
+
throw new Error('Weaviate URL is required (--weaviate-url or WEAVIATE_REST_URL)');
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
return config;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// ============================================================================
|
|
1074
|
+
// Main
|
|
1075
|
+
// ============================================================================
|
|
1076
|
+
|
|
1077
|
+
async function main() {
|
|
1078
|
+
try {
|
|
1079
|
+
const config = loadConfig();
|
|
1080
|
+
const migration = new V1ToV2Migration(config);
|
|
1081
|
+
await migration.run();
|
|
1082
|
+
process.exit(0);
|
|
1083
|
+
} catch (error: any) {
|
|
1084
|
+
console.error(`\nFatal error: ${error.message}`);
|
|
1085
|
+
console.error(error.stack);
|
|
1086
|
+
process.exit(1);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
1091
|
+
main();
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
export { V1ToV2Migration, type MigrationConfig };
|