@mrc2204/agent-smart-memo 5.1.0 → 5.1.3
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 +209 -375
- package/bin/asm.mjs +365 -0
- package/bin/opencode-mcp-server.mjs +320 -0
- package/dist/cli/platform-installers.d.ts +62 -0
- package/dist/cli/platform-installers.d.ts.map +1 -0
- package/dist/cli/platform-installers.js +342 -0
- package/dist/cli/platform-installers.js.map +1 -0
- package/dist/core/contracts/adapter-contracts.d.ts +1 -1
- package/dist/core/contracts/adapter-contracts.d.ts.map +1 -1
- package/dist/core/contracts/change-overlay-contracts.d.ts +69 -0
- package/dist/core/contracts/change-overlay-contracts.d.ts.map +1 -0
- package/dist/core/contracts/change-overlay-contracts.js +2 -0
- package/dist/core/contracts/change-overlay-contracts.js.map +1 -0
- package/dist/core/contracts/feature-pack-contracts.d.ts +37 -0
- package/dist/core/contracts/feature-pack-contracts.d.ts.map +1 -0
- package/dist/core/contracts/feature-pack-contracts.js +8 -0
- package/dist/core/contracts/feature-pack-contracts.js.map +1 -0
- package/dist/core/contracts/project-query-contracts.d.ts +157 -0
- package/dist/core/contracts/project-query-contracts.d.ts.map +1 -0
- package/dist/core/contracts/project-query-contracts.js +2 -0
- package/dist/core/contracts/project-query-contracts.js.map +1 -0
- package/dist/core/graph/code-graph-model.d.ts +9 -0
- package/dist/core/graph/code-graph-model.d.ts.map +1 -0
- package/dist/core/graph/code-graph-model.js +70 -0
- package/dist/core/graph/code-graph-model.js.map +1 -0
- package/dist/core/graph/code-graph-populator.d.ts +20 -0
- package/dist/core/graph/code-graph-populator.d.ts.map +1 -0
- package/dist/core/graph/code-graph-populator.js +760 -0
- package/dist/core/graph/code-graph-populator.js.map +1 -0
- package/dist/core/graph/contracts.d.ts +29 -0
- package/dist/core/graph/contracts.d.ts.map +1 -0
- package/dist/core/graph/contracts.js +47 -0
- package/dist/core/graph/contracts.js.map +1 -0
- package/dist/core/ingest/contracts.d.ts +1 -1
- package/dist/core/ingest/contracts.d.ts.map +1 -1
- package/dist/core/ingest/ingest-pipeline.js +1 -1
- package/dist/core/ingest/ingest-pipeline.js.map +1 -1
- package/dist/core/ingest/semantic-block-extractor.d.ts.map +1 -1
- package/dist/core/ingest/semantic-block-extractor.js +36 -0
- package/dist/core/ingest/semantic-block-extractor.js.map +1 -1
- package/dist/core/usecases/default-memory-usecase-port.d.ts +38 -0
- package/dist/core/usecases/default-memory-usecase-port.d.ts.map +1 -1
- package/dist/core/usecases/default-memory-usecase-port.js +1675 -19
- package/dist/core/usecases/default-memory-usecase-port.js.map +1 -1
- package/dist/db/graph-db.d.ts +24 -0
- package/dist/db/graph-db.d.ts.map +1 -1
- package/dist/db/graph-db.js +81 -2
- package/dist/db/graph-db.js.map +1 -1
- package/dist/db/slot-db.d.ts +227 -1
- package/dist/db/slot-db.d.ts.map +1 -1
- package/dist/db/slot-db.js +700 -13
- package/dist/db/slot-db.js.map +1 -1
- package/dist/index.d.ts +7 -247
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -119
- package/dist/index.js.map +1 -1
- package/dist/shared/asm-config.d.ts +82 -0
- package/dist/shared/asm-config.d.ts.map +1 -0
- package/dist/shared/asm-config.js +254 -0
- package/dist/shared/asm-config.js.map +1 -0
- package/dist/shared/slotdb-path.d.ts +4 -3
- package/dist/shared/slotdb-path.d.ts.map +1 -1
- package/dist/shared/slotdb-path.js +15 -6
- package/dist/shared/slotdb-path.js.map +1 -1
- package/dist/tools/graph-tools.d.ts.map +1 -1
- package/dist/tools/graph-tools.js +131 -0
- package/dist/tools/graph-tools.js.map +1 -1
- package/dist/tools/project-tools.d.ts.map +1 -1
- package/dist/tools/project-tools.js +561 -1
- package/dist/tools/project-tools.js.map +1 -1
- package/openclaw.plugin.json +5 -164
- package/package.json +61 -26
- package/scripts/init-openclaw.mjs +727 -0
|
@@ -2,6 +2,10 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
5
|
+
import { UNIVERSAL_GRAPH_MODEL_VERSION, isUniversalGraphNodeType, isUniversalGraphRelationType, isValidUniversalGraphProvenance, } from "../graph/contracts.js";
|
|
6
|
+
import { upsertUniversalGraphNode, upsertUniversalGraphRelation, } from "../graph/code-graph-model.js";
|
|
7
|
+
import { FEATURE_PACK_KEYS, } from "../contracts/feature-pack-contracts.js";
|
|
8
|
+
import { resolveAsmCoreProjectWorkspaceRoot } from "../../shared/asm-config.js";
|
|
5
9
|
function asRecord(value) {
|
|
6
10
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
7
11
|
? value
|
|
@@ -35,6 +39,9 @@ function allScopeIdentities(ctx) {
|
|
|
35
39
|
function randomJobId() {
|
|
36
40
|
return `idxjob_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
37
41
|
}
|
|
42
|
+
function sha1(raw) {
|
|
43
|
+
return createHash("sha1").update(raw).digest("hex");
|
|
44
|
+
}
|
|
38
45
|
function shellEscape(value) {
|
|
39
46
|
const input = String(value || "");
|
|
40
47
|
return `'${input.replace(/'/g, `'"'"'`)}'`;
|
|
@@ -61,6 +68,10 @@ export class DefaultMemoryUseCasePort {
|
|
|
61
68
|
return this.handleProjectRegister(payload, req);
|
|
62
69
|
case "project.get":
|
|
63
70
|
return this.handleProjectGet(payload, req);
|
|
71
|
+
case "project.binding_preview":
|
|
72
|
+
return this.handleProjectBindingPreview(payload, req);
|
|
73
|
+
case "project.opencode_search":
|
|
74
|
+
return this.handleProjectOpenCodeSearch(payload, req);
|
|
64
75
|
case "project.list":
|
|
65
76
|
return this.handleProjectList(req);
|
|
66
77
|
case "project.set_registration_state":
|
|
@@ -73,6 +84,16 @@ export class DefaultMemoryUseCasePort {
|
|
|
73
84
|
return this.handleProjectLinkTracker(payload, req);
|
|
74
85
|
case "project.trigger_index":
|
|
75
86
|
return this.handleProjectTriggerIndex(payload, req);
|
|
87
|
+
case "project.deindex":
|
|
88
|
+
return this.handleProjectDeindex(payload, req);
|
|
89
|
+
case "project.detach":
|
|
90
|
+
return this.handleProjectDetach(payload, req);
|
|
91
|
+
case "project.unregister":
|
|
92
|
+
return this.handleProjectUnregister(payload, req);
|
|
93
|
+
case "project.purge_preview":
|
|
94
|
+
return this.handleProjectPurgePreview(payload, req);
|
|
95
|
+
case "project.purge":
|
|
96
|
+
return this.handleProjectPurge(payload, req);
|
|
76
97
|
case "project.reindex_diff":
|
|
77
98
|
return this.handleProjectReindexDiff(payload, req);
|
|
78
99
|
case "project.index_event":
|
|
@@ -87,10 +108,22 @@ export class DefaultMemoryUseCasePort {
|
|
|
87
108
|
return this.handleProjectTaskLineageContext(payload, req);
|
|
88
109
|
case "project.hybrid_search":
|
|
89
110
|
return this.handleProjectHybridSearch(payload, req);
|
|
111
|
+
case "project.change_overlay.query":
|
|
112
|
+
return this.handleProjectChangeOverlayQuery(payload, req);
|
|
90
113
|
case "project.legacy_backfill":
|
|
91
114
|
return this.handleProjectLegacyBackfill(payload, req);
|
|
92
115
|
case "project.telegram_onboarding":
|
|
93
116
|
return this.handleProjectTelegramOnboarding(payload, req);
|
|
117
|
+
case "project.feature_pack.generate":
|
|
118
|
+
return this.handleProjectFeaturePackGenerate(payload, req);
|
|
119
|
+
case "project.feature_pack.query":
|
|
120
|
+
return this.handleProjectFeaturePackQuery(payload, req);
|
|
121
|
+
case "project.developer_query":
|
|
122
|
+
return this.handleProjectDeveloperQuery(payload, req);
|
|
123
|
+
case "project.routing_contract":
|
|
124
|
+
return this.handleProjectRoutingContract(payload, req);
|
|
125
|
+
case "project.coding_packet":
|
|
126
|
+
return this.handleProjectCodingPacket(payload, req);
|
|
94
127
|
case "graph.entity.get":
|
|
95
128
|
return this.handleGraphEntityGet(payload, req);
|
|
96
129
|
case "graph.entity.set":
|
|
@@ -101,6 +134,10 @@ export class DefaultMemoryUseCasePort {
|
|
|
101
134
|
return this.handleGraphRelRemove(payload, req);
|
|
102
135
|
case "graph.search":
|
|
103
136
|
return this.handleGraphSearch(payload, req);
|
|
137
|
+
case "graph.code.upsert":
|
|
138
|
+
return this.handleGraphCodeUpsert(payload, req);
|
|
139
|
+
case "graph.code.chain":
|
|
140
|
+
return this.handleGraphCodeChain(payload, req);
|
|
104
141
|
case "memory.capture":
|
|
105
142
|
return this.handleMemoryCapture(payload, req);
|
|
106
143
|
case "memory.search":
|
|
@@ -253,6 +290,236 @@ export class DefaultMemoryUseCasePort {
|
|
|
253
290
|
const identity = normalizePrivateIdentity(req.context);
|
|
254
291
|
return this.slotDb.listProjects(identity.userId, identity.agentId);
|
|
255
292
|
}
|
|
293
|
+
handleProjectBindingPreview(payload, req) {
|
|
294
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
295
|
+
const projects = this.slotDb.listProjects(identity.userId, identity.agentId);
|
|
296
|
+
const normalizedRepoRoot = this.normalizeRepoRootInput(payload.repo_root);
|
|
297
|
+
const aliasSelector = String(payload.project_alias || payload.session_project_alias || "").trim();
|
|
298
|
+
const matches = [];
|
|
299
|
+
const pushMatch = (project, aliases, source) => {
|
|
300
|
+
if (matches.some((item) => item.project_id === project.project_id && item.source === source))
|
|
301
|
+
return;
|
|
302
|
+
matches.push({
|
|
303
|
+
project_id: project.project_id,
|
|
304
|
+
project_alias: aliases.find((item) => item.is_primary === 1)?.project_alias || aliases[0]?.project_alias || null,
|
|
305
|
+
project_name: project.project_name,
|
|
306
|
+
repo_root: project.repo_root,
|
|
307
|
+
lifecycle_status: project.lifecycle_status,
|
|
308
|
+
source,
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
if (payload.project_id) {
|
|
312
|
+
const direct = this.slotDb.getProjectById(identity.userId, identity.agentId, payload.project_id);
|
|
313
|
+
if (direct) {
|
|
314
|
+
const row = projects.find((item) => item.project.project_id === direct.project_id);
|
|
315
|
+
pushMatch(direct, row?.aliases || [], "project_id");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (aliasSelector) {
|
|
319
|
+
const byAlias = this.slotDb.getProjectByAlias(identity.userId, identity.agentId, aliasSelector);
|
|
320
|
+
if (byAlias) {
|
|
321
|
+
const row = projects.find((item) => item.project.project_id === byAlias.project.project_id);
|
|
322
|
+
pushMatch(byAlias.project, row?.aliases || [byAlias.alias], payload.project_alias ? "project_alias" : "session_project_alias");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (normalizedRepoRoot) {
|
|
326
|
+
for (const row of projects) {
|
|
327
|
+
const projectRepoRoot = this.normalizeRepoRootInput(row.project.repo_root || undefined);
|
|
328
|
+
if (projectRepoRoot && projectRepoRoot === normalizedRepoRoot) {
|
|
329
|
+
pushMatch(row.project, row.aliases, "repo_root");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const uniqueByProject = Array.from(new Map(matches.map((item) => [item.project_id, item])).values());
|
|
334
|
+
const activeMatches = uniqueByProject.filter((item) => item.lifecycle_status === "active");
|
|
335
|
+
const crossProjectRequired = activeMatches.length > 1;
|
|
336
|
+
const allowed = !crossProjectRequired || payload.allow_cross_project === true;
|
|
337
|
+
const selected = activeMatches[0] || uniqueByProject[0] || null;
|
|
338
|
+
const resolutionStatus = selected
|
|
339
|
+
? (crossProjectRequired && !allowed ? "ambiguous" : "resolved")
|
|
340
|
+
: "unresolved";
|
|
341
|
+
const resolutionReason = resolutionStatus === "ambiguous"
|
|
342
|
+
? "multiple_active_projects"
|
|
343
|
+
: resolutionStatus === "unresolved"
|
|
344
|
+
? (normalizedRepoRoot ? "unregistered_repo_root" : "selector_not_matched")
|
|
345
|
+
: "matched";
|
|
346
|
+
return {
|
|
347
|
+
mode: "read-only",
|
|
348
|
+
project_scoped_by_default: true,
|
|
349
|
+
cross_project_allowed: payload.allow_cross_project === true,
|
|
350
|
+
resolution_status: resolutionStatus,
|
|
351
|
+
selected_project: selected,
|
|
352
|
+
candidate_projects: uniqueByProject,
|
|
353
|
+
resolution: {
|
|
354
|
+
selectors: {
|
|
355
|
+
project_id: payload.project_id || null,
|
|
356
|
+
project_alias: payload.project_alias || null,
|
|
357
|
+
session_project_alias: payload.session_project_alias || null,
|
|
358
|
+
repo_root: normalizedRepoRoot || null,
|
|
359
|
+
},
|
|
360
|
+
reason: resolutionReason,
|
|
361
|
+
cross_project_required: crossProjectRequired,
|
|
362
|
+
explicit_cross_project_required: crossProjectRequired,
|
|
363
|
+
read_only_tool_surface: ["project_registry_get", "project_registry_list", "project_hybrid_search", "project_developer_query"],
|
|
364
|
+
},
|
|
365
|
+
errors: selected
|
|
366
|
+
? (crossProjectRequired && !allowed ? ["multiple active project matches found; explicit cross-project approval required"] : [])
|
|
367
|
+
: [normalizedRepoRoot ? "no active registered project matched repo_root" : "no registered project matched provided selectors"],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
handleProjectOpenCodeSearch(payload, req) {
|
|
371
|
+
const binding = this.handleProjectBindingPreview({
|
|
372
|
+
project_id: payload.explicit_project_id || payload.project_id,
|
|
373
|
+
project_alias: payload.explicit_project_alias || payload.project_alias,
|
|
374
|
+
repo_root: payload.repo_root,
|
|
375
|
+
session_project_alias: payload.session_project_alias,
|
|
376
|
+
allow_cross_project: payload.explicit_cross_project || payload.allow_cross_project,
|
|
377
|
+
}, req);
|
|
378
|
+
if (binding.resolution_status !== "resolved" || !binding.selected_project?.project_id) {
|
|
379
|
+
return {
|
|
380
|
+
mode: "read-only",
|
|
381
|
+
resolution_status: binding.resolution_status,
|
|
382
|
+
binding,
|
|
383
|
+
query: payload.query,
|
|
384
|
+
results: null,
|
|
385
|
+
errors: binding.errors || ["project binding could not be resolved for read-only search"],
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const selectedProjectId = binding.selected_project.project_id;
|
|
389
|
+
const selectedProjectAlias = binding.selected_project.project_alias;
|
|
390
|
+
const selectedLifecycle = String(binding.selected_project.lifecycle_status || "active");
|
|
391
|
+
if (["deindexed", "detached", "disabled", "purged"].includes(selectedLifecycle)) {
|
|
392
|
+
return {
|
|
393
|
+
mode: "read-only",
|
|
394
|
+
resolution_status: "resolved",
|
|
395
|
+
binding,
|
|
396
|
+
query: payload.query,
|
|
397
|
+
results: {
|
|
398
|
+
project_id: selectedProjectId,
|
|
399
|
+
project_alias: selectedProjectAlias || null,
|
|
400
|
+
project_lifecycle_status: selectedLifecycle,
|
|
401
|
+
searchable: false,
|
|
402
|
+
count: 0,
|
|
403
|
+
results: [],
|
|
404
|
+
reason: selectedLifecycle === "deindexed"
|
|
405
|
+
? "project is deindexed; read-only retrieval is disabled until reindex"
|
|
406
|
+
: selectedLifecycle === "detached"
|
|
407
|
+
? "project is detached; read-only retrieval is disabled until re-attachment"
|
|
408
|
+
: selectedLifecycle === "disabled"
|
|
409
|
+
? "project is unregistered/disabled; read-only retrieval is disabled"
|
|
410
|
+
: "project is purged; read-only retrieval is disabled",
|
|
411
|
+
},
|
|
412
|
+
errors: [],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const results = this.handleProjectDeveloperQuery({
|
|
416
|
+
project_id: selectedProjectId,
|
|
417
|
+
project_alias: selectedProjectAlias || undefined,
|
|
418
|
+
query: payload.query,
|
|
419
|
+
limit: payload.limit,
|
|
420
|
+
}, req);
|
|
421
|
+
return {
|
|
422
|
+
mode: "read-only",
|
|
423
|
+
resolution_status: "resolved",
|
|
424
|
+
binding,
|
|
425
|
+
query: payload.query,
|
|
426
|
+
results,
|
|
427
|
+
errors: [],
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
normalizeWorkstreamType(raw) {
|
|
431
|
+
const normalized = String(raw || "").trim().toLowerCase();
|
|
432
|
+
if (normalized === "research_execution" || normalized === "coding_execution" || normalized === "review_execution") {
|
|
433
|
+
return normalized;
|
|
434
|
+
}
|
|
435
|
+
return "coding_execution";
|
|
436
|
+
}
|
|
437
|
+
handleProjectRoutingContract(payload, req) {
|
|
438
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
439
|
+
const project = this.resolveProjectRef(identity.userId, identity.agentId, {
|
|
440
|
+
project_id: payload.project_id,
|
|
441
|
+
project_alias: payload.project_alias,
|
|
442
|
+
});
|
|
443
|
+
const workstreamType = this.normalizeWorkstreamType(payload.workstream_type);
|
|
444
|
+
const reasonByWorkstream = {
|
|
445
|
+
coding_execution: "OpenClaw orchestrates and routes coding execution to OpenCode lane foundation.",
|
|
446
|
+
research_execution: "Research execution keeps OpenCode lane ready through project/code-aware retrieval foundation.",
|
|
447
|
+
review_execution: "Review execution can reuse OpenCode coding lane foundation with project-aware packet context.",
|
|
448
|
+
};
|
|
449
|
+
return {
|
|
450
|
+
routing_contract_version: "asm-routing-v1",
|
|
451
|
+
workstream_type: workstreamType,
|
|
452
|
+
project_id: project.project_id,
|
|
453
|
+
project_alias: payload.project_alias || null,
|
|
454
|
+
route_target: {
|
|
455
|
+
lane: "opencode",
|
|
456
|
+
mode: "foundation",
|
|
457
|
+
reason: reasonByWorkstream[workstreamType],
|
|
458
|
+
},
|
|
459
|
+
retrieval_profile: {
|
|
460
|
+
project_aware: true,
|
|
461
|
+
code_aware: true,
|
|
462
|
+
primary_usecase: "project.developer_query",
|
|
463
|
+
},
|
|
464
|
+
generated_at: new Date().toISOString(),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
handleProjectCodingPacket(payload, req) {
|
|
468
|
+
const routing = this.handleProjectRoutingContract({
|
|
469
|
+
project_id: payload.project_id,
|
|
470
|
+
project_alias: payload.project_alias,
|
|
471
|
+
query: payload.query,
|
|
472
|
+
objective: payload.objective,
|
|
473
|
+
workstream_type: "coding_execution",
|
|
474
|
+
}, req);
|
|
475
|
+
const query = String(payload.query || payload.objective || payload.task_title || "").trim();
|
|
476
|
+
const objective = String(payload.objective || payload.query || "").trim() || "Implement requested coding change with project-aware/code-aware context.";
|
|
477
|
+
const developerQuery = this.handleProjectDeveloperQuery({
|
|
478
|
+
project_id: routing.project_id,
|
|
479
|
+
project_alias: routing.project_alias || undefined,
|
|
480
|
+
query,
|
|
481
|
+
task_id: payload.task_id,
|
|
482
|
+
tracker_issue_key: payload.tracker_issue_key,
|
|
483
|
+
task_title: payload.task_title,
|
|
484
|
+
symbol_name: payload.symbol_name,
|
|
485
|
+
relative_path: payload.relative_path,
|
|
486
|
+
route_path: payload.route_path,
|
|
487
|
+
limit: payload.limit,
|
|
488
|
+
}, req);
|
|
489
|
+
const primaryFiles = developerQuery.files.slice(0, 12);
|
|
490
|
+
const primarySymbols = developerQuery.symbols.slice(0, 16);
|
|
491
|
+
return {
|
|
492
|
+
packet_version: "asm-coding-packet-v1",
|
|
493
|
+
routing,
|
|
494
|
+
objective,
|
|
495
|
+
project: {
|
|
496
|
+
project_id: routing.project_id,
|
|
497
|
+
project_alias: routing.project_alias,
|
|
498
|
+
},
|
|
499
|
+
selectors: {
|
|
500
|
+
...(payload.task_id ? { task_id: payload.task_id } : {}),
|
|
501
|
+
...(payload.tracker_issue_key ? { tracker_issue_key: payload.tracker_issue_key } : {}),
|
|
502
|
+
...(payload.task_title ? { task_title: payload.task_title } : {}),
|
|
503
|
+
...(payload.symbol_name ? { symbol_name: payload.symbol_name } : {}),
|
|
504
|
+
...(payload.relative_path ? { relative_path: payload.relative_path } : {}),
|
|
505
|
+
...(payload.route_path ? { route_path: payload.route_path } : {}),
|
|
506
|
+
},
|
|
507
|
+
context: {
|
|
508
|
+
developer_query: developerQuery,
|
|
509
|
+
primary_files: primaryFiles,
|
|
510
|
+
primary_symbols: primarySymbols,
|
|
511
|
+
change_context: developerQuery.change_context,
|
|
512
|
+
},
|
|
513
|
+
execution_hints: {
|
|
514
|
+
acceptance_criteria: payload.acceptance_criteria || [],
|
|
515
|
+
constraints: payload.constraints || [],
|
|
516
|
+
out_of_scope: payload.out_of_scope || [],
|
|
517
|
+
validation_commands: payload.validation_commands || [],
|
|
518
|
+
handoff_language: "vi",
|
|
519
|
+
},
|
|
520
|
+
generated_at: new Date().toISOString(),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
256
523
|
handleProjectSetRegistrationState(payload, req) {
|
|
257
524
|
const identity = normalizePrivateIdentity(req.context);
|
|
258
525
|
if (!payload.project_id) {
|
|
@@ -423,7 +690,7 @@ export class DefaultMemoryUseCasePort {
|
|
|
423
690
|
const project = this.resolveProjectRef(identity.userId, identity.agentId, payload.project_ref);
|
|
424
691
|
const queuedAt = new Date().toISOString();
|
|
425
692
|
const jobId = randomJobId();
|
|
426
|
-
const normalizedPaths = (payload.paths || []).filter((item) => String(item.relative_path || "").trim().length > 0);
|
|
693
|
+
const normalizedPaths = this.hydrateIndexPathsFromRepo(project.repo_root, (payload.paths || []).filter((item) => String(item.relative_path || "").trim().length > 0));
|
|
427
694
|
this.scheduleProjectReindexJob({
|
|
428
695
|
scopeUserId: identity.userId,
|
|
429
696
|
scopeAgentId: identity.agentId,
|
|
@@ -452,13 +719,16 @@ export class DefaultMemoryUseCasePort {
|
|
|
452
719
|
scheduleProjectReindexJob(input) {
|
|
453
720
|
setTimeout(() => {
|
|
454
721
|
try {
|
|
722
|
+
const project = this.slotDb.getProjectById(input.scopeUserId, input.scopeAgentId, input.projectId);
|
|
455
723
|
let paths = input.paths;
|
|
456
724
|
if (paths.length === 0) {
|
|
457
|
-
const project = this.slotDb.getProjectById(input.scopeUserId, input.scopeAgentId, input.projectId);
|
|
458
725
|
if (project?.repo_root) {
|
|
459
726
|
paths = this.collectGitTrackedPaths(project.repo_root);
|
|
460
727
|
}
|
|
461
728
|
}
|
|
729
|
+
else {
|
|
730
|
+
paths = this.hydrateIndexPathsFromRepo(project?.repo_root, paths);
|
|
731
|
+
}
|
|
462
732
|
if (paths.length === 0)
|
|
463
733
|
return;
|
|
464
734
|
this.slotDb.reindexProjectByDiff(input.scopeUserId, input.scopeAgentId, {
|
|
@@ -494,25 +764,86 @@ export class DefaultMemoryUseCasePort {
|
|
|
494
764
|
if (!repoRoot)
|
|
495
765
|
return { installed: false, hooks: [], note: 'repo_root_missing' };
|
|
496
766
|
const gitDir = resolve(repoRoot, '.git');
|
|
497
|
-
const hooksDir = resolve(gitDir, 'hooks');
|
|
498
767
|
if (!existsSync(gitDir))
|
|
499
768
|
return { installed: false, hooks: [], note: 'git_dir_missing' };
|
|
769
|
+
let hooksDir = resolve(gitDir, 'hooks');
|
|
770
|
+
try {
|
|
771
|
+
const hooksPath = execSync('git config --get core.hooksPath', { cwd: repoRoot, stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
|
|
772
|
+
if (hooksPath)
|
|
773
|
+
hooksDir = resolve(repoRoot, hooksPath);
|
|
774
|
+
}
|
|
775
|
+
catch { }
|
|
500
776
|
mkdirSync(hooksDir, { recursive: true });
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
777
|
+
const listenerPath = resolve(hooksDir, 'asm-project-event.sh');
|
|
778
|
+
const marker = '# ASM_AUTO_INDEX_HOOK';
|
|
779
|
+
const listener = `#!/bin/sh
|
|
504
780
|
PROJECT_ID="${projectId}"
|
|
505
781
|
REPO_ROOT="${repoRoot}"
|
|
782
|
+
EVENT_TYPE="$1"
|
|
506
783
|
SOURCE_REV="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
784
|
+
DEFAULT_BRANCH="$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')"
|
|
785
|
+
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
|
786
|
+
WORKTREE_DIRTY="$(if git diff --quiet --ignore-submodules HEAD -- 2>/dev/null && git diff --cached --quiet --ignore-submodules -- 2>/dev/null; then echo 0; else echo 1; fi)"
|
|
787
|
+
TRUSTED_SYNC="0"
|
|
788
|
+
FULL_SNAPSHOT="0"
|
|
789
|
+
CHANGED_FILES=""
|
|
790
|
+
DELETED_FILES=""
|
|
791
|
+
|
|
792
|
+
if [ "$EVENT_TYPE" = "post_commit" ]; then
|
|
793
|
+
CHANGED_FILES="$(git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | paste -sd, -)"
|
|
794
|
+
DELETED_FILES="$(git diff-tree --no-commit-id --name-only --diff-filter=D -r HEAD 2>/dev/null | paste -sd, -)"
|
|
795
|
+
fi
|
|
796
|
+
|
|
797
|
+
if [ -n "$DEFAULT_BRANCH" ] && [ "$CURRENT_BRANCH" = "$DEFAULT_BRANCH" ] && [ "$WORKTREE_DIRTY" = "0" ]; then
|
|
798
|
+
if git rev-parse --verify "origin/$DEFAULT_BRANCH" >/dev/null 2>&1; then
|
|
799
|
+
LOCAL_HEAD="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
800
|
+
REMOTE_HEAD="$(git rev-parse "origin/$DEFAULT_BRANCH" 2>/dev/null || echo unknown)"
|
|
801
|
+
if [ "$LOCAL_HEAD" = "$REMOTE_HEAD" ]; then
|
|
802
|
+
TRUSTED_SYNC="1"
|
|
803
|
+
FULL_SNAPSHOT="1"
|
|
804
|
+
fi
|
|
805
|
+
fi
|
|
806
|
+
fi
|
|
807
|
+
|
|
808
|
+
if [ "$TRUSTED_SYNC" = "1" ]; then
|
|
809
|
+
CHANGED_FILES="$(git ls-files 2>/dev/null | paste -sd, -)"
|
|
810
|
+
DELETED_FILES=""
|
|
811
|
+
fi
|
|
812
|
+
|
|
813
|
+
asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-type "$EVENT_TYPE" --source-rev "$SOURCE_REV" --changed-files "$CHANGED_FILES" --deleted-files "$DELETED_FILES" --trusted-sync "$TRUSTED_SYNC" --full-snapshot "$FULL_SNAPSHOT" >/dev/null 2>&1 || true
|
|
510
814
|
`;
|
|
511
|
-
|
|
815
|
+
writeFileSync(listenerPath, listener, 'utf8');
|
|
816
|
+
chmodSync(listenerPath, 0o755);
|
|
817
|
+
const attachHook = (name, eventType) => {
|
|
818
|
+
const hookPath = resolve(hooksDir, name);
|
|
819
|
+
const callLine = `${marker}\n\"${listenerPath}\" ${eventType} || true`;
|
|
820
|
+
let content = existsSync(hookPath) ? readFileSync(hookPath, 'utf8') : '';
|
|
821
|
+
if (content.includes(marker))
|
|
822
|
+
return hookPath;
|
|
823
|
+
if (!content.trim()) {
|
|
824
|
+
content = `#!/bin/sh\n\n${callLine}\n`;
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
const backupPath = `${hookPath}.asm-backup`;
|
|
828
|
+
if (!existsSync(backupPath))
|
|
829
|
+
writeFileSync(backupPath, content, 'utf8');
|
|
830
|
+
if (!content.startsWith('#!')) {
|
|
831
|
+
content = `#!/bin/sh\n${content}`;
|
|
832
|
+
}
|
|
833
|
+
if (!content.endsWith('\n'))
|
|
834
|
+
content += '\n';
|
|
835
|
+
content += `\n${callLine}\n`;
|
|
836
|
+
}
|
|
837
|
+
writeFileSync(hookPath, content, 'utf8');
|
|
512
838
|
chmodSync(hookPath, 0o755);
|
|
513
839
|
return hookPath;
|
|
514
840
|
};
|
|
515
|
-
const hooks = [
|
|
841
|
+
const hooks = [
|
|
842
|
+
attachHook('post-commit', 'post_commit'),
|
|
843
|
+
attachHook('post-merge', 'post_merge'),
|
|
844
|
+
attachHook('post-rewrite', 'post_rewrite'),
|
|
845
|
+
listenerPath,
|
|
846
|
+
];
|
|
516
847
|
return { installed: true, hooks };
|
|
517
848
|
}
|
|
518
849
|
validateJiraTrackerFields(trackerSpaceKey, defaultEpicKey) {
|
|
@@ -551,9 +882,7 @@ asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-ty
|
|
|
551
882
|
process.env.PROJECT_WORKSPACE_ROOT,
|
|
552
883
|
process.env.REPO_CLONE_ROOT,
|
|
553
884
|
req?.meta?.projectWorkspaceRoot,
|
|
554
|
-
req?.meta?.repoCloneRoot,
|
|
555
885
|
req?.context?.metadata?.projectWorkspaceRoot,
|
|
556
|
-
req?.context?.metadata?.repoCloneRoot,
|
|
557
886
|
req?.context?.metadata?.workspaceRoot,
|
|
558
887
|
];
|
|
559
888
|
for (const raw of candidates) {
|
|
@@ -570,6 +899,20 @@ asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-ty
|
|
|
570
899
|
if (existsSync(resolved))
|
|
571
900
|
return resolved;
|
|
572
901
|
}
|
|
902
|
+
const sharedWorkspaceRoot = resolveAsmCoreProjectWorkspaceRoot({ env: process.env, homeDir: process.env.HOME });
|
|
903
|
+
if (sharedWorkspaceRoot) {
|
|
904
|
+
const resolved = isAbsolute(sharedWorkspaceRoot)
|
|
905
|
+
? resolve(sharedWorkspaceRoot)
|
|
906
|
+
: resolve(process.cwd(), sharedWorkspaceRoot);
|
|
907
|
+
try {
|
|
908
|
+
mkdirSync(resolved, { recursive: true });
|
|
909
|
+
}
|
|
910
|
+
catch {
|
|
911
|
+
// ignore and fallback
|
|
912
|
+
}
|
|
913
|
+
if (existsSync(resolved))
|
|
914
|
+
return resolved;
|
|
915
|
+
}
|
|
573
916
|
const fallback = resolve(process.env.HOME || process.cwd(), ".openclaw", "workspace", "projects");
|
|
574
917
|
mkdirSync(fallback, { recursive: true });
|
|
575
918
|
return fallback;
|
|
@@ -846,7 +1189,7 @@ asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-ty
|
|
|
846
1189
|
}
|
|
847
1190
|
return {
|
|
848
1191
|
relative_path: relativePath,
|
|
849
|
-
checksum: `git:${relativePath}`,
|
|
1192
|
+
checksum: content != null ? sha1(content) : `git:${relativePath}`,
|
|
850
1193
|
module: relativePath.split("/")[0] || undefined,
|
|
851
1194
|
language: ext || undefined,
|
|
852
1195
|
content,
|
|
@@ -887,7 +1230,7 @@ asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-ty
|
|
|
887
1230
|
}
|
|
888
1231
|
return {
|
|
889
1232
|
relative_path: relativePath,
|
|
890
|
-
checksum: `fs:${relativePath}`,
|
|
1233
|
+
checksum: content != null ? sha1(content) : `fs:${relativePath}`,
|
|
891
1234
|
module: relativePath.split("/")[0] || undefined,
|
|
892
1235
|
language: relativePath.includes(".") ? relativePath.split(".").pop() || undefined : undefined,
|
|
893
1236
|
content,
|
|
@@ -895,6 +1238,77 @@ asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-ty
|
|
|
895
1238
|
});
|
|
896
1239
|
}
|
|
897
1240
|
}
|
|
1241
|
+
hydrateIndexPathsFromRepo(repoRoot, paths) {
|
|
1242
|
+
if (!repoRoot)
|
|
1243
|
+
return paths;
|
|
1244
|
+
return paths.map((item) => {
|
|
1245
|
+
const relativePath = String(item.relative_path || '').trim();
|
|
1246
|
+
if (!relativePath)
|
|
1247
|
+
return item;
|
|
1248
|
+
const abs = resolve(repoRoot, relativePath);
|
|
1249
|
+
let content = item.content;
|
|
1250
|
+
if ((content == null || content === '') && existsSync(abs)) {
|
|
1251
|
+
try {
|
|
1252
|
+
content = readFileSync(abs, 'utf8');
|
|
1253
|
+
}
|
|
1254
|
+
catch {
|
|
1255
|
+
content = item.content;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
const checksum = content != null && String(content).trim() !== ''
|
|
1259
|
+
? sha1(String(content))
|
|
1260
|
+
: (item.checksum || null);
|
|
1261
|
+
return {
|
|
1262
|
+
...item,
|
|
1263
|
+
checksum,
|
|
1264
|
+
content,
|
|
1265
|
+
};
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
handleProjectDeindex(payload, req) {
|
|
1269
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
1270
|
+
if (!payload.project_id) {
|
|
1271
|
+
throw new Error("project.deindex requires payload.project_id");
|
|
1272
|
+
}
|
|
1273
|
+
return this.slotDb.deindexProject(identity.userId, identity.agentId, {
|
|
1274
|
+
project_id: payload.project_id,
|
|
1275
|
+
reason: payload.reason,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
handleProjectDetach(payload, req) {
|
|
1279
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
1280
|
+
const project = this.resolveProjectRef(identity.userId, identity.agentId, payload.project_ref || {});
|
|
1281
|
+
return this.slotDb.detachProject(identity.userId, identity.agentId, {
|
|
1282
|
+
project_id: project.project_id,
|
|
1283
|
+
reason: payload.reason,
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
handleProjectUnregister(payload, req) {
|
|
1287
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
1288
|
+
const project = this.resolveProjectRef(identity.userId, identity.agentId, payload.project_ref || {});
|
|
1289
|
+
return this.slotDb.unregisterProject(identity.userId, identity.agentId, {
|
|
1290
|
+
project_id: project.project_id,
|
|
1291
|
+
confirm: payload.confirm,
|
|
1292
|
+
mode: payload.mode,
|
|
1293
|
+
reason: payload.reason,
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
handleProjectPurgePreview(payload, req) {
|
|
1297
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
1298
|
+
const project = this.resolveProjectRef(identity.userId, identity.agentId, payload.project_ref || {});
|
|
1299
|
+
return this.slotDb.purgePreviewProject(identity.userId, identity.agentId, {
|
|
1300
|
+
project_id: project.project_id,
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
handleProjectPurge(payload, req) {
|
|
1304
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
1305
|
+
const project = this.resolveProjectRef(identity.userId, identity.agentId, payload.project_ref || {});
|
|
1306
|
+
return this.slotDb.purgeProject(identity.userId, identity.agentId, {
|
|
1307
|
+
project_id: project.project_id,
|
|
1308
|
+
confirm: payload.confirm,
|
|
1309
|
+
reason: payload.reason,
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
898
1312
|
handleProjectReindexDiff(payload, req) {
|
|
899
1313
|
const identity = normalizePrivateIdentity(req.context);
|
|
900
1314
|
if (!payload.project_id) {
|
|
@@ -918,8 +1332,15 @@ asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-ty
|
|
|
918
1332
|
if (!project || !project.repo_root) {
|
|
919
1333
|
throw new Error("project.index_event requires a registered project with repo_root");
|
|
920
1334
|
}
|
|
1335
|
+
const registeredRepoRoot = this.normalizeRepoRootInput(project.repo_root || undefined);
|
|
1336
|
+
const eventRepoRoot = this.normalizeRepoRootInput(payload.repo_root || undefined);
|
|
1337
|
+
if (eventRepoRoot && registeredRepoRoot && eventRepoRoot !== registeredRepoRoot) {
|
|
1338
|
+
throw new Error(`project.index_event repo_root mismatch: event='${eventRepoRoot}' registered='${registeredRepoRoot}'`);
|
|
1339
|
+
}
|
|
921
1340
|
const changedFiles = Array.from(new Set((payload.changed_files || []).map((x) => String(x || "").trim()).filter(Boolean)));
|
|
922
1341
|
const deletedFiles = Array.from(new Set((payload.deleted_files || []).map((x) => String(x || "").trim()).filter(Boolean)));
|
|
1342
|
+
const trustedSync = payload.trusted_sync === true;
|
|
1343
|
+
const fullSnapshot = payload.full_snapshot === true || trustedSync;
|
|
923
1344
|
const paths = changedFiles.map((relativePath) => {
|
|
924
1345
|
const abs = resolve(project.repo_root, relativePath);
|
|
925
1346
|
let content = null;
|
|
@@ -931,7 +1352,7 @@ asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-ty
|
|
|
931
1352
|
}
|
|
932
1353
|
return {
|
|
933
1354
|
relative_path: relativePath,
|
|
934
|
-
checksum: `event:${relativePath}:${payload.source_rev || "unknown"}`,
|
|
1355
|
+
checksum: content != null && String(content).trim() !== '' ? sha1(String(content)) : `event:${relativePath}:${payload.source_rev || "unknown"}`,
|
|
935
1356
|
module: relativePath.split("/")[0] || undefined,
|
|
936
1357
|
language: relativePath.includes(".") ? relativePath.split(".").pop() || undefined : undefined,
|
|
937
1358
|
content,
|
|
@@ -940,9 +1361,9 @@ asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-ty
|
|
|
940
1361
|
const reindex = this.slotDb.reindexProjectByDiff(identity.userId, identity.agentId, {
|
|
941
1362
|
project_id: payload.project_id,
|
|
942
1363
|
source_rev: payload.source_rev || null,
|
|
943
|
-
trigger_type: "incremental",
|
|
944
|
-
index_profile: "default",
|
|
945
|
-
full_snapshot:
|
|
1364
|
+
trigger_type: trustedSync ? "repair" : "incremental",
|
|
1365
|
+
index_profile: trustedSync ? "authoritative" : "default",
|
|
1366
|
+
full_snapshot: fullSnapshot,
|
|
946
1367
|
paths,
|
|
947
1368
|
});
|
|
948
1369
|
if (deletedFiles.length > 0) {
|
|
@@ -955,6 +1376,8 @@ asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-ty
|
|
|
955
1376
|
project_id: payload.project_id,
|
|
956
1377
|
event_type: payload.event_type || "manual",
|
|
957
1378
|
source_rev: payload.source_rev || null,
|
|
1379
|
+
trusted_sync: trustedSync,
|
|
1380
|
+
full_snapshot: fullSnapshot,
|
|
958
1381
|
changed_files: changedFiles,
|
|
959
1382
|
deleted_files: deletedFiles,
|
|
960
1383
|
reindex,
|
|
@@ -1033,6 +1456,160 @@ asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-ty
|
|
|
1033
1456
|
task_context: payload.task_context,
|
|
1034
1457
|
});
|
|
1035
1458
|
}
|
|
1459
|
+
handleProjectChangeOverlayQuery(payload, req) {
|
|
1460
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
1461
|
+
if (!payload.project_id) {
|
|
1462
|
+
throw new Error("project.change_overlay.query requires payload.project_id");
|
|
1463
|
+
}
|
|
1464
|
+
if (!payload.task_id && !payload.tracker_issue_key && !payload.task_title) {
|
|
1465
|
+
throw new Error("project.change_overlay.query requires one selector: task_id|tracker_issue_key|task_title");
|
|
1466
|
+
}
|
|
1467
|
+
const requestedFeature = payload.feature_key
|
|
1468
|
+
? payload.feature_key
|
|
1469
|
+
: (payload.feature_name ? this.resolveFeatureKeyInput(undefined, payload.feature_name) : undefined);
|
|
1470
|
+
const result = this.slotDb.queryProjectChangeOverlay(identity.userId, identity.agentId, {
|
|
1471
|
+
project_id: payload.project_id,
|
|
1472
|
+
task_id: payload.task_id,
|
|
1473
|
+
tracker_issue_key: payload.tracker_issue_key,
|
|
1474
|
+
task_title: payload.task_title,
|
|
1475
|
+
feature_key: requestedFeature,
|
|
1476
|
+
feature_name: payload.feature_name,
|
|
1477
|
+
include_related: payload.include_related,
|
|
1478
|
+
include_parent_chain: payload.include_parent_chain,
|
|
1479
|
+
});
|
|
1480
|
+
const baseEvidence = [
|
|
1481
|
+
{ type: "task", ref: result.focus.task_id, note: result.focus.task_title },
|
|
1482
|
+
...(result.focus.tracker_issue_key
|
|
1483
|
+
? [{ type: "tracker_issue", ref: result.focus.tracker_issue_key }]
|
|
1484
|
+
: []),
|
|
1485
|
+
...result.changed_files.map((file) => ({ type: "file", ref: file })),
|
|
1486
|
+
...result.related_symbols.slice(0, 20).map((symbol) => ({
|
|
1487
|
+
type: "symbol",
|
|
1488
|
+
ref: symbol.symbol_fqn || symbol.symbol_name,
|
|
1489
|
+
note: symbol.relative_path,
|
|
1490
|
+
})),
|
|
1491
|
+
...result.commit_refs.map((ref) => ({ type: "commit_ref", ref })),
|
|
1492
|
+
];
|
|
1493
|
+
const featurePackCandidates = requestedFeature
|
|
1494
|
+
? [requestedFeature]
|
|
1495
|
+
: [
|
|
1496
|
+
"project_onboarding_registration_indexing",
|
|
1497
|
+
"code_aware_retrieval",
|
|
1498
|
+
"heartbeat_health_runtime_integrity",
|
|
1499
|
+
"change_aware_impact",
|
|
1500
|
+
"post_entry_review_decision_support",
|
|
1501
|
+
];
|
|
1502
|
+
const featurePackMatches = [];
|
|
1503
|
+
for (const featureKey of featurePackCandidates) {
|
|
1504
|
+
let pack = null;
|
|
1505
|
+
try {
|
|
1506
|
+
pack = this.handleProjectFeaturePackGenerate({
|
|
1507
|
+
project_id: result.project_id,
|
|
1508
|
+
feature_key: featureKey,
|
|
1509
|
+
}, req);
|
|
1510
|
+
}
|
|
1511
|
+
catch {
|
|
1512
|
+
// Optional mapping: skip packs without enough evidence/context for this project.
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
const packEvidenceSet = new Set((pack.evidence || []).map((item) => `${item.type}:${String(item.ref || "").toLowerCase()}`));
|
|
1516
|
+
const matchedEvidence = baseEvidence.filter((item) => packEvidenceSet.has(`${item.type}:${String(item.ref || "").toLowerCase()}`));
|
|
1517
|
+
if (matchedEvidence.length === 0) {
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
const trackerHit = Boolean(result.focus.tracker_issue_key) &&
|
|
1521
|
+
pack.evidence.some((item) => item.type === "task" && String(item.ref || "") === String(result.focus.tracker_issue_key || ""));
|
|
1522
|
+
const taskHit = pack.evidence.some((item) => item.type === "task" && String(item.ref || "") === String(result.focus.task_id));
|
|
1523
|
+
const commitHitCount = result.commit_refs.filter((ref) => pack.related_commits.some((hint) => hint.toLowerCase().includes(String(ref || "").toLowerCase()))).length;
|
|
1524
|
+
const overlapRatio = matchedEvidence.length / Math.max(baseEvidence.length, 1);
|
|
1525
|
+
const confidenceRaw = Math.min(1, overlapRatio * 0.75) +
|
|
1526
|
+
(trackerHit ? 0.12 : 0) +
|
|
1527
|
+
(taskHit ? 0.08 : 0) +
|
|
1528
|
+
Math.min(0.1, commitHitCount * 0.05);
|
|
1529
|
+
const confidence = Number(Math.min(1, confidenceRaw).toFixed(2));
|
|
1530
|
+
if (confidence < 0.25) {
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
featurePackMatches.push({
|
|
1534
|
+
feature_key: featureKey,
|
|
1535
|
+
title: pack.title,
|
|
1536
|
+
confidence,
|
|
1537
|
+
matched_evidence: matchedEvidence.slice(0, 10),
|
|
1538
|
+
note: `matched ${matchedEvidence.length} evidence items`,
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
featurePackMatches.sort((a, b) => b.confidence - a.confidence);
|
|
1542
|
+
const symbolsByPath = new Map();
|
|
1543
|
+
for (const path of result.changed_files) {
|
|
1544
|
+
symbolsByPath.set(path, 0);
|
|
1545
|
+
}
|
|
1546
|
+
for (const symbol of result.related_symbols) {
|
|
1547
|
+
if (symbol.relative_path && symbolsByPath.has(symbol.relative_path)) {
|
|
1548
|
+
symbolsByPath.set(symbol.relative_path, Number(symbolsByPath.get(symbol.relative_path) || 0) + 1);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
const enrichedSymbols = result.related_symbols
|
|
1552
|
+
.map((symbol) => {
|
|
1553
|
+
const hasPath = Boolean(symbol.relative_path && result.changed_files.includes(symbol.relative_path));
|
|
1554
|
+
const pathDensity = hasPath
|
|
1555
|
+
? Math.min(1, Number(symbolsByPath.get(symbol.relative_path || "") || 0) / 4)
|
|
1556
|
+
: 0;
|
|
1557
|
+
const name = String(symbol.symbol_name || "");
|
|
1558
|
+
const nameLower = name.toLowerCase();
|
|
1559
|
+
const symbolFromTask = result.focus.task_title
|
|
1560
|
+
.toLowerCase()
|
|
1561
|
+
.split(/[^a-z0-9_]+/)
|
|
1562
|
+
.filter((token) => token.length >= 3)
|
|
1563
|
+
.some((token) => nameLower.includes(token));
|
|
1564
|
+
const confidence = Number(Math.min(1, (symbol.source === "task_registry" ? 0.45 : 0.35) +
|
|
1565
|
+
(hasPath ? 0.25 : 0) +
|
|
1566
|
+
pathDensity * 0.2 +
|
|
1567
|
+
(symbolFromTask ? 0.1 : 0)).toFixed(2));
|
|
1568
|
+
return {
|
|
1569
|
+
...symbol,
|
|
1570
|
+
confidence,
|
|
1571
|
+
evidence_refs: [
|
|
1572
|
+
...(symbol.relative_path ? [`file:${symbol.relative_path}`] : []),
|
|
1573
|
+
...(symbol.symbol_fqn ? [`symbol:${symbol.symbol_fqn}`] : [`symbol:${symbol.symbol_name}`]),
|
|
1574
|
+
],
|
|
1575
|
+
};
|
|
1576
|
+
})
|
|
1577
|
+
.sort((a, b) => {
|
|
1578
|
+
const confidenceDiff = Number((b.confidence || 0) - (a.confidence || 0));
|
|
1579
|
+
if (confidenceDiff !== 0)
|
|
1580
|
+
return confidenceDiff;
|
|
1581
|
+
return String(a.symbol_name).localeCompare(String(b.symbol_name));
|
|
1582
|
+
});
|
|
1583
|
+
const confidence = {
|
|
1584
|
+
overall: Number(Math.min(1, (result.changed_files.length > 0 ? 0.3 : 0) +
|
|
1585
|
+
Math.min(0.25, result.related_symbols.length * 0.03) +
|
|
1586
|
+
Math.min(0.15, result.commit_refs.length * 0.05) +
|
|
1587
|
+
Math.min(0.3, featurePackMatches.length * 0.1)).toFixed(2)),
|
|
1588
|
+
signals: {
|
|
1589
|
+
changed_files: result.changed_files.length,
|
|
1590
|
+
related_symbols: result.related_symbols.length,
|
|
1591
|
+
commit_refs: result.commit_refs.length,
|
|
1592
|
+
feature_pack_matches: featurePackMatches.length,
|
|
1593
|
+
},
|
|
1594
|
+
};
|
|
1595
|
+
return {
|
|
1596
|
+
overlay_id: `change-overlay:${result.project_id}:${result.focus.task_id}${requestedFeature ? `:${requestedFeature}` : ""}`,
|
|
1597
|
+
status: result.status,
|
|
1598
|
+
...(result.reason ? { reason: result.reason } : {}),
|
|
1599
|
+
selector: result.selector,
|
|
1600
|
+
recoverable: result.recoverable,
|
|
1601
|
+
project_id: result.project_id,
|
|
1602
|
+
focus: result.focus,
|
|
1603
|
+
changed_files: result.changed_files,
|
|
1604
|
+
related_symbols: enrichedSymbols,
|
|
1605
|
+
commit_refs: result.commit_refs,
|
|
1606
|
+
feature_packs: featurePackMatches,
|
|
1607
|
+
evidence: baseEvidence,
|
|
1608
|
+
confidence,
|
|
1609
|
+
generated_at: new Date().toISOString(),
|
|
1610
|
+
generator_version: "asm-94-slice3",
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1036
1613
|
handleProjectLegacyBackfill(payload, req) {
|
|
1037
1614
|
const identity = normalizePrivateIdentity(req.context);
|
|
1038
1615
|
return this.slotDb.runLegacyCompatibilityBackfill(identity.userId, identity.agentId, {
|
|
@@ -1154,6 +1731,1032 @@ asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-ty
|
|
|
1154
1731
|
used_commands: ["project.register_command", "project.link_tracker", "project.trigger_index"],
|
|
1155
1732
|
};
|
|
1156
1733
|
}
|
|
1734
|
+
handleProjectFeaturePackGenerate(payload, req) {
|
|
1735
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
1736
|
+
const featureKey = payload.feature_key || "project_onboarding_registration_indexing";
|
|
1737
|
+
const project = this.resolveProjectRef(identity.userId, identity.agentId, {
|
|
1738
|
+
project_id: payload.project_id,
|
|
1739
|
+
project_alias: payload.project_alias,
|
|
1740
|
+
});
|
|
1741
|
+
const snapshot = this.slotDb.getProjectFeaturePackProjectOnboardingIndexingSnapshot(identity.userId, identity.agentId, project.project_id);
|
|
1742
|
+
const primaryAlias = snapshot.aliases.find((item) => item.is_primary === 1)?.project_alias || payload.project_alias || project.project_name;
|
|
1743
|
+
switch (featureKey) {
|
|
1744
|
+
case "project_onboarding_registration_indexing":
|
|
1745
|
+
return this.buildProjectOnboardingRegistrationIndexingPack(snapshot, project.project_id, primaryAlias);
|
|
1746
|
+
case "code_aware_retrieval":
|
|
1747
|
+
return this.buildCodeAwareRetrievalPack(snapshot, project.project_id, primaryAlias);
|
|
1748
|
+
case "heartbeat_health_runtime_integrity":
|
|
1749
|
+
return this.buildHeartbeatHealthRuntimeIntegrityPack(snapshot, project.project_id, primaryAlias);
|
|
1750
|
+
case "change_aware_impact":
|
|
1751
|
+
return this.buildChangeAwareImpactPack(snapshot, project.project_id, primaryAlias);
|
|
1752
|
+
case "post_entry_review_decision_support":
|
|
1753
|
+
return this.buildPostEntryReviewDecisionSupportPack(snapshot, project.project_id, primaryAlias);
|
|
1754
|
+
default:
|
|
1755
|
+
throw new Error(`Unsupported feature_key: ${featureKey}`);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
handleProjectFeaturePackQuery(payload, req) {
|
|
1759
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
1760
|
+
const project = this.resolveProjectRef(identity.userId, identity.agentId, {
|
|
1761
|
+
project_id: payload.project_id,
|
|
1762
|
+
project_alias: payload.project_alias,
|
|
1763
|
+
});
|
|
1764
|
+
const requestedFeature = this.resolveFeatureKeyInput(payload.feature_key, payload.feature_name);
|
|
1765
|
+
const pack = this.handleProjectFeaturePackGenerate({
|
|
1766
|
+
project_id: project.project_id,
|
|
1767
|
+
project_alias: payload.project_alias,
|
|
1768
|
+
feature_key: requestedFeature,
|
|
1769
|
+
}, req);
|
|
1770
|
+
return {
|
|
1771
|
+
project_id: project.project_id,
|
|
1772
|
+
project_alias: payload.project_alias || null,
|
|
1773
|
+
feature_key: requestedFeature,
|
|
1774
|
+
pack,
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
handleProjectDeveloperQuery(payload, req) {
|
|
1778
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
1779
|
+
const parsed = this.parseProjectDeveloperQueryPayload(payload);
|
|
1780
|
+
const query = parsed.query_text;
|
|
1781
|
+
const project = this.resolveProjectRef(identity.userId, identity.agentId, {
|
|
1782
|
+
project_id: payload.project_id,
|
|
1783
|
+
project_alias: payload.project_alias,
|
|
1784
|
+
});
|
|
1785
|
+
const inferredIntent = parsed.legacy_intent;
|
|
1786
|
+
const retrievalPlan = this.buildDeveloperRetrievalPlan(parsed.canonical_intent, payload.limit, parsed);
|
|
1787
|
+
const queryFingerprint = createHash("sha1")
|
|
1788
|
+
.update(`${project.project_id}|${parsed.canonical_intent}|${query.toLowerCase()}`)
|
|
1789
|
+
.digest("hex")
|
|
1790
|
+
.slice(0, 16);
|
|
1791
|
+
const queryId = `pdevq:${queryFingerprint}`;
|
|
1792
|
+
const trackerIssueHint = parsed.tracker_issue_key || parsed.tracker_issue_keys?.[0] || query.match(/\b[A-Z][A-Z0-9_]+-\d+\b/)?.[0];
|
|
1793
|
+
const hybridTaskContext = retrievalPlan.use_task_context
|
|
1794
|
+
? {
|
|
1795
|
+
tracker_issue_key: trackerIssueHint,
|
|
1796
|
+
task_id: parsed.task_id,
|
|
1797
|
+
task_title: parsed.task_title,
|
|
1798
|
+
include_related: true,
|
|
1799
|
+
include_parent_chain: inferredIntent === "trace_flow",
|
|
1800
|
+
}
|
|
1801
|
+
: undefined;
|
|
1802
|
+
let locate = this.handleProjectHybridSearch({
|
|
1803
|
+
project_id: project.project_id,
|
|
1804
|
+
query,
|
|
1805
|
+
limit: retrievalPlan.locate_limit,
|
|
1806
|
+
debug: false,
|
|
1807
|
+
...(hybridTaskContext ? { task_context: hybridTaskContext } : {}),
|
|
1808
|
+
...(retrievalPlan.path_prefix_hint && retrievalPlan.path_prefix_hint.length > 0
|
|
1809
|
+
? { path_prefix: retrievalPlan.path_prefix_hint }
|
|
1810
|
+
: {}),
|
|
1811
|
+
}, req);
|
|
1812
|
+
let locateFallbackUsed = false;
|
|
1813
|
+
if (locate.results.length === 0 && hybridTaskContext) {
|
|
1814
|
+
locate = this.handleProjectHybridSearch({
|
|
1815
|
+
project_id: project.project_id,
|
|
1816
|
+
query,
|
|
1817
|
+
limit: retrievalPlan.locate_limit,
|
|
1818
|
+
debug: false,
|
|
1819
|
+
...(retrievalPlan.path_prefix_hint && retrievalPlan.path_prefix_hint.length > 0
|
|
1820
|
+
? { path_prefix: retrievalPlan.path_prefix_hint }
|
|
1821
|
+
: {}),
|
|
1822
|
+
}, req);
|
|
1823
|
+
locateFallbackUsed = true;
|
|
1824
|
+
}
|
|
1825
|
+
const topN = {
|
|
1826
|
+
primary_results: 12,
|
|
1827
|
+
files: 12,
|
|
1828
|
+
symbols: 16,
|
|
1829
|
+
snippets: 10,
|
|
1830
|
+
graph_paths: 8,
|
|
1831
|
+
change_context: 8,
|
|
1832
|
+
answer_points: 5,
|
|
1833
|
+
};
|
|
1834
|
+
const sourceRank = retrievalPlan.source_priority.reduce((acc, source, index) => {
|
|
1835
|
+
acc[source] = index;
|
|
1836
|
+
return acc;
|
|
1837
|
+
}, {
|
|
1838
|
+
symbol_registry: 99,
|
|
1839
|
+
file_index_state: 99,
|
|
1840
|
+
chunk_registry: 99,
|
|
1841
|
+
task_registry: 99,
|
|
1842
|
+
});
|
|
1843
|
+
const sourceBoostByPriority = retrievalPlan.source_priority.reduce((acc, source, index) => {
|
|
1844
|
+
acc[source] = Math.max(0, 0.15 - index * 0.04);
|
|
1845
|
+
return acc;
|
|
1846
|
+
}, {
|
|
1847
|
+
symbol_registry: 0,
|
|
1848
|
+
file_index_state: 0,
|
|
1849
|
+
chunk_registry: 0,
|
|
1850
|
+
task_registry: 0,
|
|
1851
|
+
});
|
|
1852
|
+
const sortStringsStable = (values) => values.sort((a, b) => a.localeCompare(b));
|
|
1853
|
+
const locateScored = locate.results.map((item) => {
|
|
1854
|
+
const source = item.source;
|
|
1855
|
+
const adjustedScore = Number((Number(item.score || 0) + (sourceBoostByPriority[source] || 0)).toFixed(4));
|
|
1856
|
+
return {
|
|
1857
|
+
...item,
|
|
1858
|
+
adjusted_score: adjustedScore,
|
|
1859
|
+
};
|
|
1860
|
+
});
|
|
1861
|
+
const locateSorted = [...locateScored].sort((a, b) => Number(b.adjusted_score || 0) - Number(a.adjusted_score || 0)
|
|
1862
|
+
|| (sourceRank[a.source] ?? 99) - (sourceRank[b.source] ?? 99)
|
|
1863
|
+
|| String(a.relative_path || "").localeCompare(String(b.relative_path || ""))
|
|
1864
|
+
|| String(a.symbol_name || "").localeCompare(String(b.symbol_name || ""))
|
|
1865
|
+
|| String(a.task_id || a.id).localeCompare(String(b.task_id || b.id)));
|
|
1866
|
+
const locatePrimary = locateSorted.map((item) => ({
|
|
1867
|
+
type: item.source === "file_index_state"
|
|
1868
|
+
? "file"
|
|
1869
|
+
: item.source === "symbol_registry"
|
|
1870
|
+
? "symbol"
|
|
1871
|
+
: item.source === "chunk_registry"
|
|
1872
|
+
? "chunk"
|
|
1873
|
+
: "task",
|
|
1874
|
+
id: item.id,
|
|
1875
|
+
title: item.symbol_name
|
|
1876
|
+
? `${item.symbol_name}${item.relative_path ? ` (${item.relative_path})` : ""}`
|
|
1877
|
+
: item.relative_path || item.task_title || item.id,
|
|
1878
|
+
score: item.adjusted_score,
|
|
1879
|
+
relative_path: item.relative_path,
|
|
1880
|
+
symbol_name: item.symbol_name,
|
|
1881
|
+
snippet: item.snippet,
|
|
1882
|
+
}));
|
|
1883
|
+
const requestedFeature = this.pickFeatureKeyForIntent(inferredIntent, parsed.feature_key || payload.feature_key, payload.feature_name, query);
|
|
1884
|
+
const shouldAttachFeaturePack = retrievalPlan.attach_feature_pack
|
|
1885
|
+
|| Boolean(parsed.feature_key || payload.feature_key || payload.feature_name);
|
|
1886
|
+
let featurePacks = [];
|
|
1887
|
+
if (shouldAttachFeaturePack && requestedFeature) {
|
|
1888
|
+
try {
|
|
1889
|
+
const featureQuery = this.handleProjectFeaturePackQuery({
|
|
1890
|
+
project_id: project.project_id,
|
|
1891
|
+
feature_key: requestedFeature,
|
|
1892
|
+
}, req);
|
|
1893
|
+
featurePacks = [featureQuery.pack];
|
|
1894
|
+
}
|
|
1895
|
+
catch {
|
|
1896
|
+
featurePacks = [];
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
const overlaySelector = this.pickOverlaySelectorFromLocate(locate.results, parsed);
|
|
1900
|
+
let overlay = null;
|
|
1901
|
+
if (retrievalPlan.attach_overlay && (overlaySelector.task_id || overlaySelector.tracker_issue_key)) {
|
|
1902
|
+
try {
|
|
1903
|
+
overlay = this.handleProjectChangeOverlayQuery({
|
|
1904
|
+
project_id: project.project_id,
|
|
1905
|
+
task_id: overlaySelector.task_id,
|
|
1906
|
+
tracker_issue_key: overlaySelector.tracker_issue_key,
|
|
1907
|
+
feature_key: requestedFeature || undefined,
|
|
1908
|
+
include_related: true,
|
|
1909
|
+
include_parent_chain: true,
|
|
1910
|
+
}, req);
|
|
1911
|
+
}
|
|
1912
|
+
catch {
|
|
1913
|
+
overlay = null;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
const featurePrimary = featurePacks
|
|
1917
|
+
.map((pack) => ({
|
|
1918
|
+
type: "feature_pack",
|
|
1919
|
+
id: pack.pack_id,
|
|
1920
|
+
title: pack.title,
|
|
1921
|
+
score: 1,
|
|
1922
|
+
snippet: pack.summary,
|
|
1923
|
+
}))
|
|
1924
|
+
.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
1925
|
+
const overlaySymbolPrimary = parsed.canonical_intent === "change_lookup"
|
|
1926
|
+
? [...(overlay?.related_symbols || [])]
|
|
1927
|
+
.sort((a, b) => Number(b.confidence || 0) - Number(a.confidence || 0)
|
|
1928
|
+
|| String(a.relative_path || "").localeCompare(String(b.relative_path || ""))
|
|
1929
|
+
|| String(a.symbol_name || "").localeCompare(String(b.symbol_name || "")))
|
|
1930
|
+
.slice(0, 3)
|
|
1931
|
+
.map((symbol) => ({
|
|
1932
|
+
type: "symbol",
|
|
1933
|
+
id: symbol.symbol_fqn || symbol.symbol_name,
|
|
1934
|
+
title: `${symbol.symbol_name}${symbol.relative_path ? ` (${symbol.relative_path})` : ""}`,
|
|
1935
|
+
score: symbol.confidence,
|
|
1936
|
+
relative_path: symbol.relative_path,
|
|
1937
|
+
symbol_name: symbol.symbol_name,
|
|
1938
|
+
}))
|
|
1939
|
+
: [];
|
|
1940
|
+
const mergedPrimaryCandidates = [
|
|
1941
|
+
...(retrievalPlan.prefer_feature_primary ? featurePrimary : []),
|
|
1942
|
+
...overlaySymbolPrimary,
|
|
1943
|
+
...locatePrimary,
|
|
1944
|
+
...(!retrievalPlan.prefer_feature_primary ? featurePrimary : []),
|
|
1945
|
+
];
|
|
1946
|
+
const mergedPrimaryDeduped = Array.from(new Map(mergedPrimaryCandidates.map((item) => {
|
|
1947
|
+
const key = `${item.type}:${item.id}`;
|
|
1948
|
+
return [key, item];
|
|
1949
|
+
})).values());
|
|
1950
|
+
const mergedPrimary = mergedPrimaryDeduped
|
|
1951
|
+
.sort((a, b) => Number(b.score || 0) - Number(a.score || 0)
|
|
1952
|
+
|| String(a.type).localeCompare(String(b.type))
|
|
1953
|
+
|| String(a.relative_path || "").localeCompare(String(b.relative_path || ""))
|
|
1954
|
+
|| String(a.title).localeCompare(String(b.title)))
|
|
1955
|
+
.slice(0, topN.primary_results);
|
|
1956
|
+
const files = sortStringsStable(Array.from(new Set([
|
|
1957
|
+
...locateSorted.map((item) => item.relative_path).filter(Boolean),
|
|
1958
|
+
...(overlay?.changed_files || []),
|
|
1959
|
+
...featurePacks.flatMap((pack) => pack.primary_files || []),
|
|
1960
|
+
]))).slice(0, topN.files);
|
|
1961
|
+
const symbols = sortStringsStable(Array.from(new Set([
|
|
1962
|
+
...locateSorted.map((item) => item.symbol_name).filter(Boolean),
|
|
1963
|
+
...(overlay?.related_symbols || []).map((item) => item.symbol_fqn || item.symbol_name).filter(Boolean),
|
|
1964
|
+
...featurePacks.flatMap((pack) => pack.primary_symbols || []),
|
|
1965
|
+
]))).slice(0, topN.symbols);
|
|
1966
|
+
const snippets = sortStringsStable(Array.from(new Set([
|
|
1967
|
+
...locateSorted.map((item) => String(item.snippet || "").trim()).filter(Boolean),
|
|
1968
|
+
...featurePacks.map((pack) => pack.summary).filter(Boolean),
|
|
1969
|
+
...(overlay
|
|
1970
|
+
? [
|
|
1971
|
+
`overlay focus ${overlay.focus.task_id}${overlay.focus.tracker_issue_key ? ` (${overlay.focus.tracker_issue_key})` : ""}`,
|
|
1972
|
+
...overlay.feature_packs.slice(0, 2).map((pack) => `${pack.feature_key}: ${pack.note || "overlay match"}`),
|
|
1973
|
+
]
|
|
1974
|
+
: []),
|
|
1975
|
+
]))).slice(0, topN.snippets);
|
|
1976
|
+
const graphPaths = sortStringsStable(overlay
|
|
1977
|
+
? Array.from(new Set(overlay.related_symbols
|
|
1978
|
+
.map((item) => `${item.relative_path || "unknown"}::${item.symbol_fqn || item.symbol_name}`)))
|
|
1979
|
+
: []).slice(0, topN.graph_paths);
|
|
1980
|
+
const changeContext = sortStringsStable(Array.from(new Set([
|
|
1981
|
+
...locateSorted
|
|
1982
|
+
.map((item) => item.task_id || item.tracker_issue_key)
|
|
1983
|
+
.filter((value) => Boolean(value)),
|
|
1984
|
+
...(overlay ? [overlay.focus.task_id, ...(overlay.focus.tracker_issue_key ? [overlay.focus.tracker_issue_key] : [])] : []),
|
|
1985
|
+
...featurePacks.flatMap((pack) => pack.related_tasks || []),
|
|
1986
|
+
]))).slice(0, topN.change_context);
|
|
1987
|
+
const assemblySources = Array.from(new Set([
|
|
1988
|
+
...(files.length > 0 ? ["file"] : []),
|
|
1989
|
+
...(symbols.length > 0 ? ["symbol"] : []),
|
|
1990
|
+
...(featurePacks.length > 0 ? ["feature_pack"] : []),
|
|
1991
|
+
...(overlay ? ["change_overlay"] : []),
|
|
1992
|
+
])).sort((a, b) => a.localeCompare(b));
|
|
1993
|
+
const confidenceOverall = Number(Math.min(0.97, (locate.results.length > 0 ? 0.45 : 0.1)
|
|
1994
|
+
+ (featurePacks.length > 0 ? 0.2 : 0)
|
|
1995
|
+
+ (overlay ? 0.2 : 0)
|
|
1996
|
+
+ (parsed.canonical_intent === "change_lookup" && overlay ? 0.07 : 0)
|
|
1997
|
+
+ (inferredIntent === "trace_flow" && !locateFallbackUsed ? 0.03 : 0)
|
|
1998
|
+
+ Math.min(0.07, snippets.length * 0.01)).toFixed(2));
|
|
1999
|
+
const confidenceReasonParts = [];
|
|
2000
|
+
confidenceReasonParts.push(`intent=${inferredIntent}`);
|
|
2001
|
+
confidenceReasonParts.push(`locate_hits=${locate.results.length}`);
|
|
2002
|
+
confidenceReasonParts.push(`plan=${retrievalPlan.plan_key}`);
|
|
2003
|
+
if (featurePacks.length > 0)
|
|
2004
|
+
confidenceReasonParts.push(`feature_pack=${featurePacks[0].feature_key}`);
|
|
2005
|
+
if (overlay)
|
|
2006
|
+
confidenceReasonParts.push(`overlay_focus=${overlay.focus.task_id}`);
|
|
2007
|
+
if (locateFallbackUsed)
|
|
2008
|
+
confidenceReasonParts.push("trace_task_context_fallback=true");
|
|
2009
|
+
const whyThisResult = [
|
|
2010
|
+
"resolved via project.hybrid_search",
|
|
2011
|
+
`retrieval plan ${retrievalPlan.plan_key} source priority: ${retrievalPlan.source_priority.join(" > ")}`,
|
|
2012
|
+
...(hybridTaskContext ? ["intent-aware task_context applied"] : []),
|
|
2013
|
+
...(retrievalPlan.path_prefix_hint && retrievalPlan.path_prefix_hint.length > 0
|
|
2014
|
+
? [`path_prefix hint: ${retrievalPlan.path_prefix_hint.join(", ")}`]
|
|
2015
|
+
: []),
|
|
2016
|
+
...(featurePacks.length > 0 ? ["enriched via project.feature_pack.query"] : []),
|
|
2017
|
+
...(overlay ? ["enriched via project.change_overlay.query"] : []),
|
|
2018
|
+
`result_count=${locate.count}`,
|
|
2019
|
+
"stable ordering: score desc -> source/path/title",
|
|
2020
|
+
"dedup key: primary(type:id), lists via set",
|
|
2021
|
+
];
|
|
2022
|
+
const answerTemplate = parsed.canonical_intent === "locate_symbol" || parsed.canonical_intent === "locate_file"
|
|
2023
|
+
? "locate"
|
|
2024
|
+
: parsed.canonical_intent === "feature_lookup"
|
|
2025
|
+
? "feature_understanding"
|
|
2026
|
+
: "generic";
|
|
2027
|
+
const answerSummary = answerTemplate === "locate"
|
|
2028
|
+
? `Located ${mergedPrimary.length} candidate result(s) for '${query}' in project ${project.project_id}.`
|
|
2029
|
+
: answerTemplate === "feature_understanding"
|
|
2030
|
+
? `Feature understanding assembled for '${query}' with ${featurePacks.length} feature pack(s).`
|
|
2031
|
+
: `Developer query '${query}' resolved with ${assemblySources.length} assembly source(s).`;
|
|
2032
|
+
const answerPoints = (answerTemplate === "locate"
|
|
2033
|
+
? [
|
|
2034
|
+
mergedPrimary[0] ? `Top hit: ${mergedPrimary[0].title}` : "Top hit: none",
|
|
2035
|
+
files.length > 0 ? `Files: ${files.slice(0, 3).join(", ")}` : "Files: none",
|
|
2036
|
+
symbols.length > 0 ? `Symbols: ${symbols.slice(0, 3).join(", ")}` : "Symbols: none",
|
|
2037
|
+
`Assembly sources: ${assemblySources.join(", ") || "none"}`,
|
|
2038
|
+
]
|
|
2039
|
+
: answerTemplate === "feature_understanding"
|
|
2040
|
+
? [
|
|
2041
|
+
featurePacks[0] ? `Feature pack: ${featurePacks[0].title}` : "Feature pack: none",
|
|
2042
|
+
featurePacks[0]?.summary ? `Summary: ${featurePacks[0].summary}` : "Summary: none",
|
|
2043
|
+
featurePacks[0]?.primary_files?.length ? `Primary files: ${featurePacks[0].primary_files.slice(0, 3).join(", ")}` : "Primary files: none",
|
|
2044
|
+
featurePacks[0]?.primary_symbols?.length ? `Primary symbols: ${featurePacks[0].primary_symbols.slice(0, 3).join(", ")}` : "Primary symbols: none",
|
|
2045
|
+
]
|
|
2046
|
+
: [
|
|
2047
|
+
`Intent: ${inferredIntent}`,
|
|
2048
|
+
`Top result: ${mergedPrimary[0]?.title || "none"}`,
|
|
2049
|
+
`Assembly sources: ${assemblySources.join(", ") || "none"}`,
|
|
2050
|
+
])
|
|
2051
|
+
.slice(0, topN.answer_points);
|
|
2052
|
+
return {
|
|
2053
|
+
query_id: queryId,
|
|
2054
|
+
intent: inferredIntent,
|
|
2055
|
+
project_id: project.project_id,
|
|
2056
|
+
project_alias: payload.project_alias || null,
|
|
2057
|
+
query,
|
|
2058
|
+
primary_results: mergedPrimary,
|
|
2059
|
+
files,
|
|
2060
|
+
symbols,
|
|
2061
|
+
snippets,
|
|
2062
|
+
graph_paths: graphPaths,
|
|
2063
|
+
feature_packs: featurePacks,
|
|
2064
|
+
change_context: changeContext,
|
|
2065
|
+
assembly_sources: assemblySources,
|
|
2066
|
+
answer_template: answerTemplate,
|
|
2067
|
+
answer_summary: answerSummary,
|
|
2068
|
+
answer_points: answerPoints,
|
|
2069
|
+
explainability: {
|
|
2070
|
+
ranking_rules: [
|
|
2071
|
+
"typed query parser maps query -> canonical intent/selectors deterministically",
|
|
2072
|
+
`retrieval plan ${retrievalPlan.plan_key} applies source priority ${retrievalPlan.source_priority.join(" > ")}`,
|
|
2073
|
+
"primary_results sorted by score desc, then type/path/title",
|
|
2074
|
+
"files/symbols/snippets/graph_paths/change_context sorted lexicographically",
|
|
2075
|
+
"hybrid locate candidates pre-sorted by score/source/path/symbol/task",
|
|
2076
|
+
],
|
|
2077
|
+
top_n: topN,
|
|
2078
|
+
evidence_counts: {
|
|
2079
|
+
locate_hits: locate.results.length,
|
|
2080
|
+
feature_pack_hits: featurePacks.length,
|
|
2081
|
+
overlay_changed_files: overlay?.changed_files.length || 0,
|
|
2082
|
+
overlay_related_symbols: overlay?.related_symbols.length || 0,
|
|
2083
|
+
},
|
|
2084
|
+
dedup: {
|
|
2085
|
+
primary_results: true,
|
|
2086
|
+
files: true,
|
|
2087
|
+
symbols: true,
|
|
2088
|
+
snippets: true,
|
|
2089
|
+
graph_paths: true,
|
|
2090
|
+
change_context: true,
|
|
2091
|
+
},
|
|
2092
|
+
fallbacks: [
|
|
2093
|
+
...(locateFallbackUsed ? ["trace_task_context_fallback=true"] : []),
|
|
2094
|
+
...(featurePacks.length === 0 && shouldAttachFeaturePack ? ["feature_pack_unavailable_or_unresolved"] : []),
|
|
2095
|
+
...(overlay === null && retrievalPlan.attach_overlay ? ["overlay_unavailable_or_unresolved"] : []),
|
|
2096
|
+
],
|
|
2097
|
+
},
|
|
2098
|
+
confidence: {
|
|
2099
|
+
overall: confidenceOverall,
|
|
2100
|
+
reason: confidenceReasonParts.join("; ") || "minimal evidence",
|
|
2101
|
+
},
|
|
2102
|
+
why_this_result: whyThisResult,
|
|
2103
|
+
generated_at: new Date().toISOString(),
|
|
2104
|
+
generator_version: "asm-109-slice8",
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
buildDeveloperRetrievalPlan(intent, requestedLimit, parsed) {
|
|
2108
|
+
const locateLimit = Math.min(Math.max(Number(requestedLimit || 8), 1), 20);
|
|
2109
|
+
if (intent === "locate_symbol") {
|
|
2110
|
+
return {
|
|
2111
|
+
plan_key: "locate_symbol",
|
|
2112
|
+
source_priority: ["symbol_registry", "chunk_registry", "file_index_state", "task_registry"],
|
|
2113
|
+
locate_limit: locateLimit,
|
|
2114
|
+
use_task_context: false,
|
|
2115
|
+
attach_feature_pack: false,
|
|
2116
|
+
attach_overlay: false,
|
|
2117
|
+
prefer_feature_primary: false,
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
if (intent === "locate_file") {
|
|
2121
|
+
return {
|
|
2122
|
+
plan_key: "locate_file",
|
|
2123
|
+
source_priority: ["file_index_state", "chunk_registry", "symbol_registry", "task_registry"],
|
|
2124
|
+
locate_limit: locateLimit,
|
|
2125
|
+
use_task_context: false,
|
|
2126
|
+
attach_feature_pack: false,
|
|
2127
|
+
attach_overlay: false,
|
|
2128
|
+
prefer_feature_primary: false,
|
|
2129
|
+
path_prefix_hint: this.buildLocatePathPrefixHint(parsed.relative_path || parsed.query_text),
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
if (intent === "feature_lookup") {
|
|
2133
|
+
return {
|
|
2134
|
+
plan_key: "feature_lookup",
|
|
2135
|
+
source_priority: ["symbol_registry", "file_index_state", "task_registry", "chunk_registry"],
|
|
2136
|
+
locate_limit: Math.min(locateLimit, 12),
|
|
2137
|
+
use_task_context: false,
|
|
2138
|
+
attach_feature_pack: true,
|
|
2139
|
+
attach_overlay: false,
|
|
2140
|
+
prefer_feature_primary: true,
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
return {
|
|
2144
|
+
plan_key: "change_lookup",
|
|
2145
|
+
source_priority: ["task_registry", "symbol_registry", "file_index_state", "chunk_registry"],
|
|
2146
|
+
locate_limit: locateLimit,
|
|
2147
|
+
use_task_context: true,
|
|
2148
|
+
attach_feature_pack: true,
|
|
2149
|
+
attach_overlay: true,
|
|
2150
|
+
prefer_feature_primary: false,
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
buildLocatePathPrefixHint(rawPath) {
|
|
2154
|
+
const normalized = String(rawPath || "").trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2155
|
+
if (!normalized || !normalized.includes("/"))
|
|
2156
|
+
return undefined;
|
|
2157
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
2158
|
+
if (segments.length === 0)
|
|
2159
|
+
return undefined;
|
|
2160
|
+
const hints = [normalized];
|
|
2161
|
+
if (segments.length > 1) {
|
|
2162
|
+
hints.push(segments.slice(0, -1).join("/"));
|
|
2163
|
+
}
|
|
2164
|
+
hints.push(segments[0]);
|
|
2165
|
+
return Array.from(new Set(hints)).filter(Boolean);
|
|
2166
|
+
}
|
|
2167
|
+
parseProjectDeveloperQueryPayload(payload) {
|
|
2168
|
+
const explicitIntent = payload.intent;
|
|
2169
|
+
const query = String(payload.query || "").trim();
|
|
2170
|
+
const symbolName = String(payload.symbol_name || "").trim();
|
|
2171
|
+
const relativePath = String(payload.relative_path || "").trim();
|
|
2172
|
+
const routePath = String(payload.route_path || "").trim();
|
|
2173
|
+
const trackerIssueKey = String(payload.tracker_issue_key || "").trim();
|
|
2174
|
+
const taskId = String(payload.task_id || "").trim();
|
|
2175
|
+
const taskTitle = String(payload.task_title || "").trim();
|
|
2176
|
+
const trackerIssueKeys = this.extractTrackerIssueKeys(query);
|
|
2177
|
+
const taskIds = this.extractTaskIds(query);
|
|
2178
|
+
const routePaths = this.extractRoutePaths(query);
|
|
2179
|
+
const inferredFeatureKey = this.extractFeatureKeyFromQuery(query);
|
|
2180
|
+
const canonicalFromExplicit = {
|
|
2181
|
+
locate_symbol: "locate_symbol",
|
|
2182
|
+
locate_file: "locate_file",
|
|
2183
|
+
feature_lookup: "feature_lookup",
|
|
2184
|
+
change_lookup: "change_lookup",
|
|
2185
|
+
locate: "locate_symbol",
|
|
2186
|
+
trace_flow: "change_lookup",
|
|
2187
|
+
impact: "change_lookup",
|
|
2188
|
+
impact_analysis: "change_lookup",
|
|
2189
|
+
change_aware_lookup: "change_lookup",
|
|
2190
|
+
feature_understanding: "feature_lookup",
|
|
2191
|
+
};
|
|
2192
|
+
const canonical_intent = explicitIntent
|
|
2193
|
+
? canonicalFromExplicit[explicitIntent]
|
|
2194
|
+
: this.inferCanonicalIntentFromQuery({
|
|
2195
|
+
query,
|
|
2196
|
+
symbolName,
|
|
2197
|
+
relativePath,
|
|
2198
|
+
routePath: routePath || routePaths[0],
|
|
2199
|
+
trackerIssueKey: trackerIssueKey || trackerIssueKeys[0],
|
|
2200
|
+
taskId: taskId || taskIds[0],
|
|
2201
|
+
taskTitle,
|
|
2202
|
+
hasFeatureSelector: Boolean(payload.feature_key || payload.feature_name),
|
|
2203
|
+
});
|
|
2204
|
+
const explicitLegacyIntents = [
|
|
2205
|
+
"locate",
|
|
2206
|
+
"trace_flow",
|
|
2207
|
+
"impact",
|
|
2208
|
+
"impact_analysis",
|
|
2209
|
+
"change_aware_lookup",
|
|
2210
|
+
"feature_understanding",
|
|
2211
|
+
];
|
|
2212
|
+
const legacy_intent = explicitIntent && explicitLegacyIntents.includes(explicitIntent)
|
|
2213
|
+
? explicitIntent
|
|
2214
|
+
: (canonical_intent === "feature_lookup"
|
|
2215
|
+
? "feature_understanding"
|
|
2216
|
+
: canonical_intent === "change_lookup"
|
|
2217
|
+
? "change_aware_lookup"
|
|
2218
|
+
: "locate");
|
|
2219
|
+
const feature_key = payload.feature_key
|
|
2220
|
+
|| (payload.feature_name ? this.tryResolveFeatureKeyInput(undefined, payload.feature_name) : null)
|
|
2221
|
+
|| ((canonical_intent === "feature_lookup" && !query && inferredFeatureKey) ? inferredFeatureKey : null)
|
|
2222
|
+
|| (canonical_intent === "change_lookup" ? "change_aware_impact" : undefined);
|
|
2223
|
+
const query_text = String(canonical_intent === "locate_symbol"
|
|
2224
|
+
? (symbolName || query)
|
|
2225
|
+
: canonical_intent === "locate_file"
|
|
2226
|
+
? (relativePath || query)
|
|
2227
|
+
: canonical_intent === "feature_lookup"
|
|
2228
|
+
? (query || payload.feature_name || feature_key || "")
|
|
2229
|
+
: (query || trackerIssueKey || taskId || taskTitle || "")).trim();
|
|
2230
|
+
if (!query_text) {
|
|
2231
|
+
throw new Error("project.developer_query requires payload.query or deterministic selectors");
|
|
2232
|
+
}
|
|
2233
|
+
return {
|
|
2234
|
+
canonical_intent,
|
|
2235
|
+
legacy_intent,
|
|
2236
|
+
query_text,
|
|
2237
|
+
...(symbolName ? { symbol_name: symbolName } : {}),
|
|
2238
|
+
...(relativePath ? { relative_path: relativePath } : {}),
|
|
2239
|
+
...((routePath || routePaths[0]) ? { route_path: routePath || routePaths[0] } : {}),
|
|
2240
|
+
...(routePaths.length > 0 ? { route_paths: routePaths } : {}),
|
|
2241
|
+
...((trackerIssueKey || trackerIssueKeys[0]) ? { tracker_issue_key: trackerIssueKey || trackerIssueKeys[0] } : {}),
|
|
2242
|
+
...((taskId || taskIds[0]) ? { task_id: taskId || taskIds[0] } : {}),
|
|
2243
|
+
...(taskTitle ? { task_title: taskTitle } : {}),
|
|
2244
|
+
...(trackerIssueKeys.length > 0 ? { tracker_issue_keys: trackerIssueKeys } : {}),
|
|
2245
|
+
...(taskIds.length > 0 ? { task_ids: taskIds } : {}),
|
|
2246
|
+
...(feature_key ? { feature_key } : {}),
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
inferCanonicalIntentFromQuery(input) {
|
|
2250
|
+
if (input.symbolName)
|
|
2251
|
+
return "locate_symbol";
|
|
2252
|
+
if (input.trackerIssueKey || input.taskId || input.taskTitle)
|
|
2253
|
+
return "change_lookup";
|
|
2254
|
+
if (input.relativePath || input.routePath)
|
|
2255
|
+
return "locate_file";
|
|
2256
|
+
const lowered = input.query.toLowerCase();
|
|
2257
|
+
if (/what breaks if|blast radius|affected|impact|impact analysis|change-aware|change aware|overlay|lookup/.test(lowered)) {
|
|
2258
|
+
return "change_lookup";
|
|
2259
|
+
}
|
|
2260
|
+
if (/where does .* flow|trace|flow/.test(lowered)) {
|
|
2261
|
+
return "change_lookup";
|
|
2262
|
+
}
|
|
2263
|
+
if (/who handles|entrypoint for|where is .* implemented|route|endpoint|api\//.test(lowered)) {
|
|
2264
|
+
return "locate_file";
|
|
2265
|
+
}
|
|
2266
|
+
if (/file|path|\.tsx?|\.jsx?|\/src\//.test(lowered)) {
|
|
2267
|
+
return "locate_file";
|
|
2268
|
+
}
|
|
2269
|
+
if (input.hasFeatureSelector || this.extractFeatureKeyFromQuery(input.query)) {
|
|
2270
|
+
return "feature_lookup";
|
|
2271
|
+
}
|
|
2272
|
+
return "locate_symbol";
|
|
2273
|
+
}
|
|
2274
|
+
pickFeatureKeyForIntent(intent, featureKey, featureName, query) {
|
|
2275
|
+
if (featureKey || featureName) {
|
|
2276
|
+
const resolved = this.tryResolveFeatureKeyInput(featureKey, featureName);
|
|
2277
|
+
if (resolved)
|
|
2278
|
+
return resolved;
|
|
2279
|
+
}
|
|
2280
|
+
if (intent === "impact" || intent === "impact_analysis" || intent === "change_aware_lookup" || intent === "change_lookup") {
|
|
2281
|
+
return "change_aware_impact";
|
|
2282
|
+
}
|
|
2283
|
+
if (intent === "feature_understanding" || intent === "feature_lookup") {
|
|
2284
|
+
if (query)
|
|
2285
|
+
return this.tryResolveFeatureKeyInput(undefined, query);
|
|
2286
|
+
}
|
|
2287
|
+
return null;
|
|
2288
|
+
}
|
|
2289
|
+
tryResolveFeatureKeyInput(featureKey, featureName) {
|
|
2290
|
+
try {
|
|
2291
|
+
return this.resolveFeatureKeyInput(featureKey, featureName);
|
|
2292
|
+
}
|
|
2293
|
+
catch {
|
|
2294
|
+
return null;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
pickOverlaySelectorFromLocate(items, parsed) {
|
|
2298
|
+
const firstTaskId = items.map((item) => item.task_id).find((value) => Boolean(value));
|
|
2299
|
+
const firstIssue = items
|
|
2300
|
+
.map((item) => item.tracker_issue_key)
|
|
2301
|
+
.find((value) => Boolean(value));
|
|
2302
|
+
return {
|
|
2303
|
+
task_id: parsed?.task_id || parsed?.task_ids?.[0] || firstTaskId,
|
|
2304
|
+
tracker_issue_key: parsed?.tracker_issue_key || parsed?.tracker_issue_keys?.[0] || firstIssue,
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
extractTrackerIssueKeys(query) {
|
|
2308
|
+
const matches = String(query || "").match(/\b[A-Z][A-Z0-9_]+-\d+\b/g) || [];
|
|
2309
|
+
return Array.from(new Set(matches.map((item) => item.trim().toUpperCase()).filter(Boolean)));
|
|
2310
|
+
}
|
|
2311
|
+
extractTaskIds(query) {
|
|
2312
|
+
const matches = String(query || "").match(/\btask-[a-z0-9-]+\b/gi) || [];
|
|
2313
|
+
return Array.from(new Set(matches.map((item) => item.trim()).filter(Boolean)));
|
|
2314
|
+
}
|
|
2315
|
+
extractRoutePaths(query) {
|
|
2316
|
+
const matches = String(query || "").match(/\/(?:[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*)?/g) || [];
|
|
2317
|
+
return Array.from(new Set(matches.map((item) => item.trim()).filter((item) => item.startsWith("/") && item.length >= 2)));
|
|
2318
|
+
}
|
|
2319
|
+
extractFeatureKeyFromQuery(query) {
|
|
2320
|
+
const normalized = String(query || "").trim();
|
|
2321
|
+
if (!normalized)
|
|
2322
|
+
return null;
|
|
2323
|
+
return this.tryResolveFeatureKeyInput(undefined, normalized);
|
|
2324
|
+
}
|
|
2325
|
+
resolveFeatureKeyInput(featureKey, featureName) {
|
|
2326
|
+
if (featureKey)
|
|
2327
|
+
return featureKey;
|
|
2328
|
+
const raw = String(featureName || "").trim().toLowerCase();
|
|
2329
|
+
if (!raw)
|
|
2330
|
+
return "project_onboarding_registration_indexing";
|
|
2331
|
+
if (FEATURE_PACK_KEYS.includes(raw)) {
|
|
2332
|
+
return raw;
|
|
2333
|
+
}
|
|
2334
|
+
const compact = raw.replace(/[^a-z0-9]+/g, " ").trim();
|
|
2335
|
+
const has = (token) => compact.includes(token);
|
|
2336
|
+
if ((has("onboarding") || has("registration") || has("index")) && !has("retrieval")) {
|
|
2337
|
+
return "project_onboarding_registration_indexing";
|
|
2338
|
+
}
|
|
2339
|
+
if (has("retrieval") || has("code aware") || has("hybrid")) {
|
|
2340
|
+
return "code_aware_retrieval";
|
|
2341
|
+
}
|
|
2342
|
+
if (has("heartbeat") || has("health") || has("integrity") || has("runtime")) {
|
|
2343
|
+
return "heartbeat_health_runtime_integrity";
|
|
2344
|
+
}
|
|
2345
|
+
if (has("impact") || has("change aware") || has("change")) {
|
|
2346
|
+
return "change_aware_impact";
|
|
2347
|
+
}
|
|
2348
|
+
if (has("post entry") || has("post-entry") || has("review") || has("decision")) {
|
|
2349
|
+
return "post_entry_review_decision_support";
|
|
2350
|
+
}
|
|
2351
|
+
throw new Error(`Unsupported feature selector '${featureName}'. Supported keys: ${FEATURE_PACK_KEYS.join(", ")}`);
|
|
2352
|
+
}
|
|
2353
|
+
buildProjectOnboardingRegistrationIndexingPack(snapshot, projectId, primaryAlias) {
|
|
2354
|
+
const latestRun = snapshot.recent_index_runs[0] || null;
|
|
2355
|
+
const primaryFiles = Array.from(new Set([
|
|
2356
|
+
"src/commands/telegram-addproject-command.ts",
|
|
2357
|
+
"src/tools/project-tools.ts",
|
|
2358
|
+
"src/core/usecases/default-memory-usecase-port.ts",
|
|
2359
|
+
...snapshot.recent_files.map((item) => item.relative_path),
|
|
2360
|
+
].filter(Boolean))).slice(0, 12);
|
|
2361
|
+
const primarySymbols = this.rankPrimarySymbols(snapshot, [
|
|
2362
|
+
"project.telegram_onboarding",
|
|
2363
|
+
"project.register_command",
|
|
2364
|
+
"project.link_tracker",
|
|
2365
|
+
"project.trigger_index",
|
|
2366
|
+
"project.reindex_diff",
|
|
2367
|
+
"registerTelegramAddProjectCommand",
|
|
2368
|
+
"registerProjectTools",
|
|
2369
|
+
"handleProjectRegisterCommand",
|
|
2370
|
+
"handleProjectTriggerIndex",
|
|
2371
|
+
]);
|
|
2372
|
+
return {
|
|
2373
|
+
pack_id: `feature-pack:project_onboarding_registration_indexing:${projectId}`,
|
|
2374
|
+
title: "Project onboarding / registration / indexing",
|
|
2375
|
+
feature_key: "project_onboarding_registration_indexing",
|
|
2376
|
+
summary: `Covers the cross-agent project setup flow from /project onboarding through registry persistence, optional tracker linking, and index/reindex execution for project '${primaryAlias}'.`,
|
|
2377
|
+
primary_files: primaryFiles,
|
|
2378
|
+
primary_symbols: primarySymbols,
|
|
2379
|
+
flow_steps: [
|
|
2380
|
+
{
|
|
2381
|
+
step: 1,
|
|
2382
|
+
title: "Operator enters onboarding",
|
|
2383
|
+
details: "Telegram /project command and project onboarding helper collect repo, alias, Jira, and index-now intent.",
|
|
2384
|
+
related_files: ["src/commands/telegram-addproject-command.ts", "src/tools/project-tools.ts"],
|
|
2385
|
+
related_symbols: ["registerTelegramAddProjectCommand", "project.telegram_onboarding"],
|
|
2386
|
+
},
|
|
2387
|
+
{
|
|
2388
|
+
step: 2,
|
|
2389
|
+
title: "Registration command resolves repo and persists registry state",
|
|
2390
|
+
details: `project.register_command normalizes alias '${primaryAlias}', resolves repo root/remote, writes project + alias + registration state, and can attach tracker mapping.`,
|
|
2391
|
+
related_files: ["src/core/usecases/default-memory-usecase-port.ts", "src/db/slot-db.ts"],
|
|
2392
|
+
related_symbols: ["handleProjectRegisterCommand", "project.register_command"],
|
|
2393
|
+
},
|
|
2394
|
+
{
|
|
2395
|
+
step: 3,
|
|
2396
|
+
title: "Tracker linking enriches project identity",
|
|
2397
|
+
details: "jira/github/other mapping is stored in project tracker mappings so later agents can navigate issue space consistently.",
|
|
2398
|
+
related_files: ["src/core/usecases/default-memory-usecase-port.ts", "src/tools/project-tools.ts"],
|
|
2399
|
+
related_symbols: ["handleProjectLinkTracker", "project.link_tracker"],
|
|
2400
|
+
},
|
|
2401
|
+
{
|
|
2402
|
+
step: 4,
|
|
2403
|
+
title: "Index bootstrap or reindex updates searchable project context",
|
|
2404
|
+
details: `latest known index state is '${latestRun?.state || "not_yet_indexed"}' via ${latestRun?.trigger_type || "bootstrap/manual"} path; background trigger and diff reindex feed file/symbol/chunk registries.`,
|
|
2405
|
+
related_files: ["src/core/usecases/default-memory-usecase-port.ts", "src/db/slot-db.ts"],
|
|
2406
|
+
related_symbols: ["handleProjectTriggerIndex", "project.trigger_index", "project.reindex_diff"],
|
|
2407
|
+
},
|
|
2408
|
+
],
|
|
2409
|
+
risk_points: [
|
|
2410
|
+
"Repo resolution can bind to wrong working tree if repo_root/repo_url selection is inconsistent.",
|
|
2411
|
+
"Invalid Jira space/default epic pairing blocks confirm flow.",
|
|
2412
|
+
"Index trigger may be accepted before concrete paths exist, so first pack consumers should inspect recent index run/watch state.",
|
|
2413
|
+
],
|
|
2414
|
+
test_points: [
|
|
2415
|
+
"project.telegram_onboarding preview rejects invalid Jira mapping and returns summary card.",
|
|
2416
|
+
"project.register_command persists project, alias, registration, and optional tracker mapping.",
|
|
2417
|
+
"project.trigger_index or project.reindex_diff updates index_runs and file/symbol registries for the target project.",
|
|
2418
|
+
],
|
|
2419
|
+
related_tasks: this.collectRelatedTasks(snapshot),
|
|
2420
|
+
related_commits: this.collectRelatedCommitHints(snapshot, ["register", "index", "onboarding"]),
|
|
2421
|
+
related_prs: [],
|
|
2422
|
+
evidence: this.buildEvidenceOrdered(snapshot, {
|
|
2423
|
+
includeRegistration: true,
|
|
2424
|
+
includeTracker: true,
|
|
2425
|
+
includeIndexRuns: 2,
|
|
2426
|
+
includeTasks: 4,
|
|
2427
|
+
includeFiles: 4,
|
|
2428
|
+
includeSymbols: 4,
|
|
2429
|
+
}),
|
|
2430
|
+
generated_at: new Date().toISOString(),
|
|
2431
|
+
generator_version: "asm-93-slice2",
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
buildCodeAwareRetrievalPack(snapshot, projectId, primaryAlias) {
|
|
2435
|
+
const primaryFiles = Array.from(new Set([
|
|
2436
|
+
"src/db/slot-db.ts",
|
|
2437
|
+
"src/core/usecases/default-memory-usecase-port.ts",
|
|
2438
|
+
"src/tools/project-tools.ts",
|
|
2439
|
+
...snapshot.recent_files.map((item) => item.relative_path),
|
|
2440
|
+
].filter(Boolean))).slice(0, 12);
|
|
2441
|
+
const primarySymbols = this.rankPrimarySymbols(snapshot, [
|
|
2442
|
+
"project.hybrid_search",
|
|
2443
|
+
"project.task_lineage_context",
|
|
2444
|
+
"graph.code.upsert",
|
|
2445
|
+
"graph.code.chain",
|
|
2446
|
+
"project.reindex_diff",
|
|
2447
|
+
]);
|
|
2448
|
+
return {
|
|
2449
|
+
pack_id: `feature-pack:code_aware_retrieval:${projectId}`,
|
|
2450
|
+
title: "Code-aware retrieval",
|
|
2451
|
+
feature_key: "code_aware_retrieval",
|
|
2452
|
+
summary: `Covers retrieving actionable code context for project '${primaryAlias}' from indexed files/symbols/chunks/tasks with optional lineage expansion and code-graph traversal signals.`,
|
|
2453
|
+
primary_files: primaryFiles,
|
|
2454
|
+
primary_symbols: primarySymbols,
|
|
2455
|
+
flow_steps: [
|
|
2456
|
+
{
|
|
2457
|
+
step: 1,
|
|
2458
|
+
title: "Ingest/reindex populates retrieval registries",
|
|
2459
|
+
details: "project.reindex_diff writes file_index_state, symbol_registry, and chunk_registry with active entries for changed files.",
|
|
2460
|
+
related_files: ["src/db/slot-db.ts", "src/core/usecases/default-memory-usecase-port.ts"],
|
|
2461
|
+
related_symbols: ["project.reindex_diff", "handleProjectReindexDiff"],
|
|
2462
|
+
},
|
|
2463
|
+
{
|
|
2464
|
+
step: 2,
|
|
2465
|
+
title: "Task lineage context narrows retrieval intent",
|
|
2466
|
+
details: "project.task_lineage_context assembles focus task, parent chain, related tasks, and touched symbols/files before ranking.",
|
|
2467
|
+
related_files: ["src/db/slot-db.ts", "src/core/usecases/default-memory-usecase-port.ts"],
|
|
2468
|
+
related_symbols: ["project.task_lineage_context", "handleProjectTaskLineageContext"],
|
|
2469
|
+
},
|
|
2470
|
+
{
|
|
2471
|
+
step: 3,
|
|
2472
|
+
title: "Hybrid retrieval ranks candidates across registries",
|
|
2473
|
+
details: "project.hybrid_search blends file/symbol/chunk/task candidates and supports debug candidate buckets for conformance checks.",
|
|
2474
|
+
related_files: ["src/db/slot-db.ts", "src/tools/project-tools.ts"],
|
|
2475
|
+
related_symbols: ["project.hybrid_search", "handleProjectHybridSearch"],
|
|
2476
|
+
},
|
|
2477
|
+
{
|
|
2478
|
+
step: 4,
|
|
2479
|
+
title: "Code graph traversal augments symbol relations",
|
|
2480
|
+
details: "graph.code.upsert / graph.code.chain provide relation-level traversal for dependency/call chains when symbol-level context is needed.",
|
|
2481
|
+
related_files: ["src/core/usecases/default-memory-usecase-port.ts", "src/tools/graph-tools.ts"],
|
|
2482
|
+
related_symbols: ["graph.code.upsert", "graph.code.chain"],
|
|
2483
|
+
},
|
|
2484
|
+
],
|
|
2485
|
+
risk_points: [
|
|
2486
|
+
"Hybrid retrieval quality depends on freshness of reindex runs and active symbol/chunk state.",
|
|
2487
|
+
"Weak or missing task metadata can reduce lineage-assisted ranking quality.",
|
|
2488
|
+
],
|
|
2489
|
+
test_points: [
|
|
2490
|
+
"project.reindex_diff persists symbols/chunks for changed files.",
|
|
2491
|
+
"project.task_lineage_context returns parent/related context for known task ids.",
|
|
2492
|
+
"project.hybrid_search returns ranked results with debug buckets when requested.",
|
|
2493
|
+
],
|
|
2494
|
+
related_tasks: this.collectRelatedTasks(snapshot),
|
|
2495
|
+
related_commits: this.collectRelatedCommitHints(snapshot, ["hybrid", "retrieval", "reindex", "graph", "symbol"]),
|
|
2496
|
+
related_prs: [],
|
|
2497
|
+
evidence: this.buildEvidenceOrdered(snapshot, {
|
|
2498
|
+
includeRegistration: false,
|
|
2499
|
+
includeTracker: false,
|
|
2500
|
+
includeIndexRuns: 2,
|
|
2501
|
+
includeTasks: 6,
|
|
2502
|
+
includeFiles: 6,
|
|
2503
|
+
includeSymbols: 8,
|
|
2504
|
+
}),
|
|
2505
|
+
generated_at: new Date().toISOString(),
|
|
2506
|
+
generator_version: "asm-93-slice2",
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
buildHeartbeatHealthRuntimeIntegrityPack(snapshot, projectId, primaryAlias) {
|
|
2510
|
+
const latestRun = snapshot.recent_index_runs[0] || null;
|
|
2511
|
+
return {
|
|
2512
|
+
pack_id: `feature-pack:heartbeat_health_runtime_integrity:${projectId}`,
|
|
2513
|
+
title: "Heartbeat / health / runtime integrity",
|
|
2514
|
+
feature_key: "heartbeat_health_runtime_integrity",
|
|
2515
|
+
summary: `Covers runtime integrity signals for project '${primaryAlias}' via registration validation state, tracker linkage consistency, and latest index run heartbeat evidence.`,
|
|
2516
|
+
primary_files: Array.from(new Set([
|
|
2517
|
+
"src/core/usecases/default-memory-usecase-port.ts",
|
|
2518
|
+
"src/db/slot-db.ts",
|
|
2519
|
+
"src/tools/project-tools.ts",
|
|
2520
|
+
...snapshot.recent_files.map((item) => item.relative_path),
|
|
2521
|
+
])).slice(0, 10),
|
|
2522
|
+
primary_symbols: this.rankPrimarySymbols(snapshot, [
|
|
2523
|
+
"project.get",
|
|
2524
|
+
"project.set_registration_state",
|
|
2525
|
+
"project.link_tracker",
|
|
2526
|
+
"project.trigger_index",
|
|
2527
|
+
"project.index_watch_get",
|
|
2528
|
+
]),
|
|
2529
|
+
flow_steps: [
|
|
2530
|
+
{
|
|
2531
|
+
step: 1,
|
|
2532
|
+
title: "Registration lifecycle state is the control baseline",
|
|
2533
|
+
details: "project registration_status + validation_status represent current readiness and integrity posture.",
|
|
2534
|
+
related_files: ["src/db/slot-db.ts"],
|
|
2535
|
+
related_symbols: ["project.set_registration_state", "project.get"],
|
|
2536
|
+
},
|
|
2537
|
+
{
|
|
2538
|
+
step: 2,
|
|
2539
|
+
title: "Tracker mapping coherence is validated",
|
|
2540
|
+
details: "Jira/GitHub tracker mappings are validated and persisted, reducing cross-agent drift in runtime operations.",
|
|
2541
|
+
related_files: ["src/core/usecases/default-memory-usecase-port.ts"],
|
|
2542
|
+
related_symbols: ["project.link_tracker", "handleProjectLinkTracker"],
|
|
2543
|
+
},
|
|
2544
|
+
{
|
|
2545
|
+
step: 3,
|
|
2546
|
+
title: "Index runs provide heartbeat for data-plane readiness",
|
|
2547
|
+
details: `latest run is '${latestRun?.state || "none"}' (${latestRun?.trigger_type || "n/a"}); index_runs are used as runtime heartbeat checkpoints.`,
|
|
2548
|
+
related_files: ["src/db/slot-db.ts"],
|
|
2549
|
+
related_symbols: ["project.trigger_index", "project.index_watch_get"],
|
|
2550
|
+
},
|
|
2551
|
+
],
|
|
2552
|
+
risk_points: [
|
|
2553
|
+
"No recent index run can indicate stale runtime context even if registration is valid.",
|
|
2554
|
+
"Validation status can be stale if lifecycle updates are not maintained after tracker/repo changes.",
|
|
2555
|
+
],
|
|
2556
|
+
test_points: [
|
|
2557
|
+
"project.get exposes registration + tracker mapping state for an alias/project id.",
|
|
2558
|
+
"project.trigger_index updates index_runs and can be used as heartbeat signal.",
|
|
2559
|
+
"project.index_watch_get returns checksum/revision watch state for integrity checks.",
|
|
2560
|
+
],
|
|
2561
|
+
related_tasks: this.collectRelatedTasks(snapshot),
|
|
2562
|
+
related_commits: this.collectRelatedCommitHints(snapshot, ["health", "integrity", "watch", "registration", "index"]),
|
|
2563
|
+
related_prs: [],
|
|
2564
|
+
evidence: this.buildEvidenceOrdered(snapshot, {
|
|
2565
|
+
includeRegistration: true,
|
|
2566
|
+
includeTracker: true,
|
|
2567
|
+
includeIndexRuns: 3,
|
|
2568
|
+
includeTasks: 3,
|
|
2569
|
+
includeFiles: 3,
|
|
2570
|
+
includeSymbols: 3,
|
|
2571
|
+
}),
|
|
2572
|
+
generated_at: new Date().toISOString(),
|
|
2573
|
+
generator_version: "asm-93-slice2",
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
buildChangeAwareImpactPack(snapshot, projectId, primaryAlias) {
|
|
2577
|
+
const latestRun = snapshot.recent_index_runs[0] || null;
|
|
2578
|
+
return {
|
|
2579
|
+
pack_id: `feature-pack:change_aware_impact:${projectId}`,
|
|
2580
|
+
title: "Change-aware impact",
|
|
2581
|
+
feature_key: "change_aware_impact",
|
|
2582
|
+
summary: `Covers impact-oriented flow for project '${primaryAlias}' where changed files/symbols are reindexed and then consumed by lineage/hybrid retrieval to estimate downstream effect scope.`,
|
|
2583
|
+
primary_files: Array.from(new Set([
|
|
2584
|
+
"src/db/slot-db.ts",
|
|
2585
|
+
"src/core/usecases/default-memory-usecase-port.ts",
|
|
2586
|
+
"src/tools/project-tools.ts",
|
|
2587
|
+
...snapshot.recent_files.map((item) => item.relative_path),
|
|
2588
|
+
])).slice(0, 12),
|
|
2589
|
+
primary_symbols: this.rankPrimarySymbols(snapshot, [
|
|
2590
|
+
"project.reindex_diff",
|
|
2591
|
+
"project.index_event",
|
|
2592
|
+
"project.index_watch_get",
|
|
2593
|
+
"project.task_registry_upsert",
|
|
2594
|
+
"project.task_lineage_context",
|
|
2595
|
+
"project.hybrid_search",
|
|
2596
|
+
]),
|
|
2597
|
+
flow_steps: [
|
|
2598
|
+
{
|
|
2599
|
+
step: 1,
|
|
2600
|
+
title: "Diff/event ingestion captures changed paths",
|
|
2601
|
+
details: "project.reindex_diff (or project.index_event) accepts changed/deleted files and updates index/watch state.",
|
|
2602
|
+
related_files: ["src/db/slot-db.ts", "src/core/usecases/default-memory-usecase-port.ts"],
|
|
2603
|
+
related_symbols: ["project.reindex_diff", "project.index_event", "project.index_watch_get"],
|
|
2604
|
+
},
|
|
2605
|
+
{
|
|
2606
|
+
step: 2,
|
|
2607
|
+
title: "Task lineage stores declared impact hints",
|
|
2608
|
+
details: "project.task_registry_upsert tracks files_touched/symbols_touched/related tasks to enrich later impact analysis.",
|
|
2609
|
+
related_files: ["src/db/slot-db.ts"],
|
|
2610
|
+
related_symbols: ["project.task_registry_upsert", "project.task_lineage_context"],
|
|
2611
|
+
},
|
|
2612
|
+
{
|
|
2613
|
+
step: 3,
|
|
2614
|
+
title: "Hybrid retrieval assembles impact candidates",
|
|
2615
|
+
details: "project.hybrid_search combines changed files/symbols and lineage context to return practical impact candidates.",
|
|
2616
|
+
related_files: ["src/db/slot-db.ts", "src/tools/project-tools.ts"],
|
|
2617
|
+
related_symbols: ["project.hybrid_search"],
|
|
2618
|
+
},
|
|
2619
|
+
],
|
|
2620
|
+
risk_points: [
|
|
2621
|
+
"Placeholder checksums or stale watch state can hide true file deltas.",
|
|
2622
|
+
"Impact accuracy depends on discipline in task_registry_upsert metadata quality.",
|
|
2623
|
+
],
|
|
2624
|
+
test_points: [
|
|
2625
|
+
"project.reindex_diff updates file index and watch state for changed/deleted paths.",
|
|
2626
|
+
"project.task_registry_upsert captures files_touched and symbols_touched.",
|
|
2627
|
+
"project.hybrid_search can be constrained by task context for impact-oriented queries.",
|
|
2628
|
+
],
|
|
2629
|
+
related_tasks: this.collectRelatedTasks(snapshot),
|
|
2630
|
+
related_commits: this.collectRelatedCommitHints(snapshot, ["reindex", "diff", "impact", "lineage", "watch"]),
|
|
2631
|
+
related_prs: [],
|
|
2632
|
+
evidence: this.buildEvidenceOrdered(snapshot, {
|
|
2633
|
+
includeRegistration: false,
|
|
2634
|
+
includeTracker: false,
|
|
2635
|
+
includeIndexRuns: 3,
|
|
2636
|
+
includeTasks: 6,
|
|
2637
|
+
includeFiles: 8,
|
|
2638
|
+
includeSymbols: 6,
|
|
2639
|
+
}),
|
|
2640
|
+
generated_at: new Date().toISOString(),
|
|
2641
|
+
generator_version: "asm-93-slice2",
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
buildPostEntryReviewDecisionSupportPack(snapshot, projectId, primaryAlias) {
|
|
2645
|
+
const keywordMatchers = ["post-entry", "post entry", "review", "decision", "outcome", "policy", "trace"];
|
|
2646
|
+
const matchingTasks = snapshot.recent_tasks.filter((task) => {
|
|
2647
|
+
const title = task.task_title.toLowerCase();
|
|
2648
|
+
return keywordMatchers.some((kw) => title.includes(kw));
|
|
2649
|
+
});
|
|
2650
|
+
const matchingSymbols = snapshot.recent_symbols.filter((symbol) => {
|
|
2651
|
+
const s = `${symbol.symbol_name} ${symbol.symbol_fqn}`.toLowerCase();
|
|
2652
|
+
return keywordMatchers.some((kw) => s.includes(kw));
|
|
2653
|
+
});
|
|
2654
|
+
if (matchingTasks.length === 0 && matchingSymbols.length === 0) {
|
|
2655
|
+
throw new Error("feature_key post_entry_review_decision_support does not have enough indexed evidence yet (need task/symbol signals for post-entry/review/decision).");
|
|
2656
|
+
}
|
|
2657
|
+
return {
|
|
2658
|
+
pack_id: `feature-pack:post_entry_review_decision_support:${projectId}`,
|
|
2659
|
+
title: "Post-entry review decision support",
|
|
2660
|
+
feature_key: "post_entry_review_decision_support",
|
|
2661
|
+
summary: `Covers post-entry decision support for project '${primaryAlias}' by prioritizing review/outcome evidence from task + symbol history and mapping it into retrieval-ready surfaces.`,
|
|
2662
|
+
primary_files: Array.from(new Set([
|
|
2663
|
+
...matchingSymbols.map((item) => item.relative_path),
|
|
2664
|
+
...snapshot.recent_files.map((item) => item.relative_path),
|
|
2665
|
+
"src/core/usecases/default-memory-usecase-port.ts",
|
|
2666
|
+
"src/db/slot-db.ts",
|
|
2667
|
+
].filter(Boolean))).slice(0, 10),
|
|
2668
|
+
primary_symbols: this.rankPrimarySymbols(snapshot, [
|
|
2669
|
+
"project.task_registry_upsert",
|
|
2670
|
+
"project.task_lineage_context",
|
|
2671
|
+
"project.hybrid_search",
|
|
2672
|
+
...matchingSymbols.map((item) => item.symbol_fqn || item.symbol_name),
|
|
2673
|
+
]),
|
|
2674
|
+
flow_steps: [
|
|
2675
|
+
{
|
|
2676
|
+
step: 1,
|
|
2677
|
+
title: "Review/decision traces are captured in task metadata",
|
|
2678
|
+
details: "task_registry entries store decision_notes, task_status, tracker key, and touched files/symbols to preserve post-entry context.",
|
|
2679
|
+
related_files: ["src/db/slot-db.ts"],
|
|
2680
|
+
related_symbols: ["project.task_registry_upsert"],
|
|
2681
|
+
},
|
|
2682
|
+
{
|
|
2683
|
+
step: 2,
|
|
2684
|
+
title: "Lineage context reconstructs decision chain",
|
|
2685
|
+
details: "project.task_lineage_context can reconstruct parent/related chain to explain why a post-entry action was taken.",
|
|
2686
|
+
related_files: ["src/core/usecases/default-memory-usecase-port.ts"],
|
|
2687
|
+
related_symbols: ["project.task_lineage_context"],
|
|
2688
|
+
},
|
|
2689
|
+
{
|
|
2690
|
+
step: 3,
|
|
2691
|
+
title: "Hybrid retrieval surfaces decision-support evidence",
|
|
2692
|
+
details: "project.hybrid_search ranks symbols/files/tasks so agents can consume review evidence with minimal manual browsing.",
|
|
2693
|
+
related_files: ["src/db/slot-db.ts", "src/tools/project-tools.ts"],
|
|
2694
|
+
related_symbols: ["project.hybrid_search"],
|
|
2695
|
+
},
|
|
2696
|
+
],
|
|
2697
|
+
risk_points: [
|
|
2698
|
+
"If task titles/notes do not contain explicit review/decision markers, this pack can become too weak.",
|
|
2699
|
+
"Without indexed symbols related to review/outcome logic, evidence may skew toward generic tasks.",
|
|
2700
|
+
],
|
|
2701
|
+
test_points: [
|
|
2702
|
+
"project.task_registry_upsert stores decision-oriented metadata for review tasks.",
|
|
2703
|
+
"project.task_lineage_context returns parent/related chain for decision tasks.",
|
|
2704
|
+
"project.hybrid_search retrieves decision keywords from task/symbol/file registries.",
|
|
2705
|
+
],
|
|
2706
|
+
related_tasks: Array.from(new Set(matchingTasks
|
|
2707
|
+
.flatMap((task) => [task.tracker_issue_key, task.task_id])
|
|
2708
|
+
.filter(Boolean))).slice(0, 12),
|
|
2709
|
+
related_commits: this.collectRelatedCommitHints(snapshot, ["post-entry", "review", "decision", "outcome", "trace"]),
|
|
2710
|
+
related_prs: [],
|
|
2711
|
+
evidence: this.buildEvidenceOrdered(snapshot, {
|
|
2712
|
+
includeRegistration: false,
|
|
2713
|
+
includeTracker: false,
|
|
2714
|
+
includeIndexRuns: 2,
|
|
2715
|
+
includeTasks: 8,
|
|
2716
|
+
includeFiles: 6,
|
|
2717
|
+
includeSymbols: 8,
|
|
2718
|
+
}),
|
|
2719
|
+
generated_at: new Date().toISOString(),
|
|
2720
|
+
generator_version: "asm-93-slice2",
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
rankPrimarySymbols(snapshot, preferred) {
|
|
2724
|
+
const snapshotSymbols = snapshot.recent_symbols.flatMap((item) => [item.symbol_fqn, item.symbol_name]);
|
|
2725
|
+
return Array.from(new Set([...preferred, ...snapshotSymbols].filter(Boolean))).slice(0, 16);
|
|
2726
|
+
}
|
|
2727
|
+
collectRelatedTasks(snapshot) {
|
|
2728
|
+
return Array.from(new Set(snapshot.recent_tasks
|
|
2729
|
+
.flatMap((task) => [task.tracker_issue_key, task.task_id])
|
|
2730
|
+
.filter(Boolean))).slice(0, 12);
|
|
2731
|
+
}
|
|
2732
|
+
collectRelatedCommitHints(snapshot, keywords) {
|
|
2733
|
+
const lowered = keywords.map((kw) => kw.toLowerCase());
|
|
2734
|
+
return Array.from(new Set(snapshot.recent_tasks
|
|
2735
|
+
.map((task) => {
|
|
2736
|
+
const title = task.task_title.toLowerCase();
|
|
2737
|
+
const hit = lowered.find((kw) => title.includes(kw));
|
|
2738
|
+
return hit ? `task:${hit.replace(/\s+/g, "-")}` : null;
|
|
2739
|
+
})
|
|
2740
|
+
.filter(Boolean)));
|
|
2741
|
+
}
|
|
2742
|
+
buildEvidenceOrdered(snapshot, options) {
|
|
2743
|
+
const registrationState = snapshot.registration;
|
|
2744
|
+
const jiraMapping = snapshot.tracker_mappings.find((item) => item.tracker_type === "jira") || snapshot.tracker_mappings[0] || null;
|
|
2745
|
+
return [
|
|
2746
|
+
{ type: "project", ref: snapshot.project.project_id, note: snapshot.project.project_name },
|
|
2747
|
+
...snapshot.aliases.slice(0, 3).map((item) => ({ type: "project", ref: item.project_alias, note: item.is_primary === 1 ? "primary_alias" : "alias" })),
|
|
2748
|
+
...(options.includeRegistration && registrationState
|
|
2749
|
+
? [{ type: "registration", ref: registrationState.registration_status, note: registrationState.validation_status }]
|
|
2750
|
+
: []),
|
|
2751
|
+
...(options.includeTracker && jiraMapping
|
|
2752
|
+
? [{ type: "tracker", ref: jiraMapping.tracker_type, note: jiraMapping.tracker_space_key || jiraMapping.default_epic_key || undefined }]
|
|
2753
|
+
: []),
|
|
2754
|
+
...snapshot.recent_index_runs.slice(0, options.includeIndexRuns).map((item) => ({ type: "index", ref: item.run_id, note: `${item.trigger_type}:${item.state}` })),
|
|
2755
|
+
...snapshot.recent_tasks.slice(0, options.includeTasks).map((item) => ({ type: "task", ref: item.tracker_issue_key || item.task_id, note: item.task_title })),
|
|
2756
|
+
...snapshot.recent_files.slice(0, options.includeFiles).map((item) => ({ type: "file", ref: item.relative_path })),
|
|
2757
|
+
...snapshot.recent_symbols.slice(0, options.includeSymbols).map((item) => ({ type: "symbol", ref: item.symbol_fqn || item.symbol_name, note: item.relative_path })),
|
|
2758
|
+
];
|
|
2759
|
+
}
|
|
1157
2760
|
handleGraphEntityGet(payload, req) {
|
|
1158
2761
|
const identity = normalizePrivateIdentity(req.context);
|
|
1159
2762
|
if (payload.id) {
|
|
@@ -1247,5 +2850,58 @@ asm project-event --project-id "$PROJECT_ID" --repo-root "$REPO_ROOT" --event-ty
|
|
|
1247
2850
|
relationships: traversed.relationships.filter((rel) => rel.relation_type === payload.relation_type),
|
|
1248
2851
|
};
|
|
1249
2852
|
}
|
|
2853
|
+
handleGraphCodeUpsert(payload, req) {
|
|
2854
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
2855
|
+
const nodes = Array.isArray(payload.nodes) ? payload.nodes : [];
|
|
2856
|
+
const relations = Array.isArray(payload.relations) ? payload.relations : [];
|
|
2857
|
+
if (nodes.length === 0) {
|
|
2858
|
+
throw new Error("graph.code.upsert requires non-empty nodes");
|
|
2859
|
+
}
|
|
2860
|
+
for (const node of nodes) {
|
|
2861
|
+
if (!node.node_id || !isUniversalGraphNodeType(node.node_type) || !node.name) {
|
|
2862
|
+
throw new Error("graph.code.upsert node invalid: require node_id, node_type, name");
|
|
2863
|
+
}
|
|
2864
|
+
upsertUniversalGraphNode(this.slotDb.graph, identity.userId, identity.agentId, node);
|
|
2865
|
+
}
|
|
2866
|
+
for (const relation of relations) {
|
|
2867
|
+
if (!relation.source_node_id ||
|
|
2868
|
+
!relation.target_node_id ||
|
|
2869
|
+
!isUniversalGraphRelationType(relation.relation_type) ||
|
|
2870
|
+
!isValidUniversalGraphProvenance(relation.provenance)) {
|
|
2871
|
+
throw new Error("graph.code.upsert relation invalid: require source_node_id, target_node_id, relation_type, provenance");
|
|
2872
|
+
}
|
|
2873
|
+
const source = this.slotDb.graph.getEntity(identity.userId, identity.agentId, relation.source_node_id);
|
|
2874
|
+
if (!source) {
|
|
2875
|
+
throw new Error(`graph.code.upsert relation source '${relation.source_node_id}' not found`);
|
|
2876
|
+
}
|
|
2877
|
+
const target = this.slotDb.graph.getEntity(identity.userId, identity.agentId, relation.target_node_id);
|
|
2878
|
+
if (!target) {
|
|
2879
|
+
throw new Error(`graph.code.upsert relation target '${relation.target_node_id}' not found`);
|
|
2880
|
+
}
|
|
2881
|
+
upsertUniversalGraphRelation(this.slotDb.graph, identity.userId, identity.agentId, relation);
|
|
2882
|
+
}
|
|
2883
|
+
return {
|
|
2884
|
+
graph_model: UNIVERSAL_GRAPH_MODEL_VERSION,
|
|
2885
|
+
nodes_upserted: nodes.length,
|
|
2886
|
+
relations_upserted: relations.length,
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
handleGraphCodeChain(payload, req) {
|
|
2890
|
+
const identity = normalizePrivateIdentity(req.context);
|
|
2891
|
+
const nodeId = String(payload.node_id || "").trim();
|
|
2892
|
+
if (!nodeId) {
|
|
2893
|
+
throw new Error("graph.code.chain requires node_id");
|
|
2894
|
+
}
|
|
2895
|
+
const depth = Math.min(Math.max(payload.depth || 2, 1), 4);
|
|
2896
|
+
const traversed = this.slotDb.graph.traverseCodeGraph(identity.userId, identity.agentId, nodeId, depth, payload.relation_type);
|
|
2897
|
+
const relationships = traversed.relationships;
|
|
2898
|
+
return {
|
|
2899
|
+
graph_model: UNIVERSAL_GRAPH_MODEL_VERSION,
|
|
2900
|
+
start_node_id: nodeId,
|
|
2901
|
+
depth,
|
|
2902
|
+
entities: traversed.entities,
|
|
2903
|
+
relationships,
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
1250
2906
|
}
|
|
1251
2907
|
//# sourceMappingURL=default-memory-usecase-port.js.map
|