@mrc2204/agent-smart-memo 4.1.3 → 5.0.1
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/README.md +445 -141
- package/dist/adapters/openclaw/tool-runtime.d.ts +29 -0
- package/dist/adapters/openclaw/tool-runtime.d.ts.map +1 -0
- package/dist/adapters/openclaw/tool-runtime.js +48 -0
- package/dist/adapters/openclaw/tool-runtime.js.map +1 -0
- package/dist/commands/telegram-addproject-command.d.ts +33 -0
- package/dist/commands/telegram-addproject-command.d.ts.map +1 -0
- package/dist/commands/telegram-addproject-command.js +208 -0
- package/dist/commands/telegram-addproject-command.js.map +1 -0
- package/dist/core/contracts/adapter-contracts.d.ts +31 -0
- package/dist/core/contracts/adapter-contracts.d.ts.map +1 -0
- package/dist/core/contracts/adapter-contracts.js +2 -0
- package/dist/core/contracts/adapter-contracts.js.map +1 -0
- package/dist/core/runtime-boundary.d.ts +23 -0
- package/dist/core/runtime-boundary.d.ts.map +1 -0
- package/dist/core/runtime-boundary.js +39 -0
- package/dist/core/runtime-boundary.js.map +1 -0
- package/dist/core/usecases/default-memory-usecase-port.d.ts +54 -0
- package/dist/core/usecases/default-memory-usecase-port.d.ts.map +1 -0
- package/dist/core/usecases/default-memory-usecase-port.js +1139 -0
- package/dist/core/usecases/default-memory-usecase-port.js.map +1 -0
- package/dist/core/usecases/semantic-memory-usecase.d.ts +52 -0
- package/dist/core/usecases/semantic-memory-usecase.d.ts.map +1 -0
- package/dist/core/usecases/semantic-memory-usecase.js +136 -0
- package/dist/core/usecases/semantic-memory-usecase.js.map +1 -0
- package/dist/db/slot-db.d.ts +293 -0
- package/dist/db/slot-db.d.ts.map +1 -1
- package/dist/db/slot-db.js +1272 -0
- package/dist/db/slot-db.js.map +1 -1
- package/dist/index.d.ts +537 -64
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +239 -99
- package/dist/index.js.map +1 -1
- package/dist/services/qdrant.d.ts.map +1 -1
- package/dist/services/qdrant.js +17 -0
- package/dist/services/qdrant.js.map +1 -1
- package/dist/tools/graph-tools.d.ts +2 -0
- package/dist/tools/graph-tools.d.ts.map +1 -1
- package/dist/tools/graph-tools.js +126 -177
- package/dist/tools/graph-tools.js.map +1 -1
- package/dist/tools/project-tools.d.ts +8 -0
- package/dist/tools/project-tools.d.ts.map +1 -0
- package/dist/tools/project-tools.js +649 -0
- package/dist/tools/project-tools.js.map +1 -0
- package/dist/tools/semantic-memory-tools.d.ts +8 -0
- package/dist/tools/semantic-memory-tools.d.ts.map +1 -0
- package/dist/tools/semantic-memory-tools.js +111 -0
- package/dist/tools/semantic-memory-tools.js.map +1 -0
- package/dist/tools/slot-tools.d.ts +3 -1
- package/dist/tools/slot-tools.d.ts.map +1 -1
- package/dist/tools/slot-tools.js +82 -156
- package/dist/tools/slot-tools.js.map +1 -1
- package/openclaw.plugin.json +22 -2
- package/package.json +25 -32
- package/dist/config.d.ts +0 -62
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -102
- package/dist/config.js.map +0 -1
- package/dist/scripts/reembed-collection.d.ts +0 -2
- package/dist/scripts/reembed-collection.d.ts.map +0 -1
- package/dist/scripts/reembed-collection.js +0 -165
- package/dist/scripts/reembed-collection.js.map +0 -1
- package/dist/tools/memory_search.d.ts +0 -89
- package/dist/tools/memory_search.d.ts.map +0 -1
- package/dist/tools/memory_search.js +0 -188
- package/dist/tools/memory_search.js.map +0 -1
- package/dist/tools/memory_store.d.ts +0 -65
- package/dist/tools/memory_store.d.ts.map +0 -1
- package/dist/tools/memory_store.js +0 -196
- package/dist/tools/memory_store.js.map +0 -1
package/dist/db/slot-db.js
CHANGED
|
@@ -77,6 +77,221 @@ export class SlotDB {
|
|
|
77
77
|
this.db.exec(`
|
|
78
78
|
CREATE INDEX IF NOT EXISTS idx_slots_updated
|
|
79
79
|
ON slots(updated_at DESC)
|
|
80
|
+
`);
|
|
81
|
+
this.db.exec(`
|
|
82
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
83
|
+
project_id TEXT NOT NULL,
|
|
84
|
+
scope_user_id TEXT NOT NULL DEFAULT '',
|
|
85
|
+
scope_agent_id TEXT NOT NULL DEFAULT '',
|
|
86
|
+
project_name TEXT NOT NULL,
|
|
87
|
+
repo_root TEXT,
|
|
88
|
+
repo_remote_primary TEXT,
|
|
89
|
+
active_version TEXT,
|
|
90
|
+
lifecycle_status TEXT NOT NULL DEFAULT 'active',
|
|
91
|
+
created_at TEXT NOT NULL,
|
|
92
|
+
updated_at TEXT NOT NULL,
|
|
93
|
+
PRIMARY KEY (scope_user_id, scope_agent_id, project_id)
|
|
94
|
+
)
|
|
95
|
+
`);
|
|
96
|
+
this.db.exec(`
|
|
97
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_scope_repo_root
|
|
98
|
+
ON projects(scope_user_id, scope_agent_id, repo_root)
|
|
99
|
+
WHERE repo_root IS NOT NULL AND repo_root != ''
|
|
100
|
+
`);
|
|
101
|
+
this.db.exec(`
|
|
102
|
+
CREATE TABLE IF NOT EXISTS project_aliases (
|
|
103
|
+
id TEXT PRIMARY KEY,
|
|
104
|
+
project_id TEXT NOT NULL,
|
|
105
|
+
scope_user_id TEXT NOT NULL DEFAULT '',
|
|
106
|
+
scope_agent_id TEXT NOT NULL DEFAULT '',
|
|
107
|
+
project_alias TEXT NOT NULL,
|
|
108
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
109
|
+
created_at TEXT NOT NULL,
|
|
110
|
+
updated_at TEXT NOT NULL,
|
|
111
|
+
UNIQUE(scope_user_id, scope_agent_id, project_alias)
|
|
112
|
+
)
|
|
113
|
+
`);
|
|
114
|
+
this.db.exec(`
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_project_aliases_project
|
|
116
|
+
ON project_aliases(scope_user_id, scope_agent_id, project_id)
|
|
117
|
+
`);
|
|
118
|
+
this.db.exec(`
|
|
119
|
+
CREATE TABLE IF NOT EXISTS project_tracker_mappings (
|
|
120
|
+
id TEXT PRIMARY KEY,
|
|
121
|
+
project_id TEXT NOT NULL,
|
|
122
|
+
scope_user_id TEXT NOT NULL DEFAULT '',
|
|
123
|
+
scope_agent_id TEXT NOT NULL DEFAULT '',
|
|
124
|
+
tracker_type TEXT NOT NULL,
|
|
125
|
+
tracker_space_key TEXT,
|
|
126
|
+
tracker_project_id TEXT,
|
|
127
|
+
default_epic_key TEXT,
|
|
128
|
+
board_key TEXT,
|
|
129
|
+
active_version TEXT,
|
|
130
|
+
external_project_url TEXT,
|
|
131
|
+
created_at TEXT NOT NULL,
|
|
132
|
+
updated_at TEXT NOT NULL,
|
|
133
|
+
UNIQUE(scope_user_id, scope_agent_id, project_id, tracker_type)
|
|
134
|
+
)
|
|
135
|
+
`);
|
|
136
|
+
this.db.exec(`
|
|
137
|
+
CREATE TABLE IF NOT EXISTS project_registration_state (
|
|
138
|
+
project_id TEXT NOT NULL,
|
|
139
|
+
scope_user_id TEXT NOT NULL DEFAULT '',
|
|
140
|
+
scope_agent_id TEXT NOT NULL DEFAULT '',
|
|
141
|
+
registration_status TEXT NOT NULL,
|
|
142
|
+
validation_status TEXT NOT NULL,
|
|
143
|
+
validation_notes TEXT,
|
|
144
|
+
completeness_score INTEGER NOT NULL DEFAULT 0,
|
|
145
|
+
missing_required_fields TEXT,
|
|
146
|
+
last_validated_at TEXT,
|
|
147
|
+
updated_at TEXT NOT NULL,
|
|
148
|
+
PRIMARY KEY (scope_user_id, scope_agent_id, project_id)
|
|
149
|
+
)
|
|
150
|
+
`);
|
|
151
|
+
// ASM-76 (v5.1) bootstrap: metadata/control plane schema for ingest & reindex lifecycle.
|
|
152
|
+
this.db.exec(`
|
|
153
|
+
CREATE TABLE IF NOT EXISTS index_runs (
|
|
154
|
+
run_id TEXT NOT NULL,
|
|
155
|
+
scope_user_id TEXT NOT NULL DEFAULT '',
|
|
156
|
+
scope_agent_id TEXT NOT NULL DEFAULT '',
|
|
157
|
+
project_id TEXT NOT NULL,
|
|
158
|
+
index_profile TEXT NOT NULL,
|
|
159
|
+
trigger_type TEXT NOT NULL,
|
|
160
|
+
state TEXT NOT NULL,
|
|
161
|
+
started_at TEXT NOT NULL,
|
|
162
|
+
finished_at TEXT,
|
|
163
|
+
error_message TEXT,
|
|
164
|
+
PRIMARY KEY (scope_user_id, scope_agent_id, run_id)
|
|
165
|
+
)
|
|
166
|
+
`);
|
|
167
|
+
this.db.exec(`
|
|
168
|
+
CREATE INDEX IF NOT EXISTS idx_index_runs_project_state
|
|
169
|
+
ON index_runs(scope_user_id, scope_agent_id, project_id, state, started_at)
|
|
170
|
+
`);
|
|
171
|
+
this.db.exec(`
|
|
172
|
+
CREATE TABLE IF NOT EXISTS file_index_state (
|
|
173
|
+
file_id TEXT NOT NULL,
|
|
174
|
+
scope_user_id TEXT NOT NULL DEFAULT '',
|
|
175
|
+
scope_agent_id TEXT NOT NULL DEFAULT '',
|
|
176
|
+
project_id TEXT NOT NULL,
|
|
177
|
+
relative_path TEXT NOT NULL,
|
|
178
|
+
module TEXT,
|
|
179
|
+
language TEXT,
|
|
180
|
+
checksum TEXT NOT NULL,
|
|
181
|
+
last_commit_sha TEXT,
|
|
182
|
+
index_state TEXT NOT NULL,
|
|
183
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
184
|
+
tombstone_at TEXT,
|
|
185
|
+
indexed_at TEXT,
|
|
186
|
+
PRIMARY KEY (scope_user_id, scope_agent_id, file_id),
|
|
187
|
+
UNIQUE(scope_user_id, scope_agent_id, project_id, relative_path)
|
|
188
|
+
)
|
|
189
|
+
`);
|
|
190
|
+
this.db.exec(`
|
|
191
|
+
CREATE INDEX IF NOT EXISTS idx_file_state_project_path
|
|
192
|
+
ON file_index_state(scope_user_id, scope_agent_id, project_id, relative_path)
|
|
193
|
+
`);
|
|
194
|
+
this.db.exec(`
|
|
195
|
+
CREATE TABLE IF NOT EXISTS chunk_registry (
|
|
196
|
+
chunk_id TEXT NOT NULL,
|
|
197
|
+
scope_user_id TEXT NOT NULL DEFAULT '',
|
|
198
|
+
scope_agent_id TEXT NOT NULL DEFAULT '',
|
|
199
|
+
project_id TEXT NOT NULL,
|
|
200
|
+
file_id TEXT,
|
|
201
|
+
relative_path TEXT,
|
|
202
|
+
chunk_kind TEXT NOT NULL,
|
|
203
|
+
symbol_id TEXT,
|
|
204
|
+
task_id TEXT,
|
|
205
|
+
checksum TEXT NOT NULL,
|
|
206
|
+
qdrant_point_id TEXT,
|
|
207
|
+
index_state TEXT NOT NULL,
|
|
208
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
209
|
+
tombstone_at TEXT,
|
|
210
|
+
indexed_at TEXT,
|
|
211
|
+
PRIMARY KEY (scope_user_id, scope_agent_id, chunk_id)
|
|
212
|
+
)
|
|
213
|
+
`);
|
|
214
|
+
this.db.exec(`
|
|
215
|
+
CREATE INDEX IF NOT EXISTS idx_chunk_project_state
|
|
216
|
+
ON chunk_registry(scope_user_id, scope_agent_id, project_id, index_state, active)
|
|
217
|
+
`);
|
|
218
|
+
this.db.exec(`
|
|
219
|
+
CREATE TABLE IF NOT EXISTS symbol_registry (
|
|
220
|
+
symbol_id TEXT NOT NULL,
|
|
221
|
+
scope_user_id TEXT NOT NULL DEFAULT '',
|
|
222
|
+
scope_agent_id TEXT NOT NULL DEFAULT '',
|
|
223
|
+
project_id TEXT NOT NULL,
|
|
224
|
+
relative_path TEXT NOT NULL,
|
|
225
|
+
module TEXT,
|
|
226
|
+
language TEXT NOT NULL,
|
|
227
|
+
symbol_name TEXT NOT NULL,
|
|
228
|
+
symbol_fqn TEXT NOT NULL,
|
|
229
|
+
symbol_kind TEXT NOT NULL,
|
|
230
|
+
signature_hash TEXT,
|
|
231
|
+
index_state TEXT NOT NULL,
|
|
232
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
233
|
+
tombstone_at TEXT,
|
|
234
|
+
indexed_at TEXT,
|
|
235
|
+
PRIMARY KEY (scope_user_id, scope_agent_id, symbol_id)
|
|
236
|
+
)
|
|
237
|
+
`);
|
|
238
|
+
this.db.exec(`
|
|
239
|
+
CREATE INDEX IF NOT EXISTS idx_symbol_project_module_name
|
|
240
|
+
ON symbol_registry(scope_user_id, scope_agent_id, project_id, module, symbol_name)
|
|
241
|
+
`);
|
|
242
|
+
this.db.exec(`
|
|
243
|
+
CREATE TABLE IF NOT EXISTS task_registry (
|
|
244
|
+
task_id TEXT NOT NULL,
|
|
245
|
+
scope_user_id TEXT NOT NULL DEFAULT '',
|
|
246
|
+
scope_agent_id TEXT NOT NULL DEFAULT '',
|
|
247
|
+
project_id TEXT NOT NULL,
|
|
248
|
+
task_title TEXT NOT NULL,
|
|
249
|
+
task_type TEXT,
|
|
250
|
+
task_status TEXT,
|
|
251
|
+
parent_task_id TEXT,
|
|
252
|
+
related_task_ids TEXT,
|
|
253
|
+
files_touched TEXT,
|
|
254
|
+
symbols_touched TEXT,
|
|
255
|
+
commit_refs TEXT,
|
|
256
|
+
diff_refs TEXT,
|
|
257
|
+
decision_notes TEXT,
|
|
258
|
+
tracker_issue_key TEXT,
|
|
259
|
+
updated_at TEXT NOT NULL,
|
|
260
|
+
PRIMARY KEY (scope_user_id, scope_agent_id, task_id)
|
|
261
|
+
)
|
|
262
|
+
`);
|
|
263
|
+
this.db.exec(`
|
|
264
|
+
CREATE INDEX IF NOT EXISTS idx_task_project_parent
|
|
265
|
+
ON task_registry(scope_user_id, scope_agent_id, project_id, parent_task_id)
|
|
266
|
+
`);
|
|
267
|
+
this.db.exec(`
|
|
268
|
+
CREATE TABLE IF NOT EXISTS migration_state (
|
|
269
|
+
migration_id TEXT NOT NULL,
|
|
270
|
+
scope_user_id TEXT NOT NULL DEFAULT '',
|
|
271
|
+
scope_agent_id TEXT NOT NULL DEFAULT '',
|
|
272
|
+
schema_from TEXT NOT NULL,
|
|
273
|
+
schema_to TEXT NOT NULL,
|
|
274
|
+
applied_at TEXT NOT NULL,
|
|
275
|
+
status TEXT NOT NULL,
|
|
276
|
+
notes TEXT,
|
|
277
|
+
PRIMARY KEY (scope_user_id, scope_agent_id, migration_id)
|
|
278
|
+
)
|
|
279
|
+
`);
|
|
280
|
+
// ASM-78 (v5.1) incremental reindex watch-state + diff/checksum control plane.
|
|
281
|
+
this.db.exec(`
|
|
282
|
+
CREATE TABLE IF NOT EXISTS project_index_watch_state (
|
|
283
|
+
project_id TEXT NOT NULL,
|
|
284
|
+
scope_user_id TEXT NOT NULL DEFAULT '',
|
|
285
|
+
scope_agent_id TEXT NOT NULL DEFAULT '',
|
|
286
|
+
last_source_rev TEXT,
|
|
287
|
+
last_checksum_snapshot TEXT NOT NULL DEFAULT '{}',
|
|
288
|
+
updated_at TEXT NOT NULL,
|
|
289
|
+
PRIMARY KEY (scope_user_id, scope_agent_id, project_id)
|
|
290
|
+
)
|
|
291
|
+
`);
|
|
292
|
+
this.db.exec(`
|
|
293
|
+
CREATE INDEX IF NOT EXISTS idx_project_watch_updated
|
|
294
|
+
ON project_index_watch_state(scope_user_id, scope_agent_id, updated_at)
|
|
80
295
|
`);
|
|
81
296
|
}
|
|
82
297
|
// --------------------------------------------------------------------------
|
|
@@ -222,9 +437,1066 @@ export class SlotDB {
|
|
|
222
437
|
const result = stmt.get(scopeUserId, scopeAgentId);
|
|
223
438
|
return result.cnt;
|
|
224
439
|
}
|
|
440
|
+
registerProject(scopeUserId, scopeAgentId, input) {
|
|
441
|
+
const projectAlias = this.normalizeProjectAlias(input.project_alias);
|
|
442
|
+
if (!projectAlias) {
|
|
443
|
+
throw new Error("project_alias is required");
|
|
444
|
+
}
|
|
445
|
+
const now = new Date().toISOString();
|
|
446
|
+
const projectId = this.normalizeProjectId(input.project_id) || randomUUID();
|
|
447
|
+
const projectName = this.normalizeProjectName(input.project_name) || projectAlias;
|
|
448
|
+
const normalizedRepoRoot = this.normalizeRepoRoot(input.repo_root);
|
|
449
|
+
const normalizedRepoRemote = this.normalizeRepoRemote(input.repo_remote);
|
|
450
|
+
const existingAlias = this.getProjectByAlias(scopeUserId, scopeAgentId, projectAlias);
|
|
451
|
+
if (existingAlias && existingAlias.project.project_id !== projectId && !input.allow_alias_update) {
|
|
452
|
+
throw new Error(`project_alias \"${projectAlias}\" is already mapped to another project_id`);
|
|
453
|
+
}
|
|
454
|
+
let targetProjectId = projectId;
|
|
455
|
+
const existingByRepoRoot = input.reuse_existing_repo_root && normalizedRepoRoot
|
|
456
|
+
? this.findProjectByRepoRoot(scopeUserId, scopeAgentId, normalizedRepoRoot)
|
|
457
|
+
: null;
|
|
458
|
+
if (existingByRepoRoot) {
|
|
459
|
+
targetProjectId = existingByRepoRoot.project_id;
|
|
460
|
+
}
|
|
461
|
+
const existing = this.getProjectById(scopeUserId, scopeAgentId, targetProjectId);
|
|
462
|
+
if (existing) {
|
|
463
|
+
const updateProject = this.db.prepare(`UPDATE projects
|
|
464
|
+
SET project_name = ?, repo_root = ?, repo_remote_primary = ?, active_version = ?, updated_at = ?
|
|
465
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`);
|
|
466
|
+
updateProject.run(projectName || existing.project_name, normalizedRepoRoot ?? existing.repo_root, normalizedRepoRemote ?? existing.repo_remote_primary, input.active_version ?? existing.active_version, now, scopeUserId, scopeAgentId, targetProjectId);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
const insertProject = this.db.prepare(`INSERT INTO projects (
|
|
470
|
+
project_id, scope_user_id, scope_agent_id, project_name, repo_root, repo_remote_primary, active_version, lifecycle_status, created_at, updated_at
|
|
471
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)`);
|
|
472
|
+
insertProject.run(targetProjectId, scopeUserId, scopeAgentId, projectName, normalizedRepoRoot, normalizedRepoRemote, input.active_version || null, now, now);
|
|
473
|
+
}
|
|
474
|
+
this.upsertProjectAlias(scopeUserId, scopeAgentId, targetProjectId, projectAlias, true, now, input.allow_alias_update === true);
|
|
475
|
+
const project = this.getProjectById(scopeUserId, scopeAgentId, targetProjectId);
|
|
476
|
+
if (!project)
|
|
477
|
+
throw new Error("failed to persist project registry record");
|
|
478
|
+
const registration = this.upsertProjectRegistrationState(scopeUserId, scopeAgentId, {
|
|
479
|
+
project_id: targetProjectId,
|
|
480
|
+
registration_status: "registered",
|
|
481
|
+
validation_status: "ok",
|
|
482
|
+
validation_notes: null,
|
|
483
|
+
completeness_score: this.computeRegistrationCompleteness(project, projectAlias),
|
|
484
|
+
missing_required_fields: this.computeMissingRegistrationFields(project, projectAlias),
|
|
485
|
+
last_validated_at: now,
|
|
486
|
+
});
|
|
487
|
+
const alias = this.getProjectAlias(scopeUserId, scopeAgentId, projectAlias);
|
|
488
|
+
if (!alias)
|
|
489
|
+
throw new Error("failed to persist project alias");
|
|
490
|
+
return { project, alias, registration };
|
|
491
|
+
}
|
|
492
|
+
findProjectByRepoRoot(scopeUserId, scopeAgentId, repoRoot) {
|
|
493
|
+
const normalizedRepoRoot = this.normalizeRepoRoot(repoRoot);
|
|
494
|
+
if (!normalizedRepoRoot)
|
|
495
|
+
return null;
|
|
496
|
+
const stmt = this.db.prepare(`SELECT * FROM projects WHERE scope_user_id = ? AND scope_agent_id = ? AND repo_root = ? LIMIT 1`);
|
|
497
|
+
const row = stmt.get(scopeUserId, scopeAgentId, normalizedRepoRoot);
|
|
498
|
+
return row || null;
|
|
499
|
+
}
|
|
500
|
+
getProjectById(scopeUserId, scopeAgentId, projectId) {
|
|
501
|
+
const stmt = this.db.prepare(`SELECT * FROM projects WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`);
|
|
502
|
+
const row = stmt.get(scopeUserId, scopeAgentId, projectId);
|
|
503
|
+
return row || null;
|
|
504
|
+
}
|
|
505
|
+
getProjectAlias(scopeUserId, scopeAgentId, projectAlias) {
|
|
506
|
+
const normalizedAlias = this.normalizeProjectAlias(projectAlias);
|
|
507
|
+
const stmt = this.db.prepare(`SELECT * FROM project_aliases WHERE scope_user_id = ? AND scope_agent_id = ? AND project_alias = ?`);
|
|
508
|
+
const row = stmt.get(scopeUserId, scopeAgentId, normalizedAlias);
|
|
509
|
+
return row || null;
|
|
510
|
+
}
|
|
511
|
+
getProjectByAlias(scopeUserId, scopeAgentId, projectAlias) {
|
|
512
|
+
const normalizedAlias = this.normalizeProjectAlias(projectAlias);
|
|
513
|
+
const stmt = this.db.prepare(`SELECT p.project_id, p.scope_user_id, p.scope_agent_id, p.project_name, p.repo_root, p.repo_remote_primary,
|
|
514
|
+
p.active_version, p.lifecycle_status, p.created_at, p.updated_at,
|
|
515
|
+
a.id as alias_id, a.project_alias, a.is_primary, a.created_at as alias_created_at, a.updated_at as alias_updated_at
|
|
516
|
+
FROM project_aliases a
|
|
517
|
+
JOIN projects p ON p.project_id = a.project_id
|
|
518
|
+
AND p.scope_user_id = a.scope_user_id
|
|
519
|
+
AND p.scope_agent_id = a.scope_agent_id
|
|
520
|
+
WHERE a.scope_user_id = ? AND a.scope_agent_id = ? AND a.project_alias = ?`);
|
|
521
|
+
const row = stmt.get(scopeUserId, scopeAgentId, normalizedAlias);
|
|
522
|
+
if (!row)
|
|
523
|
+
return null;
|
|
524
|
+
return {
|
|
525
|
+
project: {
|
|
526
|
+
project_id: String(row.project_id),
|
|
527
|
+
scope_user_id: String(row.scope_user_id),
|
|
528
|
+
scope_agent_id: String(row.scope_agent_id),
|
|
529
|
+
project_name: String(row.project_name),
|
|
530
|
+
repo_root: row.repo_root ? String(row.repo_root) : null,
|
|
531
|
+
repo_remote_primary: row.repo_remote_primary ? String(row.repo_remote_primary) : null,
|
|
532
|
+
active_version: row.active_version ? String(row.active_version) : null,
|
|
533
|
+
lifecycle_status: String(row.lifecycle_status),
|
|
534
|
+
created_at: String(row.created_at),
|
|
535
|
+
updated_at: String(row.updated_at),
|
|
536
|
+
},
|
|
537
|
+
alias: {
|
|
538
|
+
id: String(row.alias_id),
|
|
539
|
+
project_id: String(row.project_id),
|
|
540
|
+
scope_user_id: String(row.scope_user_id),
|
|
541
|
+
scope_agent_id: String(row.scope_agent_id),
|
|
542
|
+
project_alias: String(row.project_alias),
|
|
543
|
+
is_primary: Number(row.is_primary),
|
|
544
|
+
created_at: String(row.alias_created_at),
|
|
545
|
+
updated_at: String(row.alias_updated_at),
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
listProjects(scopeUserId, scopeAgentId) {
|
|
550
|
+
const projectsStmt = this.db.prepare(`SELECT * FROM projects WHERE scope_user_id = ? AND scope_agent_id = ? ORDER BY updated_at DESC`);
|
|
551
|
+
const projects = projectsStmt.all(scopeUserId, scopeAgentId);
|
|
552
|
+
const aliasesStmt = this.db.prepare(`SELECT * FROM project_aliases WHERE scope_user_id = ? AND scope_agent_id = ? ORDER BY is_primary DESC, project_alias ASC`);
|
|
553
|
+
const aliases = aliasesStmt.all(scopeUserId, scopeAgentId);
|
|
554
|
+
const aliasesByProject = new Map();
|
|
555
|
+
for (const alias of aliases) {
|
|
556
|
+
const list = aliasesByProject.get(alias.project_id) || [];
|
|
557
|
+
list.push(alias);
|
|
558
|
+
aliasesByProject.set(alias.project_id, list);
|
|
559
|
+
}
|
|
560
|
+
return projects.map((project) => ({
|
|
561
|
+
project,
|
|
562
|
+
aliases: aliasesByProject.get(project.project_id) || [],
|
|
563
|
+
registration: this.getProjectRegistrationState(scopeUserId, scopeAgentId, project.project_id),
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
setProjectTrackerMapping(scopeUserId, scopeAgentId, input) {
|
|
567
|
+
const now = new Date().toISOString();
|
|
568
|
+
const existing = this.getProjectTrackerMapping(scopeUserId, scopeAgentId, input.project_id, input.tracker_type);
|
|
569
|
+
if (existing) {
|
|
570
|
+
const stmt = this.db.prepare(`UPDATE project_tracker_mappings
|
|
571
|
+
SET tracker_space_key = ?, tracker_project_id = ?, default_epic_key = ?, board_key = ?,
|
|
572
|
+
active_version = ?, external_project_url = ?, updated_at = ?
|
|
573
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND tracker_type = ?`);
|
|
574
|
+
stmt.run(input.tracker_space_key || null, input.tracker_project_id || null, input.default_epic_key || null, input.board_key || null, input.active_version || null, input.external_project_url || null, now, scopeUserId, scopeAgentId, input.project_id, input.tracker_type);
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
const stmt = this.db.prepare(`INSERT INTO project_tracker_mappings (
|
|
578
|
+
id, project_id, scope_user_id, scope_agent_id, tracker_type, tracker_space_key, tracker_project_id,
|
|
579
|
+
default_epic_key, board_key, active_version, external_project_url, created_at, updated_at
|
|
580
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
581
|
+
stmt.run(randomUUID(), input.project_id, scopeUserId, scopeAgentId, input.tracker_type, input.tracker_space_key || null, input.tracker_project_id || null, input.default_epic_key || null, input.board_key || null, input.active_version || null, input.external_project_url || null, now, now);
|
|
582
|
+
}
|
|
583
|
+
const mapping = this.getProjectTrackerMapping(scopeUserId, scopeAgentId, input.project_id, input.tracker_type);
|
|
584
|
+
if (!mapping)
|
|
585
|
+
throw new Error("failed to persist project tracker mapping");
|
|
586
|
+
return mapping;
|
|
587
|
+
}
|
|
588
|
+
getProjectTrackerMapping(scopeUserId, scopeAgentId, projectId, trackerType) {
|
|
589
|
+
const stmt = this.db.prepare(`SELECT * FROM project_tracker_mappings WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND tracker_type = ?`);
|
|
590
|
+
const row = stmt.get(scopeUserId, scopeAgentId, projectId, trackerType);
|
|
591
|
+
return row || null;
|
|
592
|
+
}
|
|
593
|
+
getProjectRegistrationState(scopeUserId, scopeAgentId, projectId) {
|
|
594
|
+
const stmt = this.db.prepare(`SELECT * FROM project_registration_state WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`);
|
|
595
|
+
const row = stmt.get(scopeUserId, scopeAgentId, projectId);
|
|
596
|
+
if (!row)
|
|
597
|
+
return null;
|
|
598
|
+
return {
|
|
599
|
+
project_id: row.project_id,
|
|
600
|
+
scope_user_id: row.scope_user_id,
|
|
601
|
+
scope_agent_id: row.scope_agent_id,
|
|
602
|
+
registration_status: row.registration_status,
|
|
603
|
+
validation_status: row.validation_status,
|
|
604
|
+
validation_notes: row.validation_notes,
|
|
605
|
+
completeness_score: row.completeness_score,
|
|
606
|
+
missing_required_fields: this.parseJsonArrayField(row.missing_required_fields),
|
|
607
|
+
last_validated_at: row.last_validated_at,
|
|
608
|
+
updated_at: row.updated_at,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
updateProjectRegistrationState(scopeUserId, scopeAgentId, input) {
|
|
612
|
+
return this.upsertProjectRegistrationState(scopeUserId, scopeAgentId, {
|
|
613
|
+
...input,
|
|
614
|
+
validation_notes: input.validation_notes ?? null,
|
|
615
|
+
last_validated_at: input.last_validated_at ?? new Date().toISOString(),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
getProjectIndexWatchState(scopeUserId, scopeAgentId, projectId) {
|
|
619
|
+
const stmt = this.db.prepare(`SELECT * FROM project_index_watch_state WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`);
|
|
620
|
+
const row = stmt.get(scopeUserId, scopeAgentId, projectId);
|
|
621
|
+
if (!row)
|
|
622
|
+
return null;
|
|
623
|
+
return {
|
|
624
|
+
project_id: row.project_id,
|
|
625
|
+
scope_user_id: row.scope_user_id,
|
|
626
|
+
scope_agent_id: row.scope_agent_id,
|
|
627
|
+
last_source_rev: row.last_source_rev,
|
|
628
|
+
last_checksum_snapshot: this.parseChecksumMap(row.last_checksum_snapshot),
|
|
629
|
+
updated_at: row.updated_at,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
reindexProjectByDiff(scopeUserId, scopeAgentId, input) {
|
|
633
|
+
const now = new Date().toISOString();
|
|
634
|
+
const runId = randomUUID();
|
|
635
|
+
const triggerType = input.trigger_type || "incremental";
|
|
636
|
+
const indexProfile = (input.index_profile || "default").trim() || "default";
|
|
637
|
+
const sourceRev = input.source_rev?.trim() || null;
|
|
638
|
+
if (!input.project_id || !String(input.project_id).trim()) {
|
|
639
|
+
throw new Error("project_id is required");
|
|
640
|
+
}
|
|
641
|
+
const project = this.getProjectById(scopeUserId, scopeAgentId, input.project_id);
|
|
642
|
+
if (!project) {
|
|
643
|
+
throw new Error(`project_id '${input.project_id}' is not registered`);
|
|
644
|
+
}
|
|
645
|
+
const watch = this.getProjectIndexWatchState(scopeUserId, scopeAgentId, input.project_id);
|
|
646
|
+
const previousSnapshot = watch?.last_checksum_snapshot || {};
|
|
647
|
+
const currentSnapshot = new Map();
|
|
648
|
+
for (const item of input.paths || []) {
|
|
649
|
+
const relativePath = this.normalizeRelativePath(item.relative_path);
|
|
650
|
+
if (!relativePath)
|
|
651
|
+
continue;
|
|
652
|
+
const checksum = (item.checksum || "").trim() || "__missing__";
|
|
653
|
+
currentSnapshot.set(relativePath, checksum);
|
|
654
|
+
}
|
|
655
|
+
const changed = [];
|
|
656
|
+
const unchanged = [];
|
|
657
|
+
for (const [relativePath, checksum] of currentSnapshot.entries()) {
|
|
658
|
+
const prev = previousSnapshot[relativePath];
|
|
659
|
+
if (!prev || prev !== checksum)
|
|
660
|
+
changed.push(relativePath);
|
|
661
|
+
else
|
|
662
|
+
unchanged.push(relativePath);
|
|
663
|
+
}
|
|
664
|
+
const deleted = [];
|
|
665
|
+
for (const prevPath of Object.keys(previousSnapshot)) {
|
|
666
|
+
if (!currentSnapshot.has(prevPath))
|
|
667
|
+
deleted.push(prevPath);
|
|
668
|
+
}
|
|
669
|
+
this.insertIndexRun(scopeUserId, scopeAgentId, {
|
|
670
|
+
run_id: runId,
|
|
671
|
+
project_id: input.project_id,
|
|
672
|
+
index_profile: indexProfile,
|
|
673
|
+
trigger_type: triggerType,
|
|
674
|
+
state: "indexing",
|
|
675
|
+
started_at: now,
|
|
676
|
+
finished_at: null,
|
|
677
|
+
error_message: null,
|
|
678
|
+
});
|
|
679
|
+
try {
|
|
680
|
+
const nowIso = new Date().toISOString();
|
|
681
|
+
for (const relativePath of changed) {
|
|
682
|
+
const item = (input.paths || []).find((p) => this.normalizeRelativePath(p.relative_path) === relativePath);
|
|
683
|
+
this.upsertFileIndexState(scopeUserId, scopeAgentId, {
|
|
684
|
+
file_id: this.makeScopedId(input.project_id, relativePath),
|
|
685
|
+
project_id: input.project_id,
|
|
686
|
+
relative_path: relativePath,
|
|
687
|
+
module: item?.module || null,
|
|
688
|
+
language: item?.language || null,
|
|
689
|
+
checksum: currentSnapshot.get(relativePath) || "__missing__",
|
|
690
|
+
last_commit_sha: sourceRev,
|
|
691
|
+
index_state: "indexed",
|
|
692
|
+
active: 1,
|
|
693
|
+
tombstone_at: null,
|
|
694
|
+
indexed_at: nowIso,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
for (const relativePath of deleted) {
|
|
698
|
+
this.markFileIndexStateDeleted(scopeUserId, scopeAgentId, input.project_id, relativePath, nowIso);
|
|
699
|
+
}
|
|
700
|
+
const checksumSnapshotRecord = Object.fromEntries(currentSnapshot.entries());
|
|
701
|
+
this.upsertProjectIndexWatchState(scopeUserId, scopeAgentId, {
|
|
702
|
+
project_id: input.project_id,
|
|
703
|
+
last_source_rev: sourceRev,
|
|
704
|
+
last_checksum_snapshot: checksumSnapshotRecord,
|
|
705
|
+
updated_at: nowIso,
|
|
706
|
+
});
|
|
707
|
+
this.finishIndexRun(scopeUserId, scopeAgentId, runId, "indexed", null, nowIso);
|
|
708
|
+
return {
|
|
709
|
+
run_id: runId,
|
|
710
|
+
project_id: input.project_id,
|
|
711
|
+
trigger_type: triggerType,
|
|
712
|
+
index_profile: indexProfile,
|
|
713
|
+
source_rev: sourceRev,
|
|
714
|
+
changed,
|
|
715
|
+
unchanged,
|
|
716
|
+
deleted,
|
|
717
|
+
run_state: "indexed",
|
|
718
|
+
watch_state: {
|
|
719
|
+
last_source_rev: sourceRev,
|
|
720
|
+
updated_at: nowIso,
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
const err = error instanceof Error ? error.message : String(error);
|
|
726
|
+
this.finishIndexRun(scopeUserId, scopeAgentId, runId, "error", err, new Date().toISOString());
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
upsertTaskRegistryRecord(scopeUserId, scopeAgentId, input) {
|
|
731
|
+
const now = new Date().toISOString();
|
|
732
|
+
const taskId = String(input.task_id || "").trim();
|
|
733
|
+
const projectId = String(input.project_id || "").trim();
|
|
734
|
+
const taskTitle = String(input.task_title || "").trim();
|
|
735
|
+
if (!taskId)
|
|
736
|
+
throw new Error("task_id is required");
|
|
737
|
+
if (!projectId)
|
|
738
|
+
throw new Error("project_id is required");
|
|
739
|
+
if (!taskTitle)
|
|
740
|
+
throw new Error("task_title is required");
|
|
741
|
+
const project = this.getProjectById(scopeUserId, scopeAgentId, projectId);
|
|
742
|
+
if (!project) {
|
|
743
|
+
throw new Error(`project_id '${projectId}' is not registered`);
|
|
744
|
+
}
|
|
745
|
+
const existing = this.getTaskRegistryRecordById(scopeUserId, scopeAgentId, taskId);
|
|
746
|
+
const relatedTaskIds = this.normalizeStringArray(input.related_task_ids);
|
|
747
|
+
const filesTouched = this.normalizeStringArray(input.files_touched).map((p) => this.normalizeRelativePath(p)).filter(Boolean);
|
|
748
|
+
const symbolsTouched = this.normalizeStringArray(input.symbols_touched);
|
|
749
|
+
const commitRefs = this.normalizeStringArray(input.commit_refs);
|
|
750
|
+
const diffRefs = this.normalizeStringArray(input.diff_refs);
|
|
751
|
+
if (existing) {
|
|
752
|
+
const stmt = this.db.prepare(`UPDATE task_registry
|
|
753
|
+
SET project_id = ?, task_title = ?, task_type = ?, task_status = ?, parent_task_id = ?,
|
|
754
|
+
related_task_ids = ?, files_touched = ?, symbols_touched = ?, commit_refs = ?, diff_refs = ?,
|
|
755
|
+
decision_notes = ?, tracker_issue_key = ?, updated_at = ?
|
|
756
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND task_id = ?`);
|
|
757
|
+
stmt.run(projectId, taskTitle, input.task_type ?? null, input.task_status ?? null, input.parent_task_id ?? null, JSON.stringify(relatedTaskIds), JSON.stringify(filesTouched), JSON.stringify(symbolsTouched), JSON.stringify(commitRefs), JSON.stringify(diffRefs), input.decision_notes ?? null, input.tracker_issue_key ?? null, now, scopeUserId, scopeAgentId, taskId);
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
const stmt = this.db.prepare(`INSERT INTO task_registry (
|
|
761
|
+
task_id, scope_user_id, scope_agent_id, project_id, task_title, task_type, task_status, parent_task_id,
|
|
762
|
+
related_task_ids, files_touched, symbols_touched, commit_refs, diff_refs, decision_notes, tracker_issue_key, updated_at
|
|
763
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
764
|
+
stmt.run(taskId, scopeUserId, scopeAgentId, projectId, taskTitle, input.task_type ?? null, input.task_status ?? null, input.parent_task_id ?? null, JSON.stringify(relatedTaskIds), JSON.stringify(filesTouched), JSON.stringify(symbolsTouched), JSON.stringify(commitRefs), JSON.stringify(diffRefs), input.decision_notes ?? null, input.tracker_issue_key ?? null, now);
|
|
765
|
+
}
|
|
766
|
+
const row = this.getTaskRegistryRecordById(scopeUserId, scopeAgentId, taskId);
|
|
767
|
+
if (!row)
|
|
768
|
+
throw new Error("failed to persist task registry record");
|
|
769
|
+
return row;
|
|
770
|
+
}
|
|
771
|
+
getTaskRegistryRecordById(scopeUserId, scopeAgentId, taskId) {
|
|
772
|
+
const normalizedTaskId = String(taskId || "").trim();
|
|
773
|
+
if (!normalizedTaskId)
|
|
774
|
+
return null;
|
|
775
|
+
const stmt = this.db.prepare(`SELECT * FROM task_registry WHERE scope_user_id = ? AND scope_agent_id = ? AND task_id = ?`);
|
|
776
|
+
const row = stmt.get(scopeUserId, scopeAgentId, normalizedTaskId);
|
|
777
|
+
if (!row)
|
|
778
|
+
return null;
|
|
779
|
+
return this.rowToTaskRecord(row);
|
|
780
|
+
}
|
|
781
|
+
getTaskRegistryRecordByTrackerIssueKey(scopeUserId, scopeAgentId, projectId, trackerIssueKey) {
|
|
782
|
+
const tracker = String(trackerIssueKey || "").trim();
|
|
783
|
+
if (!tracker)
|
|
784
|
+
return null;
|
|
785
|
+
const stmt = this.db.prepare(`SELECT * FROM task_registry
|
|
786
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND tracker_issue_key = ?
|
|
787
|
+
ORDER BY updated_at DESC LIMIT 1`);
|
|
788
|
+
const row = stmt.get(scopeUserId, scopeAgentId, projectId, tracker);
|
|
789
|
+
if (!row)
|
|
790
|
+
return null;
|
|
791
|
+
return this.rowToTaskRecord(row);
|
|
792
|
+
}
|
|
793
|
+
getTaskLineageContext(scopeUserId, scopeAgentId, input) {
|
|
794
|
+
const projectId = String(input.project_id || "").trim();
|
|
795
|
+
if (!projectId)
|
|
796
|
+
throw new Error("project_id is required");
|
|
797
|
+
const project = this.getProjectById(scopeUserId, scopeAgentId, projectId);
|
|
798
|
+
if (!project) {
|
|
799
|
+
throw new Error(`project_id '${projectId}' is not registered`);
|
|
800
|
+
}
|
|
801
|
+
let focus = null;
|
|
802
|
+
if (input.task_id) {
|
|
803
|
+
const byId = this.getTaskRegistryRecordById(scopeUserId, scopeAgentId, input.task_id);
|
|
804
|
+
if (byId && byId.project_id === projectId)
|
|
805
|
+
focus = byId;
|
|
806
|
+
}
|
|
807
|
+
if (!focus && input.tracker_issue_key) {
|
|
808
|
+
focus = this.getTaskRegistryRecordByTrackerIssueKey(scopeUserId, scopeAgentId, projectId, input.tracker_issue_key);
|
|
809
|
+
}
|
|
810
|
+
if (!focus && input.task_title) {
|
|
811
|
+
const stmt = this.db.prepare(`SELECT * FROM task_registry
|
|
812
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND lower(task_title) LIKE ?
|
|
813
|
+
ORDER BY updated_at DESC LIMIT 1`);
|
|
814
|
+
const row = stmt.get(scopeUserId, scopeAgentId, projectId, `%${String(input.task_title).trim().toLowerCase()}%`);
|
|
815
|
+
if (row)
|
|
816
|
+
focus = this.rowToTaskRecord(row);
|
|
817
|
+
}
|
|
818
|
+
if (!focus) {
|
|
819
|
+
throw new Error("task lineage focus not found for provided selector");
|
|
820
|
+
}
|
|
821
|
+
const includeParentChain = input.include_parent_chain !== false;
|
|
822
|
+
const includeRelated = input.include_related !== false;
|
|
823
|
+
const parentChain = [];
|
|
824
|
+
if (includeParentChain) {
|
|
825
|
+
let cursor = focus.parent_task_id;
|
|
826
|
+
const guard = new Set();
|
|
827
|
+
while (cursor && !guard.has(cursor)) {
|
|
828
|
+
guard.add(cursor);
|
|
829
|
+
const parent = this.getTaskRegistryRecordById(scopeUserId, scopeAgentId, cursor);
|
|
830
|
+
if (!parent)
|
|
831
|
+
break;
|
|
832
|
+
parentChain.push(parent);
|
|
833
|
+
cursor = parent.parent_task_id;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const relatedTasks = [];
|
|
837
|
+
if (includeRelated) {
|
|
838
|
+
const seen = new Set();
|
|
839
|
+
for (const relatedId of focus.related_task_ids || []) {
|
|
840
|
+
if (!relatedId || seen.has(relatedId))
|
|
841
|
+
continue;
|
|
842
|
+
seen.add(relatedId);
|
|
843
|
+
const related = this.getTaskRegistryRecordById(scopeUserId, scopeAgentId, relatedId);
|
|
844
|
+
if (related)
|
|
845
|
+
relatedTasks.push(related);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const aggregate = [focus, ...parentChain, ...relatedTasks];
|
|
849
|
+
const touchedFiles = this.uniqueSorted(aggregate.flatMap((t) => t.files_touched || []));
|
|
850
|
+
const touchedSymbols = this.uniqueSorted(aggregate.flatMap((t) => t.symbols_touched || []));
|
|
851
|
+
const commitRefs = this.uniqueSorted(aggregate.flatMap((t) => t.commit_refs || []));
|
|
852
|
+
const decisionNotes = this.uniqueSorted(aggregate
|
|
853
|
+
.map((t) => String(t.decision_notes || "").trim())
|
|
854
|
+
.filter(Boolean));
|
|
855
|
+
return {
|
|
856
|
+
focus: {
|
|
857
|
+
project_id: projectId,
|
|
858
|
+
task_id: focus.task_id,
|
|
859
|
+
tracker_issue_key: focus.tracker_issue_key,
|
|
860
|
+
task_title: focus.task_title,
|
|
861
|
+
},
|
|
862
|
+
parent_chain: parentChain,
|
|
863
|
+
related_tasks: relatedTasks,
|
|
864
|
+
touched_files: touchedFiles,
|
|
865
|
+
touched_symbols: touchedSymbols,
|
|
866
|
+
commit_refs: commitRefs,
|
|
867
|
+
decision_notes: decisionNotes,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
hybridSearchProjectContext(scopeUserId, scopeAgentId, input) {
|
|
871
|
+
const projectId = String(input.project_id || "").trim();
|
|
872
|
+
const query = String(input.query || "").trim();
|
|
873
|
+
if (!projectId)
|
|
874
|
+
throw new Error("project_id is required");
|
|
875
|
+
if (!query)
|
|
876
|
+
throw new Error("query is required");
|
|
877
|
+
const project = this.getProjectById(scopeUserId, scopeAgentId, projectId);
|
|
878
|
+
if (!project) {
|
|
879
|
+
throw new Error(`project_id '${projectId}' is not registered`);
|
|
880
|
+
}
|
|
881
|
+
const limit = Math.min(Math.max(Number(input.limit || 10), 1), 50);
|
|
882
|
+
const queryLc = query.toLowerCase();
|
|
883
|
+
const queryTokens = Array.from(new Set(queryLc.split(/[^a-z0-9._/-]+/i).map((t) => t.trim()).filter(Boolean)));
|
|
884
|
+
const tokenScore = (text) => {
|
|
885
|
+
if (!queryTokens.length)
|
|
886
|
+
return 0;
|
|
887
|
+
const hay = text.toLowerCase();
|
|
888
|
+
const words = Array.from(new Set(hay.split(/[^a-z0-9]+/i).map((t) => t.trim()).filter(Boolean)));
|
|
889
|
+
let matched = 0;
|
|
890
|
+
for (const token of queryTokens) {
|
|
891
|
+
if (hay.includes(token)) {
|
|
892
|
+
matched += 1;
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
if (words.some((word) => word.startsWith(token) || token.startsWith(word))) {
|
|
896
|
+
matched += 0.8;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return matched / queryTokens.length;
|
|
900
|
+
};
|
|
901
|
+
const taskContextInput = input.task_context;
|
|
902
|
+
let lineageContext = null;
|
|
903
|
+
if (taskContextInput && (taskContextInput.task_id || taskContextInput.tracker_issue_key || taskContextInput.task_title)) {
|
|
904
|
+
lineageContext = this.getTaskLineageContext(scopeUserId, scopeAgentId, {
|
|
905
|
+
project_id: projectId,
|
|
906
|
+
task_id: taskContextInput.task_id,
|
|
907
|
+
tracker_issue_key: taskContextInput.tracker_issue_key,
|
|
908
|
+
task_title: taskContextInput.task_title,
|
|
909
|
+
include_parent_chain: taskContextInput.include_parent_chain,
|
|
910
|
+
include_related: taskContextInput.include_related,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
const lexicalPathPrefix = this.normalizeStringArray(input.path_prefix).map((p) => this.normalizeRelativePath(p)).filter(Boolean);
|
|
914
|
+
const lexicalModules = new Set(this.normalizeStringArray(input.module).map((s) => s.toLowerCase()));
|
|
915
|
+
const lexicalLanguages = new Set(this.normalizeStringArray(input.language).map((s) => s.toLowerCase()));
|
|
916
|
+
const lexicalTaskIds = new Set(this.normalizeStringArray(input.task_id));
|
|
917
|
+
const lexicalIssueKeys = new Set(this.normalizeStringArray(input.tracker_issue_key).map((s) => s.toUpperCase()));
|
|
918
|
+
if (lineageContext) {
|
|
919
|
+
lexicalTaskIds.add(lineageContext.focus.task_id);
|
|
920
|
+
if (lineageContext.focus.tracker_issue_key)
|
|
921
|
+
lexicalIssueKeys.add(lineageContext.focus.tracker_issue_key.toUpperCase());
|
|
922
|
+
for (const t of [...lineageContext.parent_chain, ...lineageContext.related_tasks]) {
|
|
923
|
+
lexicalTaskIds.add(t.task_id);
|
|
924
|
+
if (t.tracker_issue_key)
|
|
925
|
+
lexicalIssueKeys.add(t.tracker_issue_key.toUpperCase());
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const results = [];
|
|
929
|
+
const fileStmt = this.db.prepare(`SELECT * FROM file_index_state
|
|
930
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1`);
|
|
931
|
+
const fileRows = fileStmt.all(scopeUserId, scopeAgentId, projectId);
|
|
932
|
+
for (const row of fileRows) {
|
|
933
|
+
const relativePath = String(row.relative_path || "");
|
|
934
|
+
const moduleName = row.module ? String(row.module) : null;
|
|
935
|
+
const language = row.language ? String(row.language) : null;
|
|
936
|
+
if (lexicalPathPrefix.length > 0 && !lexicalPathPrefix.some((prefix) => relativePath.startsWith(prefix)))
|
|
937
|
+
continue;
|
|
938
|
+
if (lexicalModules.size > 0 && !moduleName)
|
|
939
|
+
continue;
|
|
940
|
+
if (lexicalModules.size > 0 && moduleName && !lexicalModules.has(moduleName.toLowerCase()))
|
|
941
|
+
continue;
|
|
942
|
+
if (lexicalLanguages.size > 0 && !language)
|
|
943
|
+
continue;
|
|
944
|
+
if (lexicalLanguages.size > 0 && language && !lexicalLanguages.has(language.toLowerCase()))
|
|
945
|
+
continue;
|
|
946
|
+
const text = `${relativePath} ${moduleName || ""} ${language || ""}`.toLowerCase();
|
|
947
|
+
let score = 0;
|
|
948
|
+
if (text.includes(queryLc))
|
|
949
|
+
score += 0.55;
|
|
950
|
+
score += tokenScore(text) * 0.45;
|
|
951
|
+
if (lineageContext && lineageContext.touched_files.includes(relativePath))
|
|
952
|
+
score += 0.35;
|
|
953
|
+
if (relativePath.includes("README") || relativePath.includes("docs/"))
|
|
954
|
+
score += 0.05;
|
|
955
|
+
if (relativePath.includes("src/services/"))
|
|
956
|
+
score += 0.08;
|
|
957
|
+
if (relativePath.endsWith('.service.ts'))
|
|
958
|
+
score += 0.06;
|
|
959
|
+
if (score <= 0.12)
|
|
960
|
+
continue;
|
|
961
|
+
results.push({
|
|
962
|
+
source: "file_index_state",
|
|
963
|
+
id: String(row.file_id),
|
|
964
|
+
score,
|
|
965
|
+
project_id: projectId,
|
|
966
|
+
relative_path: relativePath,
|
|
967
|
+
module: moduleName,
|
|
968
|
+
language,
|
|
969
|
+
snippet: `file ${relativePath}${moduleName ? ` (module ${moduleName})` : ""}`,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
const symbolStmt = this.db.prepare(`SELECT * FROM symbol_registry
|
|
973
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1`);
|
|
974
|
+
const symbolRows = symbolStmt.all(scopeUserId, scopeAgentId, projectId);
|
|
975
|
+
for (const row of symbolRows) {
|
|
976
|
+
const relativePath = String(row.relative_path || "");
|
|
977
|
+
const moduleName = row.module ? String(row.module) : null;
|
|
978
|
+
const language = row.language ? String(row.language) : null;
|
|
979
|
+
const symbolName = String(row.symbol_name || "");
|
|
980
|
+
const symbolKind = String(row.symbol_kind || "");
|
|
981
|
+
const symbolFqn = String(row.symbol_fqn || "");
|
|
982
|
+
if (lexicalPathPrefix.length > 0 && !lexicalPathPrefix.some((prefix) => relativePath.startsWith(prefix)))
|
|
983
|
+
continue;
|
|
984
|
+
if (lexicalModules.size > 0 && !moduleName)
|
|
985
|
+
continue;
|
|
986
|
+
if (lexicalModules.size > 0 && moduleName && !lexicalModules.has(moduleName.toLowerCase()))
|
|
987
|
+
continue;
|
|
988
|
+
if (lexicalLanguages.size > 0 && !language)
|
|
989
|
+
continue;
|
|
990
|
+
if (lexicalLanguages.size > 0 && language && !lexicalLanguages.has(language.toLowerCase()))
|
|
991
|
+
continue;
|
|
992
|
+
const text = `${symbolName} ${symbolFqn} ${relativePath} ${moduleName || ""} ${symbolKind}`.toLowerCase();
|
|
993
|
+
let score = 0;
|
|
994
|
+
if (text.includes(queryLc))
|
|
995
|
+
score += 0.62;
|
|
996
|
+
score += tokenScore(text) * 0.4;
|
|
997
|
+
if (lineageContext && lineageContext.touched_symbols.includes(symbolName))
|
|
998
|
+
score += 0.3;
|
|
999
|
+
if (lineageContext && lineageContext.touched_files.includes(relativePath))
|
|
1000
|
+
score += 0.12;
|
|
1001
|
+
if (score <= 0.15)
|
|
1002
|
+
continue;
|
|
1003
|
+
results.push({
|
|
1004
|
+
source: "symbol_registry",
|
|
1005
|
+
id: String(row.symbol_id),
|
|
1006
|
+
score,
|
|
1007
|
+
project_id: projectId,
|
|
1008
|
+
relative_path: relativePath,
|
|
1009
|
+
module: moduleName,
|
|
1010
|
+
language,
|
|
1011
|
+
symbol_name: symbolName,
|
|
1012
|
+
symbol_kind: symbolKind,
|
|
1013
|
+
snippet: `symbol ${symbolName} (${symbolKind}) in ${relativePath}`,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
const taskStmt = this.db.prepare(`SELECT * FROM task_registry
|
|
1017
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`);
|
|
1018
|
+
const taskRows = taskStmt.all(scopeUserId, scopeAgentId, projectId);
|
|
1019
|
+
for (const row of taskRows) {
|
|
1020
|
+
const task = this.rowToTaskRecord(row);
|
|
1021
|
+
const taskIssueKey = task.tracker_issue_key ? task.tracker_issue_key.toUpperCase() : null;
|
|
1022
|
+
if (lexicalTaskIds.size > 0 && !lexicalTaskIds.has(task.task_id)) {
|
|
1023
|
+
if (!taskIssueKey || !lexicalIssueKeys.has(taskIssueKey)) {
|
|
1024
|
+
// keep if user query still lexically matches strongly
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const text = [
|
|
1028
|
+
task.task_id,
|
|
1029
|
+
task.task_title,
|
|
1030
|
+
task.task_status || "",
|
|
1031
|
+
task.tracker_issue_key || "",
|
|
1032
|
+
...(task.files_touched || []),
|
|
1033
|
+
...(task.symbols_touched || []),
|
|
1034
|
+
...(task.commit_refs || []),
|
|
1035
|
+
task.decision_notes || "",
|
|
1036
|
+
].join(" ").toLowerCase();
|
|
1037
|
+
let score = 0;
|
|
1038
|
+
if (text.includes(queryLc))
|
|
1039
|
+
score += 0.58;
|
|
1040
|
+
score += tokenScore(text) * 0.35;
|
|
1041
|
+
if (lexicalTaskIds.has(task.task_id))
|
|
1042
|
+
score += 0.28;
|
|
1043
|
+
if (taskIssueKey && lexicalIssueKeys.has(taskIssueKey))
|
|
1044
|
+
score += 0.28;
|
|
1045
|
+
if (lineageContext) {
|
|
1046
|
+
if (task.task_id === lineageContext.focus.task_id)
|
|
1047
|
+
score += 0.35;
|
|
1048
|
+
if (lineageContext.parent_chain.some((t) => t.task_id === task.task_id))
|
|
1049
|
+
score += 0.2;
|
|
1050
|
+
if (lineageContext.related_tasks.some((t) => t.task_id === task.task_id))
|
|
1051
|
+
score += 0.2;
|
|
1052
|
+
}
|
|
1053
|
+
if (score <= 0.15)
|
|
1054
|
+
continue;
|
|
1055
|
+
results.push({
|
|
1056
|
+
source: "task_registry",
|
|
1057
|
+
id: task.task_id,
|
|
1058
|
+
score,
|
|
1059
|
+
project_id: projectId,
|
|
1060
|
+
task_id: task.task_id,
|
|
1061
|
+
task_title: task.task_title,
|
|
1062
|
+
tracker_issue_key: task.tracker_issue_key,
|
|
1063
|
+
snippet: `task ${task.task_id}: ${task.task_title}`,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
const ranked = results
|
|
1067
|
+
.sort((a, b) => b.score - a.score)
|
|
1068
|
+
.slice(0, limit)
|
|
1069
|
+
.map((item) => ({ ...item, score: Number(item.score.toFixed(4)) }));
|
|
1070
|
+
return {
|
|
1071
|
+
query,
|
|
1072
|
+
project_id: projectId,
|
|
1073
|
+
count: ranked.length,
|
|
1074
|
+
task_lineage_context: lineageContext,
|
|
1075
|
+
results: ranked,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
runLegacyCompatibilityBackfill(scopeUserId, scopeAgentId, input = {}) {
|
|
1079
|
+
const mode = input.mode || "dry_run";
|
|
1080
|
+
const source = input.source || "mixed";
|
|
1081
|
+
const now = new Date().toISOString();
|
|
1082
|
+
const onlyProjectIds = new Set(this.normalizeStringArray(input.only_project_ids));
|
|
1083
|
+
const onlyAliases = new Set(this.normalizeStringArray(input.only_aliases).map((a) => this.normalizeProjectAlias(a)));
|
|
1084
|
+
const projects = this.listProjects(scopeUserId, scopeAgentId);
|
|
1085
|
+
const selected = projects.filter((row) => {
|
|
1086
|
+
if (onlyProjectIds.size > 0 && !onlyProjectIds.has(row.project.project_id))
|
|
1087
|
+
return false;
|
|
1088
|
+
if (onlyAliases.size > 0) {
|
|
1089
|
+
const aliases = row.aliases.map((a) => this.normalizeProjectAlias(a.project_alias));
|
|
1090
|
+
if (!aliases.some((a) => onlyAliases.has(a)))
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
return true;
|
|
1094
|
+
});
|
|
1095
|
+
let updatedAliases = 0;
|
|
1096
|
+
let updatedMappings = 0;
|
|
1097
|
+
let updatedRegistrations = 0;
|
|
1098
|
+
let migrationStateUpserts = 0;
|
|
1099
|
+
const items = [];
|
|
1100
|
+
for (const row of selected) {
|
|
1101
|
+
const project = row.project;
|
|
1102
|
+
const warnings = [];
|
|
1103
|
+
const actions = [];
|
|
1104
|
+
const existingAliases = new Set(row.aliases.map((a) => this.normalizeProjectAlias(a.project_alias)).filter(Boolean));
|
|
1105
|
+
const inferredAliases = this.inferBackfillAliases(project, existingAliases, source);
|
|
1106
|
+
const inferredMappings = this.inferBackfillTrackerMappings(scopeUserId, scopeAgentId, project.project_id, project, source);
|
|
1107
|
+
if (mode === "apply") {
|
|
1108
|
+
for (const alias of inferredAliases) {
|
|
1109
|
+
if (existingAliases.has(alias))
|
|
1110
|
+
continue;
|
|
1111
|
+
this.upsertProjectAlias(scopeUserId, scopeAgentId, project.project_id, alias, false, now, false);
|
|
1112
|
+
existingAliases.add(alias);
|
|
1113
|
+
updatedAliases += 1;
|
|
1114
|
+
actions.push(`alias.backfilled:${alias}`);
|
|
1115
|
+
}
|
|
1116
|
+
for (const mapping of inferredMappings) {
|
|
1117
|
+
const existing = this.getProjectTrackerMapping(scopeUserId, scopeAgentId, project.project_id, mapping.tracker_type);
|
|
1118
|
+
if (existing
|
|
1119
|
+
&& existing.tracker_space_key === mapping.tracker_space_key
|
|
1120
|
+
&& existing.default_epic_key === mapping.default_epic_key
|
|
1121
|
+
&& existing.tracker_project_id === mapping.tracker_project_id) {
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
this.setProjectTrackerMapping(scopeUserId, scopeAgentId, {
|
|
1125
|
+
project_id: project.project_id,
|
|
1126
|
+
tracker_type: mapping.tracker_type,
|
|
1127
|
+
tracker_space_key: mapping.tracker_space_key || undefined,
|
|
1128
|
+
tracker_project_id: mapping.tracker_project_id || undefined,
|
|
1129
|
+
default_epic_key: mapping.default_epic_key || undefined,
|
|
1130
|
+
});
|
|
1131
|
+
updatedMappings += 1;
|
|
1132
|
+
actions.push(`tracker.backfilled:${mapping.tracker_type}`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const primaryAlias = this.pickPrimaryAlias(existingAliases, project);
|
|
1136
|
+
const completeness = this.computeRegistrationCompleteness(project, primaryAlias);
|
|
1137
|
+
const missingFields = this.computeMissingRegistrationFields(project, primaryAlias);
|
|
1138
|
+
const hasTracker = inferredMappings.length > 0 || Boolean(this.getProjectTrackerMapping(scopeUserId, scopeAgentId, project.project_id, "jira"));
|
|
1139
|
+
const status = hasTracker && completeness >= 90 ? "validated" : "registered";
|
|
1140
|
+
const validationStatus = missingFields.length === 0 ? "ok" : "warn";
|
|
1141
|
+
const existingRegistration = this.getProjectRegistrationState(scopeUserId, scopeAgentId, project.project_id);
|
|
1142
|
+
const shouldUpdateRegistration = !existingRegistration
|
|
1143
|
+
|| input.force_registration_state === true
|
|
1144
|
+
|| existingRegistration.registration_status === "draft"
|
|
1145
|
+
|| existingRegistration.validation_status !== validationStatus;
|
|
1146
|
+
if (shouldUpdateRegistration) {
|
|
1147
|
+
if (mode === "apply") {
|
|
1148
|
+
this.upsertProjectRegistrationState(scopeUserId, scopeAgentId, {
|
|
1149
|
+
project_id: project.project_id,
|
|
1150
|
+
registration_status: status,
|
|
1151
|
+
validation_status: validationStatus,
|
|
1152
|
+
validation_notes: `legacy_backfill:${source}`,
|
|
1153
|
+
completeness_score: completeness,
|
|
1154
|
+
missing_required_fields: missingFields,
|
|
1155
|
+
last_validated_at: now,
|
|
1156
|
+
});
|
|
1157
|
+
updatedRegistrations += 1;
|
|
1158
|
+
actions.push("registration.backfilled");
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (mode === "apply") {
|
|
1162
|
+
this.upsertMigrationState(scopeUserId, scopeAgentId, {
|
|
1163
|
+
migration_id: `legacy-backfill:${project.project_id}`,
|
|
1164
|
+
schema_from: "legacy",
|
|
1165
|
+
schema_to: "5.1",
|
|
1166
|
+
applied_at: now,
|
|
1167
|
+
status: "migrated",
|
|
1168
|
+
notes: JSON.stringify({
|
|
1169
|
+
source,
|
|
1170
|
+
alias_count: inferredAliases.length,
|
|
1171
|
+
tracker_count: inferredMappings.length,
|
|
1172
|
+
}),
|
|
1173
|
+
});
|
|
1174
|
+
migrationStateUpserts += 1;
|
|
1175
|
+
}
|
|
1176
|
+
if (inferredAliases.length === 0) {
|
|
1177
|
+
warnings.push("no additional alias inferred");
|
|
1178
|
+
}
|
|
1179
|
+
if (inferredMappings.length === 0) {
|
|
1180
|
+
warnings.push("no tracker mapping inferred");
|
|
1181
|
+
}
|
|
1182
|
+
items.push({
|
|
1183
|
+
project_id: project.project_id,
|
|
1184
|
+
project_name: project.project_name,
|
|
1185
|
+
inferred_aliases: inferredAliases,
|
|
1186
|
+
inferred_tracker_mappings: inferredMappings,
|
|
1187
|
+
actions,
|
|
1188
|
+
warnings,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
return {
|
|
1192
|
+
mode,
|
|
1193
|
+
source,
|
|
1194
|
+
scanned_projects: projects.length,
|
|
1195
|
+
candidates: selected.length,
|
|
1196
|
+
updated_aliases: updatedAliases,
|
|
1197
|
+
updated_tracker_mappings: updatedMappings,
|
|
1198
|
+
updated_registration_states: updatedRegistrations,
|
|
1199
|
+
migration_state_upserts: migrationStateUpserts,
|
|
1200
|
+
items,
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
225
1203
|
// --------------------------------------------------------------------------
|
|
226
1204
|
// Helpers
|
|
227
1205
|
// --------------------------------------------------------------------------
|
|
1206
|
+
normalizeProjectAlias(alias) {
|
|
1207
|
+
return String(alias || "")
|
|
1208
|
+
.trim()
|
|
1209
|
+
.toLowerCase()
|
|
1210
|
+
.replace(/\s+/g, "-")
|
|
1211
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
1212
|
+
.replace(/-+/g, "-")
|
|
1213
|
+
.replace(/^-|-$/g, "");
|
|
1214
|
+
}
|
|
1215
|
+
normalizeRelativePath(pathInput) {
|
|
1216
|
+
const normalized = String(pathInput || "")
|
|
1217
|
+
.trim()
|
|
1218
|
+
.replace(/\\/g, "/")
|
|
1219
|
+
.replace(/^\.\//, "")
|
|
1220
|
+
.replace(/\/+/g, "/");
|
|
1221
|
+
return normalized;
|
|
1222
|
+
}
|
|
1223
|
+
makeScopedId(projectId, relativePath) {
|
|
1224
|
+
return `${projectId}::${relativePath}`;
|
|
1225
|
+
}
|
|
1226
|
+
parseChecksumMap(raw) {
|
|
1227
|
+
if (!raw)
|
|
1228
|
+
return {};
|
|
1229
|
+
try {
|
|
1230
|
+
const parsed = JSON.parse(raw);
|
|
1231
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
1232
|
+
return {};
|
|
1233
|
+
const out = {};
|
|
1234
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1235
|
+
if (typeof key === "string" && typeof value === "string") {
|
|
1236
|
+
out[key] = value;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return out;
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
return {};
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
insertIndexRun(scopeUserId, scopeAgentId, input) {
|
|
1246
|
+
const stmt = this.db.prepare(`INSERT INTO index_runs (
|
|
1247
|
+
run_id, scope_user_id, scope_agent_id, project_id, index_profile, trigger_type, state, started_at, finished_at, error_message
|
|
1248
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
1249
|
+
stmt.run(input.run_id, scopeUserId, scopeAgentId, input.project_id, input.index_profile, input.trigger_type, input.state, input.started_at, input.finished_at, input.error_message);
|
|
1250
|
+
}
|
|
1251
|
+
finishIndexRun(scopeUserId, scopeAgentId, runId, state, errorMessage, finishedAt) {
|
|
1252
|
+
const stmt = this.db.prepare(`UPDATE index_runs
|
|
1253
|
+
SET state = ?, finished_at = ?, error_message = ?
|
|
1254
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND run_id = ?`);
|
|
1255
|
+
stmt.run(state, finishedAt, errorMessage, scopeUserId, scopeAgentId, runId);
|
|
1256
|
+
}
|
|
1257
|
+
upsertFileIndexState(scopeUserId, scopeAgentId, input) {
|
|
1258
|
+
const existingStmt = this.db.prepare(`SELECT file_id FROM file_index_state
|
|
1259
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND relative_path = ?`);
|
|
1260
|
+
const existing = existingStmt.get(scopeUserId, scopeAgentId, input.project_id, input.relative_path);
|
|
1261
|
+
if (existing) {
|
|
1262
|
+
const updateStmt = this.db.prepare(`UPDATE file_index_state
|
|
1263
|
+
SET module = ?, language = ?, checksum = ?, last_commit_sha = ?, index_state = ?, active = ?, tombstone_at = ?, indexed_at = ?
|
|
1264
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND file_id = ?`);
|
|
1265
|
+
updateStmt.run(input.module, input.language, input.checksum, input.last_commit_sha, input.index_state, input.active, input.tombstone_at, input.indexed_at, scopeUserId, scopeAgentId, existing.file_id);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const insertStmt = this.db.prepare(`INSERT INTO file_index_state (
|
|
1269
|
+
file_id, scope_user_id, scope_agent_id, project_id, relative_path, module, language,
|
|
1270
|
+
checksum, last_commit_sha, index_state, active, tombstone_at, indexed_at
|
|
1271
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
1272
|
+
insertStmt.run(input.file_id, scopeUserId, scopeAgentId, input.project_id, input.relative_path, input.module, input.language, input.checksum, input.last_commit_sha, input.index_state, input.active, input.tombstone_at, input.indexed_at);
|
|
1273
|
+
}
|
|
1274
|
+
markFileIndexStateDeleted(scopeUserId, scopeAgentId, projectId, relativePath, tombstoneAt) {
|
|
1275
|
+
const stmt = this.db.prepare(`UPDATE file_index_state
|
|
1276
|
+
SET index_state = 'stale', active = 0, tombstone_at = ?, indexed_at = ?
|
|
1277
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND relative_path = ?`);
|
|
1278
|
+
stmt.run(tombstoneAt, tombstoneAt, scopeUserId, scopeAgentId, projectId, relativePath);
|
|
1279
|
+
}
|
|
1280
|
+
upsertProjectIndexWatchState(scopeUserId, scopeAgentId, input) {
|
|
1281
|
+
const existing = this.getProjectIndexWatchState(scopeUserId, scopeAgentId, input.project_id);
|
|
1282
|
+
const checksumJson = JSON.stringify(input.last_checksum_snapshot || {});
|
|
1283
|
+
if (existing) {
|
|
1284
|
+
const stmt = this.db.prepare(`UPDATE project_index_watch_state
|
|
1285
|
+
SET last_source_rev = ?, last_checksum_snapshot = ?, updated_at = ?
|
|
1286
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`);
|
|
1287
|
+
stmt.run(input.last_source_rev, checksumJson, input.updated_at, scopeUserId, scopeAgentId, input.project_id);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const stmt = this.db.prepare(`INSERT INTO project_index_watch_state (
|
|
1291
|
+
project_id, scope_user_id, scope_agent_id, last_source_rev, last_checksum_snapshot, updated_at
|
|
1292
|
+
) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
1293
|
+
stmt.run(input.project_id, scopeUserId, scopeAgentId, input.last_source_rev, checksumJson, input.updated_at);
|
|
1294
|
+
}
|
|
1295
|
+
normalizeProjectId(projectId) {
|
|
1296
|
+
return String(projectId || "").trim();
|
|
1297
|
+
}
|
|
1298
|
+
normalizeProjectName(projectName) {
|
|
1299
|
+
return String(projectName || "").trim();
|
|
1300
|
+
}
|
|
1301
|
+
normalizeRepoRoot(repoRoot) {
|
|
1302
|
+
const normalized = String(repoRoot || "").trim();
|
|
1303
|
+
return normalized || null;
|
|
1304
|
+
}
|
|
1305
|
+
normalizeRepoRemote(repoRemote) {
|
|
1306
|
+
const normalized = String(repoRemote || "").trim();
|
|
1307
|
+
return normalized || null;
|
|
1308
|
+
}
|
|
1309
|
+
parseJsonArrayField(raw) {
|
|
1310
|
+
if (!raw)
|
|
1311
|
+
return [];
|
|
1312
|
+
try {
|
|
1313
|
+
const parsed = JSON.parse(raw);
|
|
1314
|
+
return Array.isArray(parsed) ? parsed.map((item) => String(item)) : [];
|
|
1315
|
+
}
|
|
1316
|
+
catch {
|
|
1317
|
+
return [];
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
normalizeStringArray(input) {
|
|
1321
|
+
if (!Array.isArray(input))
|
|
1322
|
+
return [];
|
|
1323
|
+
return this.uniqueSorted(input
|
|
1324
|
+
.map((item) => String(item || "").trim())
|
|
1325
|
+
.filter(Boolean));
|
|
1326
|
+
}
|
|
1327
|
+
uniqueSorted(values) {
|
|
1328
|
+
return Array.from(new Set(values.map((v) => String(v || "").trim()).filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
1329
|
+
}
|
|
1330
|
+
inferBackfillAliases(project, existingAliases, source) {
|
|
1331
|
+
const candidates = new Set();
|
|
1332
|
+
if (source === "repo_root" || source === "mixed") {
|
|
1333
|
+
if (project.repo_root) {
|
|
1334
|
+
const parts = project.repo_root.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
1335
|
+
const leaf = parts[parts.length - 1] || "";
|
|
1336
|
+
const normalized = this.normalizeProjectAlias(leaf);
|
|
1337
|
+
if (normalized)
|
|
1338
|
+
candidates.add(normalized);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (source === "repo_remote" || source === "mixed") {
|
|
1342
|
+
if (project.repo_remote_primary) {
|
|
1343
|
+
const remote = String(project.repo_remote_primary);
|
|
1344
|
+
const m = remote.match(/[:\/]([^/]+?)\.git$/i) || remote.match(/[:\/]([^/]+?)$/i);
|
|
1345
|
+
const repoName = m?.[1] || "";
|
|
1346
|
+
const normalized = this.normalizeProjectAlias(repoName);
|
|
1347
|
+
if (normalized)
|
|
1348
|
+
candidates.add(normalized);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
const filtered = Array.from(candidates).filter((a) => !existingAliases.has(a));
|
|
1352
|
+
return filtered.sort((a, b) => a.localeCompare(b));
|
|
1353
|
+
}
|
|
1354
|
+
inferBackfillTrackerMappings(scopeUserId, scopeAgentId, projectId, project, source) {
|
|
1355
|
+
const result = [];
|
|
1356
|
+
if (source === "repo_remote" || source === "mixed") {
|
|
1357
|
+
const remote = String(project.repo_remote_primary || "").trim();
|
|
1358
|
+
if (remote.includes("github.com")) {
|
|
1359
|
+
result.push({
|
|
1360
|
+
tracker_type: "github",
|
|
1361
|
+
tracker_space_key: null,
|
|
1362
|
+
tracker_project_id: null,
|
|
1363
|
+
default_epic_key: null,
|
|
1364
|
+
confidence: 0.7,
|
|
1365
|
+
source: "repo_remote",
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (source === "task_registry" || source === "mixed") {
|
|
1370
|
+
const stmt = this.db.prepare(`SELECT tracker_issue_key FROM task_registry
|
|
1371
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND tracker_issue_key IS NOT NULL AND tracker_issue_key != ''
|
|
1372
|
+
ORDER BY updated_at DESC LIMIT 200`);
|
|
1373
|
+
const rows = stmt.all(scopeUserId, scopeAgentId, projectId);
|
|
1374
|
+
const keys = rows.map((r) => String(r.tracker_issue_key || "").trim()).filter(Boolean);
|
|
1375
|
+
const jiraLike = keys
|
|
1376
|
+
.map((key) => key.match(/^([A-Z][A-Z0-9_]+)-\d+$/)?.[1] || null)
|
|
1377
|
+
.filter((x) => Boolean(x));
|
|
1378
|
+
if (jiraLike.length > 0) {
|
|
1379
|
+
const counts = new Map();
|
|
1380
|
+
for (const p of jiraLike)
|
|
1381
|
+
counts.set(p, (counts.get(p) || 0) + 1);
|
|
1382
|
+
const top = Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0];
|
|
1383
|
+
if (top) {
|
|
1384
|
+
result.push({
|
|
1385
|
+
tracker_type: "jira",
|
|
1386
|
+
tracker_space_key: top[0],
|
|
1387
|
+
tracker_project_id: null,
|
|
1388
|
+
default_epic_key: `${top[0]}-1`,
|
|
1389
|
+
confidence: Math.min(0.95, 0.55 + top[1] * 0.03),
|
|
1390
|
+
source: "task_registry",
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
const dedup = new Map();
|
|
1396
|
+
for (const item of result) {
|
|
1397
|
+
const key = `${item.tracker_type}:${item.tracker_space_key || ""}:${item.default_epic_key || ""}`;
|
|
1398
|
+
const prev = dedup.get(key);
|
|
1399
|
+
if (!prev || item.confidence > prev.confidence)
|
|
1400
|
+
dedup.set(key, item);
|
|
1401
|
+
}
|
|
1402
|
+
return Array.from(dedup.values()).sort((a, b) => b.confidence - a.confidence);
|
|
1403
|
+
}
|
|
1404
|
+
pickPrimaryAlias(existingAliases, project) {
|
|
1405
|
+
const aliases = Array.from(existingAliases).filter(Boolean).sort((a, b) => a.localeCompare(b));
|
|
1406
|
+
if (aliases.length > 0)
|
|
1407
|
+
return aliases[0];
|
|
1408
|
+
const fromName = this.normalizeProjectAlias(project.project_name);
|
|
1409
|
+
if (fromName)
|
|
1410
|
+
return fromName;
|
|
1411
|
+
return this.normalizeProjectAlias(project.project_id) || "project";
|
|
1412
|
+
}
|
|
1413
|
+
upsertMigrationState(scopeUserId, scopeAgentId, input) {
|
|
1414
|
+
const existingStmt = this.db.prepare(`SELECT migration_id FROM migration_state
|
|
1415
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND migration_id = ?`);
|
|
1416
|
+
const existing = existingStmt.get(scopeUserId, scopeAgentId, input.migration_id);
|
|
1417
|
+
if (existing) {
|
|
1418
|
+
const updateStmt = this.db.prepare(`UPDATE migration_state
|
|
1419
|
+
SET schema_from = ?, schema_to = ?, applied_at = ?, status = ?, notes = ?
|
|
1420
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND migration_id = ?`);
|
|
1421
|
+
updateStmt.run(input.schema_from, input.schema_to, input.applied_at, input.status, input.notes || null, scopeUserId, scopeAgentId, input.migration_id);
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
const insertStmt = this.db.prepare(`INSERT INTO migration_state (
|
|
1425
|
+
migration_id, scope_user_id, scope_agent_id, schema_from, schema_to, applied_at, status, notes
|
|
1426
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
1427
|
+
insertStmt.run(input.migration_id, scopeUserId, scopeAgentId, input.schema_from, input.schema_to, input.applied_at, input.status, input.notes || null);
|
|
1428
|
+
}
|
|
1429
|
+
rowToTaskRecord(row) {
|
|
1430
|
+
return {
|
|
1431
|
+
task_id: row.task_id,
|
|
1432
|
+
scope_user_id: row.scope_user_id,
|
|
1433
|
+
scope_agent_id: row.scope_agent_id,
|
|
1434
|
+
project_id: row.project_id,
|
|
1435
|
+
task_title: row.task_title,
|
|
1436
|
+
task_type: row.task_type,
|
|
1437
|
+
task_status: row.task_status,
|
|
1438
|
+
parent_task_id: row.parent_task_id,
|
|
1439
|
+
related_task_ids: this.parseJsonArrayField(row.related_task_ids),
|
|
1440
|
+
files_touched: this.parseJsonArrayField(row.files_touched),
|
|
1441
|
+
symbols_touched: this.parseJsonArrayField(row.symbols_touched),
|
|
1442
|
+
commit_refs: this.parseJsonArrayField(row.commit_refs),
|
|
1443
|
+
diff_refs: this.parseJsonArrayField(row.diff_refs),
|
|
1444
|
+
decision_notes: row.decision_notes,
|
|
1445
|
+
tracker_issue_key: row.tracker_issue_key,
|
|
1446
|
+
updated_at: row.updated_at,
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
computeMissingRegistrationFields(project, alias) {
|
|
1450
|
+
const missing = [];
|
|
1451
|
+
if (!project.project_id)
|
|
1452
|
+
missing.push("project_id");
|
|
1453
|
+
if (!alias)
|
|
1454
|
+
missing.push("project_alias");
|
|
1455
|
+
if (!project.project_name)
|
|
1456
|
+
missing.push("project_name");
|
|
1457
|
+
return missing;
|
|
1458
|
+
}
|
|
1459
|
+
computeRegistrationCompleteness(project, alias) {
|
|
1460
|
+
const requiredTotal = 3;
|
|
1461
|
+
const requiredPresent = [project.project_id, alias, project.project_name].filter(Boolean).length;
|
|
1462
|
+
const optionalTotal = 3;
|
|
1463
|
+
const optionalPresent = [project.repo_root, project.repo_remote_primary, project.active_version].filter(Boolean).length;
|
|
1464
|
+
return Math.round((requiredPresent / requiredTotal) * 80 + (optionalPresent / optionalTotal) * 20);
|
|
1465
|
+
}
|
|
1466
|
+
upsertProjectAlias(scopeUserId, scopeAgentId, projectId, projectAlias, isPrimary, now, allowAliasUpdate) {
|
|
1467
|
+
const existingAlias = this.getProjectAlias(scopeUserId, scopeAgentId, projectAlias);
|
|
1468
|
+
if (existingAlias) {
|
|
1469
|
+
if (existingAlias.project_id !== projectId && !allowAliasUpdate) {
|
|
1470
|
+
throw new Error(`project_alias \"${projectAlias}\" is already mapped to another project_id`);
|
|
1471
|
+
}
|
|
1472
|
+
const stmt = this.db.prepare(`UPDATE project_aliases SET project_id = ?, is_primary = ?, updated_at = ? WHERE id = ?`);
|
|
1473
|
+
stmt.run(projectId, isPrimary ? 1 : 0, now, existingAlias.id);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
const insertStmt = this.db.prepare(`INSERT INTO project_aliases (id, project_id, scope_user_id, scope_agent_id, project_alias, is_primary, created_at, updated_at)
|
|
1477
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
1478
|
+
insertStmt.run(randomUUID(), projectId, scopeUserId, scopeAgentId, projectAlias, isPrimary ? 1 : 0, now, now);
|
|
1479
|
+
}
|
|
1480
|
+
upsertProjectRegistrationState(scopeUserId, scopeAgentId, input) {
|
|
1481
|
+
const now = new Date().toISOString();
|
|
1482
|
+
const existing = this.getProjectRegistrationState(scopeUserId, scopeAgentId, input.project_id);
|
|
1483
|
+
const missingJson = JSON.stringify(input.missing_required_fields || []);
|
|
1484
|
+
if (existing) {
|
|
1485
|
+
const stmt = this.db.prepare(`UPDATE project_registration_state
|
|
1486
|
+
SET registration_status = ?, validation_status = ?, validation_notes = ?, completeness_score = ?, missing_required_fields = ?, last_validated_at = ?, updated_at = ?
|
|
1487
|
+
WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`);
|
|
1488
|
+
stmt.run(input.registration_status, input.validation_status, input.validation_notes, input.completeness_score, missingJson, input.last_validated_at, now, scopeUserId, scopeAgentId, input.project_id);
|
|
1489
|
+
}
|
|
1490
|
+
else {
|
|
1491
|
+
const stmt = this.db.prepare(`INSERT INTO project_registration_state (project_id, scope_user_id, scope_agent_id, registration_status, validation_status, validation_notes, completeness_score, missing_required_fields, last_validated_at, updated_at)
|
|
1492
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
1493
|
+
stmt.run(input.project_id, scopeUserId, scopeAgentId, input.registration_status, input.validation_status, input.validation_notes, input.completeness_score, missingJson, input.last_validated_at, now);
|
|
1494
|
+
}
|
|
1495
|
+
const state = this.getProjectRegistrationState(scopeUserId, scopeAgentId, input.project_id);
|
|
1496
|
+
if (!state)
|
|
1497
|
+
throw new Error("failed to persist project_registration_state");
|
|
1498
|
+
return state;
|
|
1499
|
+
}
|
|
228
1500
|
rowToSlot(row) {
|
|
229
1501
|
let parsedValue;
|
|
230
1502
|
try {
|