claude-memory-layer 1.0.40 → 1.0.41

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.
@@ -2775,6 +2775,127 @@ var SQLiteEventStore = class {
2775
2775
  updated_at TEXT DEFAULT (datetime('now'))
2776
2776
  );
2777
2777
 
2778
+ -- Memory Operations: facet assignments (derived, rebuildable projection)
2779
+ CREATE TABLE IF NOT EXISTS memory_facets (
2780
+ id TEXT PRIMARY KEY,
2781
+ target_type TEXT NOT NULL,
2782
+ target_id TEXT NOT NULL,
2783
+ dimension TEXT NOT NULL,
2784
+ value TEXT NOT NULL,
2785
+ confidence REAL NOT NULL DEFAULT 1.0,
2786
+ source TEXT NOT NULL DEFAULT 'manual',
2787
+ evidence_event_ids TEXT NOT NULL DEFAULT '[]',
2788
+ project_hash TEXT NOT NULL DEFAULT '',
2789
+ created_at TEXT NOT NULL,
2790
+ updated_at TEXT NOT NULL,
2791
+ UNIQUE(target_type, target_id, dimension, value, source, project_hash)
2792
+ );
2793
+
2794
+ -- Memory Operations: operational action projection
2795
+ CREATE TABLE IF NOT EXISTS memory_actions (
2796
+ action_id TEXT PRIMARY KEY,
2797
+ project_hash TEXT NOT NULL,
2798
+ title TEXT NOT NULL,
2799
+ status TEXT NOT NULL DEFAULT 'pending',
2800
+ priority INTEGER NOT NULL DEFAULT 0,
2801
+ source_event_ids TEXT NOT NULL DEFAULT '[]',
2802
+ related_entity_ids TEXT NOT NULL DEFAULT '[]',
2803
+ current_checkpoint_id TEXT,
2804
+ lease_id TEXT,
2805
+ created_at TEXT NOT NULL,
2806
+ updated_at TEXT NOT NULL
2807
+ );
2808
+
2809
+ -- Memory Operations: action dependency/reference edges
2810
+ CREATE TABLE IF NOT EXISTS memory_action_edges (
2811
+ edge_id TEXT PRIMARY KEY,
2812
+ src_action_id TEXT NOT NULL,
2813
+ rel_type TEXT NOT NULL,
2814
+ dst_type TEXT NOT NULL,
2815
+ dst_id TEXT NOT NULL,
2816
+ confidence REAL NOT NULL DEFAULT 1.0,
2817
+ source TEXT NOT NULL DEFAULT 'manual',
2818
+ created_at TEXT NOT NULL,
2819
+ UNIQUE(src_action_id, rel_type, dst_type, dst_id, source)
2820
+ );
2821
+
2822
+ -- Memory Operations: short-lived leases for operational work
2823
+ CREATE TABLE IF NOT EXISTS memory_leases (
2824
+ lease_id TEXT PRIMARY KEY,
2825
+ target_type TEXT NOT NULL,
2826
+ target_id TEXT NOT NULL,
2827
+ holder TEXT NOT NULL,
2828
+ expires_at TEXT NOT NULL,
2829
+ metadata_json TEXT,
2830
+ created_at TEXT NOT NULL,
2831
+ renewed_at TEXT,
2832
+ released_at TEXT
2833
+ );
2834
+
2835
+ -- Memory Operations: resumable checkpoints for delegated or long-running work
2836
+ CREATE TABLE IF NOT EXISTS memory_checkpoints (
2837
+ checkpoint_id TEXT PRIMARY KEY,
2838
+ project_hash TEXT NOT NULL,
2839
+ action_id TEXT,
2840
+ session_id TEXT,
2841
+ title TEXT NOT NULL,
2842
+ summary TEXT NOT NULL,
2843
+ state_json TEXT NOT NULL,
2844
+ source_event_ids TEXT NOT NULL DEFAULT '[]',
2845
+ created_at TEXT NOT NULL,
2846
+ expires_at TEXT
2847
+ );
2848
+
2849
+ -- Memory Operations: retention lifecycle score projection
2850
+ CREATE TABLE IF NOT EXISTS memory_retention_scores (
2851
+ score_id TEXT PRIMARY KEY,
2852
+ target_type TEXT NOT NULL,
2853
+ target_id TEXT NOT NULL,
2854
+ project_hash TEXT NOT NULL,
2855
+ policy_version TEXT NOT NULL,
2856
+ decision TEXT NOT NULL,
2857
+ lifecycle_score REAL NOT NULL,
2858
+ factors_json TEXT NOT NULL,
2859
+ reasons_json TEXT NOT NULL,
2860
+ dry_run_diff_json TEXT NOT NULL,
2861
+ source_event_ids TEXT NOT NULL DEFAULT '[]',
2862
+ evaluated_at TEXT NOT NULL,
2863
+ created_at TEXT NOT NULL,
2864
+ updated_at TEXT NOT NULL,
2865
+ UNIQUE(target_type, target_id, project_hash, policy_version)
2866
+ );
2867
+
2868
+ -- Memory Operations: procedural lessons derived from successful workflows
2869
+ CREATE TABLE IF NOT EXISTS memory_lessons (
2870
+ lesson_id TEXT PRIMARY KEY,
2871
+ project_hash TEXT NOT NULL DEFAULT '',
2872
+ name TEXT NOT NULL,
2873
+ trigger TEXT NOT NULL,
2874
+ steps_json TEXT NOT NULL,
2875
+ confidence REAL NOT NULL,
2876
+ source_session_ids TEXT NOT NULL DEFAULT '[]',
2877
+ source_event_ids TEXT NOT NULL DEFAULT '[]',
2878
+ failure_modes_json TEXT NOT NULL DEFAULT '[]',
2879
+ skill_candidate INTEGER NOT NULL DEFAULT 0,
2880
+ created_at TEXT NOT NULL,
2881
+ updated_at TEXT NOT NULL,
2882
+ UNIQUE(project_hash, name)
2883
+ );
2884
+
2885
+ -- Memory Operations: governance/audit trail for state-changing operations
2886
+ CREATE TABLE IF NOT EXISTS memory_governance_audit (
2887
+ audit_id TEXT PRIMARY KEY,
2888
+ operation TEXT NOT NULL,
2889
+ actor TEXT NOT NULL,
2890
+ project_hash TEXT,
2891
+ target_type TEXT NOT NULL,
2892
+ target_id TEXT NOT NULL,
2893
+ before_json TEXT,
2894
+ after_json TEXT,
2895
+ source_event_ids TEXT NOT NULL DEFAULT '[]',
2896
+ created_at TEXT NOT NULL
2897
+ );
2898
+
2778
2899
  -- Create indexes
2779
2900
  CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
2780
2901
  CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
@@ -2801,6 +2922,23 @@ var SQLiteEventStore = class {
2801
2922
  CREATE INDEX IF NOT EXISTS idx_retrieval_traces_created_at ON retrieval_traces(created_at DESC);
2802
2923
  CREATE INDEX IF NOT EXISTS idx_retrieval_traces_project_hash ON retrieval_traces(project_hash);
2803
2924
  CREATE INDEX IF NOT EXISTS idx_retrieval_traces_session_id ON retrieval_traces(session_id);
2925
+ CREATE INDEX IF NOT EXISTS idx_memory_facets_project_dimension_value ON memory_facets(project_hash, dimension, value);
2926
+ CREATE INDEX IF NOT EXISTS idx_memory_facets_target ON memory_facets(target_type, target_id);
2927
+ CREATE INDEX IF NOT EXISTS idx_memory_facets_dimension_value_confidence ON memory_facets(dimension, value, confidence DESC);
2928
+ CREATE INDEX IF NOT EXISTS idx_memory_actions_project_status_priority ON memory_actions(project_hash, status, priority DESC, updated_at DESC);
2929
+ CREATE INDEX IF NOT EXISTS idx_memory_action_edges_src ON memory_action_edges(src_action_id, rel_type);
2930
+ CREATE INDEX IF NOT EXISTS idx_memory_action_edges_dst ON memory_action_edges(dst_type, dst_id);
2931
+ CREATE INDEX IF NOT EXISTS idx_memory_leases_target_expires ON memory_leases(target_type, target_id, expires_at);
2932
+ CREATE INDEX IF NOT EXISTS idx_memory_checkpoints_project_action_created ON memory_checkpoints(project_hash, action_id, created_at DESC);
2933
+ CREATE INDEX IF NOT EXISTS idx_memory_checkpoints_project_session_created ON memory_checkpoints(project_hash, session_id, created_at DESC);
2934
+ CREATE INDEX IF NOT EXISTS idx_memory_retention_scores_project_decision_score ON memory_retention_scores(project_hash, decision, lifecycle_score ASC, evaluated_at DESC);
2935
+ CREATE INDEX IF NOT EXISTS idx_memory_retention_scores_target ON memory_retention_scores(target_type, target_id, project_hash);
2936
+ CREATE INDEX IF NOT EXISTS idx_memory_retention_scores_policy_evaluated ON memory_retention_scores(policy_version, evaluated_at DESC);
2937
+ CREATE INDEX IF NOT EXISTS idx_memory_lessons_project_confidence ON memory_lessons(project_hash, confidence DESC, updated_at DESC);
2938
+ CREATE INDEX IF NOT EXISTS idx_memory_lessons_skill_candidate ON memory_lessons(project_hash, skill_candidate, confidence DESC);
2939
+ CREATE INDEX IF NOT EXISTS idx_memory_lessons_updated ON memory_lessons(updated_at DESC);
2940
+ CREATE INDEX IF NOT EXISTS idx_memory_governance_audit_project_operation ON memory_governance_audit(project_hash, operation, created_at DESC);
2941
+ CREATE INDEX IF NOT EXISTS idx_memory_governance_audit_target ON memory_governance_audit(target_type, target_id, created_at DESC);
2804
2942
 
2805
2943
  -- FTS5 Full-Text Search for fast keyword search
2806
2944
  CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
@@ -2823,6 +2961,48 @@ var SQLiteEventStore = class {
2823
2961
  INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
2824
2962
  END;
2825
2963
  `);
2964
+ try {
2965
+ sqliteExec(this.db, `ALTER TABLE memory_action_edges ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';`);
2966
+ } catch {
2967
+ }
2968
+ try {
2969
+ const edgeIndexes = sqliteAll(this.db, `PRAGMA index_list(memory_action_edges)`, []);
2970
+ const hasSourceAwareUnique = edgeIndexes.some((index) => {
2971
+ if (Number(index.unique) !== 1)
2972
+ return false;
2973
+ if (!/^[A-Za-z0-9_]+$/.test(index.name))
2974
+ return false;
2975
+ const escapedName = index.name.replace(/"/g, '""');
2976
+ const columns = sqliteAll(this.db, 'PRAGMA index_info("' + escapedName + '")', []).map((column) => column.name);
2977
+ return columns.length === 5 && columns[0] === "src_action_id" && columns[1] === "rel_type" && columns[2] === "dst_type" && columns[3] === "dst_id" && columns[4] === "source";
2978
+ });
2979
+ if (!hasSourceAwareUnique) {
2980
+ sqliteExec(this.db, `
2981
+ DROP TABLE IF EXISTS memory_action_edges_v2;
2982
+ CREATE TABLE memory_action_edges_v2 (
2983
+ edge_id TEXT PRIMARY KEY,
2984
+ src_action_id TEXT NOT NULL,
2985
+ rel_type TEXT NOT NULL,
2986
+ dst_type TEXT NOT NULL,
2987
+ dst_id TEXT NOT NULL,
2988
+ confidence REAL NOT NULL DEFAULT 1.0,
2989
+ source TEXT NOT NULL DEFAULT 'manual',
2990
+ created_at TEXT NOT NULL,
2991
+ UNIQUE(src_action_id, rel_type, dst_type, dst_id, source)
2992
+ );
2993
+ INSERT OR IGNORE INTO memory_action_edges_v2 (
2994
+ edge_id, src_action_id, rel_type, dst_type, dst_id, confidence, source, created_at
2995
+ )
2996
+ SELECT edge_id, src_action_id, rel_type, dst_type, dst_id, confidence, source, created_at
2997
+ FROM memory_action_edges;
2998
+ DROP TABLE memory_action_edges;
2999
+ ALTER TABLE memory_action_edges_v2 RENAME TO memory_action_edges;
3000
+ CREATE INDEX IF NOT EXISTS idx_memory_action_edges_src ON memory_action_edges(src_action_id, rel_type);
3001
+ CREATE INDEX IF NOT EXISTS idx_memory_action_edges_dst ON memory_action_edges(dst_type, dst_id);
3002
+ `);
3003
+ }
3004
+ } catch {
3005
+ }
2826
3006
  try {
2827
3007
  sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN selected_details_json TEXT;`);
2828
3008
  } catch {
@@ -4891,6 +5071,896 @@ var MemoryQueryService = class {
4891
5071
  }
4892
5072
  };
4893
5073
 
5074
+ // src/core/operations/facet-repository.ts
5075
+ import { randomUUID as randomUUID7 } from "crypto";
5076
+
5077
+ // src/core/operations/facets.ts
5078
+ import { z } from "zod";
5079
+ var FacetTargetTypeSchema = z.enum([
5080
+ "event",
5081
+ "entity",
5082
+ "edge",
5083
+ "consolidated_memory",
5084
+ "lesson",
5085
+ "action"
5086
+ ]);
5087
+ var BUILT_IN_FACET_DIMENSIONS = [
5088
+ "kind",
5089
+ "workflow",
5090
+ "artifact",
5091
+ "source",
5092
+ "privacy",
5093
+ "quality",
5094
+ "retention",
5095
+ "project"
5096
+ ];
5097
+ var customDimensionPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
5098
+ var TrimmedStringSchema = z.preprocess(
5099
+ (value) => typeof value === "string" ? value.trim() : value,
5100
+ z.string().min(1)
5101
+ );
5102
+ var FacetDimensionSchema = z.preprocess(
5103
+ (value) => typeof value === "string" ? value.trim() : value,
5104
+ z.string().min(1).max(64).refine((value) => BUILT_IN_FACET_DIMENSIONS.indexOf(value) !== -1 || customDimensionPattern.test(value), {
5105
+ message: "Facet dimension must be built-in or lowercase kebab-case"
5106
+ })
5107
+ );
5108
+ var FacetSourceSchema = z.enum(["manual", "imported", "derived", "llm", "system"]).default("manual");
5109
+ var EvidenceEventIdsSchema = z.preprocess(
5110
+ (value) => Array.isArray(value) ? value.map((item) => typeof item === "string" ? item.trim() : item).filter(Boolean) : value,
5111
+ z.array(z.string().min(1)).default([])
5112
+ );
5113
+ var OptionalTrimmedStringSchema = z.preprocess(
5114
+ (value) => typeof value === "string" ? value.trim() : value,
5115
+ z.string().min(1).optional()
5116
+ );
5117
+ var MemoryFacetAssignmentInputSchema = z.object({
5118
+ targetType: FacetTargetTypeSchema,
5119
+ targetId: TrimmedStringSchema,
5120
+ dimension: FacetDimensionSchema,
5121
+ value: TrimmedStringSchema,
5122
+ confidence: z.number().min(0).max(1).default(1),
5123
+ source: FacetSourceSchema,
5124
+ evidenceEventIds: EvidenceEventIdsSchema,
5125
+ projectHash: OptionalTrimmedStringSchema,
5126
+ actor: OptionalTrimmedStringSchema
5127
+ });
5128
+ var MemoryFacetAssignmentSchema = MemoryFacetAssignmentInputSchema.extend({
5129
+ id: z.string().min(1),
5130
+ createdAt: z.date(),
5131
+ updatedAt: z.date()
5132
+ });
5133
+ var FacetRemoveInputSchema = z.object({
5134
+ targetType: FacetTargetTypeSchema,
5135
+ targetId: TrimmedStringSchema,
5136
+ dimension: FacetDimensionSchema,
5137
+ value: TrimmedStringSchema,
5138
+ source: FacetSourceSchema,
5139
+ projectHash: OptionalTrimmedStringSchema,
5140
+ actor: OptionalTrimmedStringSchema
5141
+ });
5142
+ var FacetQuerySchema = z.object({
5143
+ targetType: FacetTargetTypeSchema.optional(),
5144
+ targetId: OptionalTrimmedStringSchema,
5145
+ dimension: FacetDimensionSchema.optional(),
5146
+ value: OptionalTrimmedStringSchema,
5147
+ source: FacetSourceSchema.optional(),
5148
+ projectHash: OptionalTrimmedStringSchema,
5149
+ limit: z.number().int().positive().max(500).default(100)
5150
+ });
5151
+ function parseFacetAssignmentInput(input) {
5152
+ return MemoryFacetAssignmentInputSchema.parse(input);
5153
+ }
5154
+ function parseFacetRemoveInput(input) {
5155
+ return FacetRemoveInputSchema.parse(input);
5156
+ }
5157
+ function parseFacetQuery(input) {
5158
+ return FacetQuerySchema.parse(input ?? {});
5159
+ }
5160
+
5161
+ // src/core/operations/governance-audit.ts
5162
+ import { randomUUID as randomUUID6 } from "crypto";
5163
+ var MEMORY_GOVERNANCE_AUDIT_OPERATIONS = [
5164
+ "facet_tag",
5165
+ "action_update",
5166
+ "lease_acquire",
5167
+ "checkpoint_create",
5168
+ "retention_score",
5169
+ "quarantine",
5170
+ "verify",
5171
+ "lesson_promote"
5172
+ ];
5173
+ function normalizeRequiredString(value, fieldName) {
5174
+ const normalized = value.trim();
5175
+ if (!normalized) {
5176
+ throw new Error(`${fieldName} is required`);
5177
+ }
5178
+ return normalized;
5179
+ }
5180
+ function normalizeOptionalString(value) {
5181
+ const normalized = value?.trim();
5182
+ return normalized ? normalized : void 0;
5183
+ }
5184
+ var REDACTED = "[REDACTED]";
5185
+ var sensitiveKeyPattern = /(?:api[_-]?key|secret|password|passwd|token|access[_-]?token|client[_-]?secret|crtfc[_-]?key|hashkey|serviceKey)/i;
5186
+ var POSIX_ABSOLUTE_PATH_PATTERN = /(^|[^A-Za-z0-9._\/\\-])\/(?!\/)[^\n\r"'<>|`]*/g;
5187
+ var WINDOWS_DRIVE_PATH_PATTERN = /(^|[^A-Za-z0-9._\/\\-])(?:[A-Za-z]:[\\/][^\n\r"'<>|`]*)/g;
5188
+ var WINDOWS_UNC_PATH_PATTERN = /(^|[^A-Za-z0-9._\/\\-])(?:\\\\[^\\\n\r"'<>|`]+\\[^\n\r"'<>|`]*)/g;
5189
+ var credentialQueryPattern = /\b((?:api[_-]?key|token|access[_-]?token|client[_-]?secret|crtfc[_-]?key|hashkey|serviceKey)=)[^&\s`"'<>]+/gi;
5190
+ var credentialAssignmentPattern = /\b((?:api[_-]?key|secret|password|passwd|token|access[_-]?token|client[_-]?secret|crtfc[_-]?key|hashkey|serviceKey)\s*[:=]\s*)[^\s`"'<>},]+/gi;
5191
+ function redactAbsolutePaths(value) {
5192
+ return value.replace(WINDOWS_UNC_PATH_PATTERN, (_match, prefix) => `${prefix}${REDACTED}`).replace(WINDOWS_DRIVE_PATH_PATTERN, (_match, prefix) => `${prefix}${REDACTED}`).replace(POSIX_ABSOLUTE_PATH_PATTERN, (_match, prefix) => `${prefix}${REDACTED}`);
5193
+ }
5194
+ function sanitizeAuditString(value) {
5195
+ return redactAbsolutePaths(value).replace(credentialQueryPattern, `$1${REDACTED}`).replace(credentialAssignmentPattern, `$1${REDACTED}`);
5196
+ }
5197
+ function sanitizeGovernanceAuditValue(value, key) {
5198
+ if (key && sensitiveKeyPattern.test(key)) {
5199
+ return REDACTED;
5200
+ }
5201
+ if (typeof value === "string") {
5202
+ return sanitizeAuditString(value);
5203
+ }
5204
+ if (value instanceof Date) {
5205
+ return sanitizeAuditString(value.toISOString());
5206
+ }
5207
+ if (Array.isArray(value)) {
5208
+ return value.map((item) => sanitizeGovernanceAuditValue(item));
5209
+ }
5210
+ if (value && typeof value === "object") {
5211
+ const sanitized = {};
5212
+ for (const [entryKey, entryValue] of Object.entries(value)) {
5213
+ sanitized[sanitizeAuditString(entryKey)] = sanitizeGovernanceAuditValue(entryValue, entryKey);
5214
+ }
5215
+ return sanitized;
5216
+ }
5217
+ return value;
5218
+ }
5219
+ function sanitizeAuditJson(value) {
5220
+ if (value === void 0)
5221
+ return void 0;
5222
+ return sanitizeGovernanceAuditValue(value);
5223
+ }
5224
+ function normalizeSourceEventIds(sourceEventIds) {
5225
+ return (sourceEventIds || []).map((sourceEventId) => sanitizeAuditString(sourceEventId.trim())).filter((sourceEventId) => sourceEventId.length > 0);
5226
+ }
5227
+ function normalizeOperation(operation) {
5228
+ if (MEMORY_GOVERNANCE_AUDIT_OPERATIONS.indexOf(operation) === -1) {
5229
+ throw new Error(`Unsupported governance audit operation: ${operation}`);
5230
+ }
5231
+ return operation;
5232
+ }
5233
+ async function writeGovernanceAuditEntry(db, input) {
5234
+ const entry = {
5235
+ auditId: randomUUID6(),
5236
+ operation: normalizeOperation(input.operation),
5237
+ actor: sanitizeAuditString(normalizeRequiredString(input.actor, "actor")),
5238
+ projectHash: normalizeOptionalString(input.projectHash),
5239
+ targetType: normalizeRequiredString(input.targetType, "targetType"),
5240
+ targetId: sanitizeAuditString(normalizeRequiredString(input.targetId, "targetId")),
5241
+ beforeJson: sanitizeAuditJson(input.beforeJson),
5242
+ afterJson: sanitizeAuditJson(input.afterJson),
5243
+ sourceEventIds: normalizeSourceEventIds(input.sourceEventIds),
5244
+ createdAt: /* @__PURE__ */ new Date()
5245
+ };
5246
+ sqliteRun(
5247
+ db,
5248
+ `INSERT INTO memory_governance_audit (
5249
+ audit_id, operation, actor, project_hash, target_type, target_id,
5250
+ before_json, after_json, source_event_ids, created_at
5251
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5252
+ [
5253
+ entry.auditId,
5254
+ entry.operation,
5255
+ entry.actor,
5256
+ entry.projectHash ?? null,
5257
+ entry.targetType,
5258
+ entry.targetId,
5259
+ entry.beforeJson === void 0 ? null : JSON.stringify(entry.beforeJson),
5260
+ entry.afterJson === void 0 ? null : JSON.stringify(entry.afterJson),
5261
+ JSON.stringify(entry.sourceEventIds),
5262
+ entry.createdAt.toISOString()
5263
+ ]
5264
+ );
5265
+ return entry;
5266
+ }
5267
+
5268
+ // src/core/operations/facet-repository.ts
5269
+ function parseStringArray(value) {
5270
+ if (typeof value !== "string")
5271
+ return [];
5272
+ try {
5273
+ const parsed = JSON.parse(value);
5274
+ if (!Array.isArray(parsed))
5275
+ return [];
5276
+ return parsed.filter((item) => typeof item === "string" && item.length > 0);
5277
+ } catch {
5278
+ return [];
5279
+ }
5280
+ }
5281
+ function projectHashToStorage(projectHash) {
5282
+ return projectHash ?? "";
5283
+ }
5284
+ function rowToFacet(row) {
5285
+ const projectHash = typeof row.project_hash === "string" && row.project_hash.length > 0 ? row.project_hash : void 0;
5286
+ return {
5287
+ id: row.id,
5288
+ targetType: FacetTargetTypeSchema.parse(row.target_type),
5289
+ targetId: row.target_id,
5290
+ dimension: row.dimension,
5291
+ value: row.value,
5292
+ confidence: Number(row.confidence),
5293
+ source: FacetSourceSchema.parse(row.source),
5294
+ evidenceEventIds: parseStringArray(row.evidence_event_ids),
5295
+ projectHash,
5296
+ createdAt: toDateFromSQLite(row.created_at),
5297
+ updatedAt: toDateFromSQLite(row.updated_at)
5298
+ };
5299
+ }
5300
+ function facetToAuditJson(facet) {
5301
+ return {
5302
+ id: facet.id,
5303
+ targetType: facet.targetType,
5304
+ targetId: facet.targetId,
5305
+ dimension: facet.dimension,
5306
+ value: facet.value,
5307
+ confidence: facet.confidence,
5308
+ source: facet.source,
5309
+ evidenceEventIds: facet.evidenceEventIds,
5310
+ projectHash: facet.projectHash,
5311
+ createdAt: facet.createdAt.toISOString(),
5312
+ updatedAt: facet.updatedAt.toISOString()
5313
+ };
5314
+ }
5315
+ var FacetRepository = class {
5316
+ constructor(db) {
5317
+ this.db = db;
5318
+ }
5319
+ async assign(input) {
5320
+ const assignment = parseFacetAssignmentInput(input);
5321
+ const existing = this.findByUniqueKey(assignment);
5322
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5323
+ if (existing) {
5324
+ sqliteRun(
5325
+ this.db,
5326
+ `UPDATE memory_facets
5327
+ SET confidence = ?, evidence_event_ids = ?, project_hash = ?, updated_at = ?
5328
+ WHERE id = ?`,
5329
+ [
5330
+ assignment.confidence,
5331
+ JSON.stringify(assignment.evidenceEventIds),
5332
+ projectHashToStorage(assignment.projectHash),
5333
+ now,
5334
+ existing.id
5335
+ ]
5336
+ );
5337
+ const saved2 = this.getById(existing.id);
5338
+ await this.auditAssignment(assignment, existing, saved2);
5339
+ return saved2;
5340
+ }
5341
+ const id = randomUUID7();
5342
+ sqliteRun(
5343
+ this.db,
5344
+ `INSERT INTO memory_facets (
5345
+ id, target_type, target_id, dimension, value, confidence, source,
5346
+ evidence_event_ids, project_hash, created_at, updated_at
5347
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5348
+ [
5349
+ id,
5350
+ assignment.targetType,
5351
+ assignment.targetId,
5352
+ assignment.dimension,
5353
+ assignment.value,
5354
+ assignment.confidence,
5355
+ assignment.source,
5356
+ JSON.stringify(assignment.evidenceEventIds),
5357
+ projectHashToStorage(assignment.projectHash),
5358
+ now,
5359
+ now
5360
+ ]
5361
+ );
5362
+ const saved = this.getById(id);
5363
+ await this.auditAssignment(assignment, null, saved);
5364
+ return saved;
5365
+ }
5366
+ async remove(input) {
5367
+ const removeInput = parseFacetRemoveInput(input);
5368
+ const { sql, params } = this.removeSql(removeInput);
5369
+ const result = sqliteRun(this.db, sql, params);
5370
+ return result.changes > 0;
5371
+ }
5372
+ async query(input) {
5373
+ const query = parseFacetQuery(input);
5374
+ const { sql, params } = this.querySql(query);
5375
+ const rows = sqliteAll(this.db, sql, params);
5376
+ return rows.map(rowToFacet);
5377
+ }
5378
+ async listForTarget(targetType, targetId) {
5379
+ const parsedTargetType = FacetTargetTypeSchema.parse(targetType);
5380
+ const trimmedTargetId = targetId.trim();
5381
+ if (!trimmedTargetId) {
5382
+ throw new Error("targetId is required");
5383
+ }
5384
+ return this.query({ targetType: parsedTargetType, targetId: trimmedTargetId, limit: 500 });
5385
+ }
5386
+ getById(id) {
5387
+ const row = sqliteGet(this.db, `SELECT * FROM memory_facets WHERE id = ?`, [id]);
5388
+ if (!row) {
5389
+ throw new Error(`Memory facet not found after write: ${id}`);
5390
+ }
5391
+ return rowToFacet(row);
5392
+ }
5393
+ findByUniqueKey(input) {
5394
+ const row = sqliteGet(
5395
+ this.db,
5396
+ `SELECT * FROM memory_facets
5397
+ WHERE target_type = ? AND target_id = ? AND dimension = ? AND value = ? AND source = ? AND project_hash = ?`,
5398
+ [
5399
+ input.targetType,
5400
+ input.targetId,
5401
+ input.dimension,
5402
+ input.value,
5403
+ input.source,
5404
+ projectHashToStorage(input.projectHash)
5405
+ ]
5406
+ );
5407
+ return row ? rowToFacet(row) : null;
5408
+ }
5409
+ async auditAssignment(input, before, after) {
5410
+ await writeGovernanceAuditEntry(this.db, {
5411
+ operation: "facet_tag",
5412
+ actor: input.actor ?? "cml-core",
5413
+ projectHash: input.projectHash,
5414
+ targetType: input.targetType,
5415
+ targetId: input.targetId,
5416
+ beforeJson: before ? facetToAuditJson(before) : void 0,
5417
+ afterJson: facetToAuditJson(after),
5418
+ sourceEventIds: input.evidenceEventIds
5419
+ });
5420
+ }
5421
+ querySql(query) {
5422
+ const clauses = [];
5423
+ const params = [];
5424
+ if (query.targetType) {
5425
+ clauses.push("target_type = ?");
5426
+ params.push(query.targetType);
5427
+ }
5428
+ if (query.targetId) {
5429
+ clauses.push("target_id = ?");
5430
+ params.push(query.targetId);
5431
+ }
5432
+ if (query.dimension) {
5433
+ clauses.push("dimension = ?");
5434
+ params.push(query.dimension);
5435
+ }
5436
+ if (query.value) {
5437
+ clauses.push("value = ?");
5438
+ params.push(query.value);
5439
+ }
5440
+ if (query.source) {
5441
+ clauses.push("source = ?");
5442
+ params.push(query.source);
5443
+ }
5444
+ if (query.projectHash) {
5445
+ clauses.push("project_hash = ?");
5446
+ params.push(query.projectHash);
5447
+ }
5448
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
5449
+ params.push(query.limit);
5450
+ return {
5451
+ sql: `SELECT * FROM memory_facets ${where} ORDER BY confidence DESC, updated_at DESC LIMIT ?`,
5452
+ params
5453
+ };
5454
+ }
5455
+ removeSql(input) {
5456
+ const clauses = [
5457
+ "target_type = ?",
5458
+ "target_id = ?",
5459
+ "dimension = ?",
5460
+ "value = ?",
5461
+ "source = ?",
5462
+ "project_hash = ?"
5463
+ ];
5464
+ const params = [
5465
+ input.targetType,
5466
+ input.targetId,
5467
+ input.dimension,
5468
+ input.value,
5469
+ input.source,
5470
+ projectHashToStorage(input.projectHash)
5471
+ ];
5472
+ return {
5473
+ sql: `DELETE FROM memory_facets WHERE ${clauses.join(" AND ")}`,
5474
+ params
5475
+ };
5476
+ }
5477
+ };
5478
+
5479
+ // src/core/operations/graph-path-service.ts
5480
+ var DEFAULT_WEIGHT = 0.5;
5481
+ var MAX_HOPS = 2;
5482
+ var DEFAULT_MAX_RESULTS = 20;
5483
+ var MAX_RESULTS = 100;
5484
+ var GraphPathService = class {
5485
+ constructor(db) {
5486
+ this.db = db;
5487
+ }
5488
+ expand(input) {
5489
+ const graph = this.loadGraph(input.direction ?? "both");
5490
+ const effectiveMaxHops = normalizeMaxHops(input.maxHops);
5491
+ const maxResults = normalizeMaxResults(input.maxResults);
5492
+ const startNodes = input.startNodes.map((node) => graph.node(node));
5493
+ const startKeys = new Set(input.startNodes.map(nodeKey));
5494
+ const bestByTarget = /* @__PURE__ */ new Map();
5495
+ const queue = startNodes.map((node) => ({
5496
+ key: nodeKey(node),
5497
+ hops: 0,
5498
+ totalCost: 0,
5499
+ steps: [],
5500
+ visited: /* @__PURE__ */ new Set([nodeKey(node)])
5501
+ }));
5502
+ while (queue.length > 0) {
5503
+ queue.sort((a, b) => a.totalCost - b.totalCost || a.hops - b.hops || a.key.localeCompare(b.key));
5504
+ const current = queue.shift();
5505
+ if (current.hops >= effectiveMaxHops)
5506
+ continue;
5507
+ for (const edge of graph.adjacency.get(current.key) ?? []) {
5508
+ if (current.visited.has(edge.toKey))
5509
+ continue;
5510
+ const nextHops = current.hops + 1;
5511
+ const nextTotalCost = current.totalCost + edge.step.cost;
5512
+ const nextSteps = [...current.steps, edge.step];
5513
+ const nextSignature = pathSignature(nextSteps);
5514
+ const existing = bestByTarget.get(edge.toKey);
5515
+ if (!existing || isBetterPath(nextTotalCost, nextHops, nextSignature, existing)) {
5516
+ if (!startKeys.has(edge.toKey)) {
5517
+ bestByTarget.set(edge.toKey, { hops: nextHops, totalCost: nextTotalCost, signature: nextSignature, steps: nextSteps });
5518
+ }
5519
+ const nextVisited = new Set(current.visited);
5520
+ nextVisited.add(edge.toKey);
5521
+ queue.push({
5522
+ key: edge.toKey,
5523
+ hops: nextHops,
5524
+ totalCost: nextTotalCost,
5525
+ steps: nextSteps,
5526
+ visited: nextVisited
5527
+ });
5528
+ }
5529
+ }
5530
+ }
5531
+ const paths = Array.from(bestByTarget.entries()).map(([key, path13]) => ({
5532
+ target: graph.node(nodeFromKey(key)),
5533
+ hops: path13.hops,
5534
+ totalCost: path13.totalCost,
5535
+ scoreContribution: path13.totalCost > 0 ? 1 / path13.totalCost : 0,
5536
+ steps: path13.steps
5537
+ })).sort((a, b) => b.scoreContribution - a.scoreContribution || a.hops - b.hops || a.target.name.localeCompare(b.target.name)).slice(0, maxResults);
5538
+ return { startNodes, effectiveMaxHops, paths };
5539
+ }
5540
+ loadGraph(direction) {
5541
+ const entityLabels = new Map(
5542
+ sqliteAll(this.db, `SELECT entity_id, title FROM entities WHERE status = 'active'`).map((row) => [row.entity_id, row.title])
5543
+ );
5544
+ const labelNode = (node) => ({
5545
+ ...node,
5546
+ name: node.type === "entity" ? entityLabels.get(node.id) ?? node.id : node.id
5547
+ });
5548
+ const adjacency = /* @__PURE__ */ new Map();
5549
+ const edges = sqliteAll(
5550
+ this.db,
5551
+ `SELECT edge_id, src_type, src_id, rel_type, dst_type, dst_id, meta_json FROM edges`
5552
+ );
5553
+ for (const edge of edges) {
5554
+ const src = labelNode({ type: edge.src_type, id: edge.src_id });
5555
+ const dst = labelNode({ type: edge.dst_type, id: edge.dst_id });
5556
+ const weight = edgeWeight(edge.meta_json);
5557
+ const cost = 1 / weight;
5558
+ const baseStep = {
5559
+ edgeId: edge.edge_id,
5560
+ relationType: edge.rel_type,
5561
+ from: src,
5562
+ to: dst,
5563
+ weight,
5564
+ cost,
5565
+ scoreContribution: weight
5566
+ };
5567
+ if (direction === "outgoing" || direction === "both") {
5568
+ addTraversal(adjacency, nodeKey(src), {
5569
+ toKey: nodeKey(dst),
5570
+ step: { ...baseStep, direction: "outgoing" }
5571
+ });
5572
+ }
5573
+ if (direction === "incoming" || direction === "both") {
5574
+ addTraversal(adjacency, nodeKey(dst), {
5575
+ toKey: nodeKey(src),
5576
+ step: { ...baseStep, direction: "incoming" }
5577
+ });
5578
+ }
5579
+ }
5580
+ return { adjacency, node: labelNode };
5581
+ }
5582
+ };
5583
+ function addTraversal(adjacency, fromKey, edge) {
5584
+ const edges = adjacency.get(fromKey) ?? [];
5585
+ edges.push(edge);
5586
+ adjacency.set(fromKey, edges);
5587
+ }
5588
+ function normalizeMaxHops(maxHops) {
5589
+ if (maxHops === void 0)
5590
+ return 1;
5591
+ if (!Number.isFinite(maxHops))
5592
+ return MAX_HOPS;
5593
+ return Math.min(Math.max(0, Math.trunc(maxHops)), MAX_HOPS);
5594
+ }
5595
+ function normalizeMaxResults(maxResults) {
5596
+ if (maxResults === void 0)
5597
+ return DEFAULT_MAX_RESULTS;
5598
+ if (!Number.isFinite(maxResults))
5599
+ return DEFAULT_MAX_RESULTS;
5600
+ return Math.min(Math.max(0, Math.trunc(maxResults)), MAX_RESULTS);
5601
+ }
5602
+ function isBetterPath(totalCost, hops, signature, existing) {
5603
+ return totalCost < existing.totalCost || totalCost === existing.totalCost && hops < existing.hops || totalCost === existing.totalCost && hops === existing.hops && signature < existing.signature;
5604
+ }
5605
+ function pathSignature(steps) {
5606
+ return steps.map((step) => `${step.edgeId}:${step.direction}:${nodeKey(step.from)}>${nodeKey(step.to)}`).join("|");
5607
+ }
5608
+ function edgeWeight(metaJson) {
5609
+ const meta = parseMeta(metaJson);
5610
+ const raw = meta.weight;
5611
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0)
5612
+ return raw;
5613
+ if (typeof raw === "string") {
5614
+ const parsed = Number(raw);
5615
+ if (Number.isFinite(parsed) && parsed > 0)
5616
+ return parsed;
5617
+ }
5618
+ return DEFAULT_WEIGHT;
5619
+ }
5620
+ function parseMeta(metaJson) {
5621
+ if (!metaJson)
5622
+ return {};
5623
+ try {
5624
+ const parsed = JSON.parse(metaJson);
5625
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
5626
+ } catch {
5627
+ return {};
5628
+ }
5629
+ }
5630
+ function nodeKey(node) {
5631
+ return `${node.type}:${node.id}`;
5632
+ }
5633
+ function nodeFromKey(key) {
5634
+ const index = key.indexOf(":");
5635
+ if (index === -1)
5636
+ return { type: "entity", id: key };
5637
+ return { type: key.slice(0, index), id: key.slice(index + 1) };
5638
+ }
5639
+
5640
+ // src/core/operations/query-entity-extractor.ts
5641
+ var DEFAULT_MAX_CANDIDATES = 20;
5642
+ var MAX_CANDIDATES = 100;
5643
+ var MAX_CANDIDATE_TEXT_LENGTH = 200;
5644
+ var SOURCE_PRIORITY = {
5645
+ entity_alias: 0,
5646
+ quoted: 1,
5647
+ file_path: 2,
5648
+ package_identifier: 3,
5649
+ capitalized_term: 4
5650
+ };
5651
+ var SOURCE_CONFIDENCE = {
5652
+ entity_alias: 0.95,
5653
+ quoted: 0.85,
5654
+ file_path: 0.8,
5655
+ package_identifier: 0.75,
5656
+ capitalized_term: 0.6
5657
+ };
5658
+ var SENTENCE_START_STOPWORDS = /* @__PURE__ */ new Set([
5659
+ "A",
5660
+ "An",
5661
+ "And",
5662
+ "Are",
5663
+ "Can",
5664
+ "Compare",
5665
+ "Does",
5666
+ "Explain",
5667
+ "Find",
5668
+ "How",
5669
+ "I",
5670
+ "If",
5671
+ "In",
5672
+ "Is",
5673
+ "List",
5674
+ "Please",
5675
+ "Should",
5676
+ "Show",
5677
+ "Tell",
5678
+ "The",
5679
+ "This",
5680
+ "Use",
5681
+ "What",
5682
+ "When",
5683
+ "Where",
5684
+ "Which",
5685
+ "Why",
5686
+ "With"
5687
+ ]);
5688
+ var QueryEntityExtractor = class {
5689
+ constructor(db) {
5690
+ this.db = db;
5691
+ }
5692
+ extract(query, options = {}) {
5693
+ const maxCandidates = normalizeMaxCandidates(options.maxCandidates);
5694
+ const candidates = [];
5695
+ const quotedRanges = this.extractQuoted(query, candidates);
5696
+ if (options.includeAliases !== false) {
5697
+ this.extractKnownAliases(query, candidates);
5698
+ }
5699
+ this.extractFilePaths(query, candidates);
5700
+ this.extractPackageIdentifiers(query, candidates);
5701
+ this.extractCapitalizedTerms(query, candidates, quotedRanges);
5702
+ return {
5703
+ query,
5704
+ candidates: dedupeAndSort(candidates).slice(0, maxCandidates).map(stripPriority)
5705
+ };
5706
+ }
5707
+ extractQuoted(query, candidates) {
5708
+ const ranges = [];
5709
+ const regex = /(["'`])((?:(?!\1)[^\n]){2,200})\1/g;
5710
+ let match;
5711
+ while ((match = regex.exec(query)) !== null) {
5712
+ const text = cleanCandidateText(match[2] ?? "");
5713
+ if (!isUsefulCandidate(text))
5714
+ continue;
5715
+ const start = match.index + 1;
5716
+ const end = start + text.length;
5717
+ ranges.push([match.index, match.index + match[0].length]);
5718
+ pushCandidate(candidates, {
5719
+ text,
5720
+ source: "quoted",
5721
+ start,
5722
+ end
5723
+ });
5724
+ }
5725
+ return ranges;
5726
+ }
5727
+ extractKnownAliases(query, candidates) {
5728
+ if (!this.db)
5729
+ return;
5730
+ const rows = sqliteAll(
5731
+ this.db,
5732
+ `SELECT
5733
+ a.entity_id,
5734
+ a.entity_type,
5735
+ a.canonical_key AS alias_key,
5736
+ e.canonical_key AS entity_canonical_key,
5737
+ e.title
5738
+ FROM entity_aliases a
5739
+ JOIN entities e ON e.entity_id = a.entity_id
5740
+ WHERE e.status = 'active'
5741
+ ORDER BY e.title COLLATE NOCASE, a.canonical_key COLLATE NOCASE`
5742
+ );
5743
+ const normalizedQuery = normalizeForContainment(query);
5744
+ const seenAliases = /* @__PURE__ */ new Set();
5745
+ for (const row of rows) {
5746
+ const aliasLabels = uniqueStrings([
5747
+ row.title,
5748
+ aliasLabelFromCanonicalKey(row.alias_key),
5749
+ aliasLabelFromCanonicalKey(row.entity_canonical_key)
5750
+ ]).filter(isUsefulCandidate);
5751
+ for (const alias of aliasLabels) {
5752
+ const normalizedAlias = normalizeForContainment(alias);
5753
+ if (!normalizedAlias || !containsPhrase(normalizedQuery, normalizedAlias))
5754
+ continue;
5755
+ const aliasKey = `${row.entity_id}:${normalizedAlias}`;
5756
+ if (seenAliases.has(aliasKey))
5757
+ continue;
5758
+ seenAliases.add(aliasKey);
5759
+ const range = findRange(query, alias);
5760
+ pushCandidate(candidates, {
5761
+ text: row.title,
5762
+ source: "entity_alias",
5763
+ start: range.start,
5764
+ end: range.end,
5765
+ entityId: row.entity_id,
5766
+ entityType: row.entity_type,
5767
+ canonicalKey: row.entity_canonical_key,
5768
+ matchedAlias: normalizedAlias
5769
+ });
5770
+ }
5771
+ }
5772
+ }
5773
+ extractFilePaths(query, candidates) {
5774
+ const regex = /(^|[\s([{<])((?:\.{1,2}\/|~\/|\/)?(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\.[A-Za-z0-9][A-Za-z0-9._-]*)(?=$|[\s)\]},>`.,;:!?])/g;
5775
+ let match;
5776
+ while ((match = regex.exec(query)) !== null) {
5777
+ const text = cleanCandidateText(match[2] ?? "");
5778
+ if (!isUsefulCandidate(text))
5779
+ continue;
5780
+ const start = match.index + (match[1]?.length ?? 0);
5781
+ pushCandidate(candidates, {
5782
+ text,
5783
+ source: "file_path",
5784
+ start,
5785
+ end: start + text.length
5786
+ });
5787
+ }
5788
+ }
5789
+ extractPackageIdentifiers(query, candidates) {
5790
+ const regex = /(^|[\s([{<`])(@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._]*[-_][a-z0-9._-]*)(?=$|[\s)\]},>`.,;:!?])/gi;
5791
+ let match;
5792
+ while ((match = regex.exec(query)) !== null) {
5793
+ const text = cleanCandidateText(match[2] ?? "");
5794
+ if (!isUsefulCandidate(text) || text.includes("/.") || text.includes("./"))
5795
+ continue;
5796
+ const start = match.index + (match[1]?.length ?? 0);
5797
+ pushCandidate(candidates, {
5798
+ text,
5799
+ source: "package_identifier",
5800
+ start,
5801
+ end: start + text.length
5802
+ });
5803
+ }
5804
+ }
5805
+ extractCapitalizedTerms(query, candidates, ignoredRanges) {
5806
+ const tokens = collectCapitalizedTokens(query).filter((token) => !isInsideAnyRange(token.start, ignoredRanges)).filter((token) => !SENTENCE_START_STOPWORDS.has(token.text));
5807
+ const groups = [];
5808
+ let current = [];
5809
+ for (const token of tokens) {
5810
+ const previous = current[current.length - 1];
5811
+ if (previous && query.slice(previous.end, token.start).match(/^\s+$/)) {
5812
+ current.push(token);
5813
+ } else {
5814
+ if (current.length > 0)
5815
+ groups.push(current);
5816
+ current = [token];
5817
+ }
5818
+ }
5819
+ if (current.length > 0)
5820
+ groups.push(current);
5821
+ for (const group of groups) {
5822
+ if (group.length === 1 && !isStrongSingleCapitalized(group[0].text))
5823
+ continue;
5824
+ const start = group[0].start;
5825
+ const end = group[group.length - 1].end;
5826
+ const text = query.slice(start, end);
5827
+ if (!isUsefulCandidate(text))
5828
+ continue;
5829
+ pushCandidate(candidates, {
5830
+ text,
5831
+ source: "capitalized_term",
5832
+ start,
5833
+ end
5834
+ });
5835
+ }
5836
+ }
5837
+ };
5838
+ function collectCapitalizedTokens(query) {
5839
+ const regex = /\b(?:[A-Z]{2,}[A-Z0-9]*|[A-Z][A-Za-z0-9]*(?:[._-][A-Za-z0-9]+)*)\b/g;
5840
+ const tokens = [];
5841
+ let match;
5842
+ while ((match = regex.exec(query)) !== null) {
5843
+ tokens.push({ text: match[0], start: match.index, end: match.index + match[0].length });
5844
+ }
5845
+ return tokens;
5846
+ }
5847
+ function pushCandidate(candidates, input) {
5848
+ const text = cleanCandidateText(input.text);
5849
+ if (!isUsefulCandidate(text))
5850
+ return;
5851
+ const source = input.source;
5852
+ candidates.push({
5853
+ ...input,
5854
+ text,
5855
+ normalized: normalizeCandidate(text),
5856
+ confidence: input.confidence ?? SOURCE_CONFIDENCE[source],
5857
+ priority: SOURCE_PRIORITY[source]
5858
+ });
5859
+ }
5860
+ function dedupeAndSort(candidates) {
5861
+ const sorted = [...candidates].sort(compareCandidates);
5862
+ const seenAliasKeys = /* @__PURE__ */ new Set();
5863
+ const seenNormalized = /* @__PURE__ */ new Set();
5864
+ const result = [];
5865
+ for (const candidate of sorted) {
5866
+ if (candidate.source === "entity_alias") {
5867
+ const aliasKey = `alias:${candidate.entityId ?? ""}:${normalizeCandidate(candidate.matchedAlias ?? candidate.text)}`;
5868
+ if (seenAliasKeys.has(aliasKey))
5869
+ continue;
5870
+ seenAliasKeys.add(aliasKey);
5871
+ seenNormalized.add(candidate.normalized);
5872
+ result.push(candidate);
5873
+ continue;
5874
+ }
5875
+ if (seenNormalized.has(candidate.normalized))
5876
+ continue;
5877
+ seenNormalized.add(candidate.normalized);
5878
+ result.push(candidate);
5879
+ }
5880
+ return result.sort(compareCandidates);
5881
+ }
5882
+ function compareCandidates(a, b) {
5883
+ return a.priority - b.priority || a.start - b.start || a.end - b.end || compareStrings(a.text, b.text) || compareStrings(a.entityId ?? "", b.entityId ?? "") || compareStrings(a.matchedAlias ?? "", b.matchedAlias ?? "");
5884
+ }
5885
+ function compareStrings(a, b) {
5886
+ if (a === b)
5887
+ return 0;
5888
+ return a < b ? -1 : 1;
5889
+ }
5890
+ function stripPriority(candidate) {
5891
+ const { priority: _priority, ...publicCandidate } = candidate;
5892
+ return publicCandidate;
5893
+ }
5894
+ function normalizeMaxCandidates(maxCandidates) {
5895
+ if (maxCandidates === void 0)
5896
+ return DEFAULT_MAX_CANDIDATES;
5897
+ if (!Number.isFinite(maxCandidates))
5898
+ return DEFAULT_MAX_CANDIDATES;
5899
+ return Math.min(Math.max(0, Math.trunc(maxCandidates)), MAX_CANDIDATES);
5900
+ }
5901
+ function cleanCandidateText(text) {
5902
+ return text.normalize("NFKC").replace(/\s+/g, " ").trim().replace(/[.,;:!?]+$/g, "");
5903
+ }
5904
+ function normalizeCandidate(text) {
5905
+ return cleanCandidateText(text).toLowerCase();
5906
+ }
5907
+ function normalizeForContainment(text) {
5908
+ return text.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}@/._-]+/gu, " ").replace(/[._-]+/g, " ").replace(/\s+/g, " ").trim();
5909
+ }
5910
+ function containsPhrase(normalizedHaystack, normalizedNeedle) {
5911
+ return ` ${normalizedHaystack} `.includes(` ${normalizedNeedle} `);
5912
+ }
5913
+ function aliasLabelFromCanonicalKey(canonicalKey) {
5914
+ const raw = canonicalKey.includes(":") ? canonicalKey.slice(canonicalKey.lastIndexOf(":") + 1) : canonicalKey;
5915
+ return raw.replace(/[._-]+/g, " ").replace(/\s+/g, " ").trim();
5916
+ }
5917
+ function findRange(query, alias) {
5918
+ const normalizedAlias = normalizeForContainment(alias);
5919
+ const directIndex = query.toLowerCase().indexOf(alias.toLowerCase());
5920
+ if (directIndex >= 0)
5921
+ return { start: directIndex, end: directIndex + alias.length };
5922
+ const normalizedQuery = normalizeForContainment(query);
5923
+ const normalizedIndex = normalizedQuery.indexOf(normalizedAlias);
5924
+ if (normalizedIndex < 0)
5925
+ return { start: 0, end: 0 };
5926
+ const queryLower = query.toLowerCase();
5927
+ const words = normalizedAlias.split(" ").filter(Boolean);
5928
+ if (words.length === 0)
5929
+ return { start: 0, end: 0 };
5930
+ const first = queryLower.indexOf(words[0]);
5931
+ const lastWord = words[words.length - 1];
5932
+ const last = queryLower.indexOf(lastWord, first >= 0 ? first : 0);
5933
+ if (first >= 0 && last >= 0)
5934
+ return { start: first, end: last + lastWord.length };
5935
+ return { start: normalizedIndex, end: normalizedIndex + normalizedAlias.length };
5936
+ }
5937
+ function isUsefulCandidate(text) {
5938
+ const cleaned = cleanCandidateText(text);
5939
+ return cleaned.length >= 2 && cleaned.length <= MAX_CANDIDATE_TEXT_LENGTH && /[\p{L}\p{N}]/u.test(cleaned);
5940
+ }
5941
+ function isInsideAnyRange(index, ranges) {
5942
+ return ranges.some(([start, end]) => index >= start && index < end);
5943
+ }
5944
+ function isStrongSingleCapitalized(text) {
5945
+ if (/^[A-Z]{2,}[A-Z0-9]*$/.test(text))
5946
+ return true;
5947
+ if (/^[A-Z][a-z]+[A-Z][A-Za-z0-9]*$/.test(text))
5948
+ return true;
5949
+ return text.length >= 4 && !SENTENCE_START_STOPWORDS.has(text);
5950
+ }
5951
+ function uniqueStrings(values) {
5952
+ const seen = /* @__PURE__ */ new Set();
5953
+ const result = [];
5954
+ for (const value of values) {
5955
+ const key = normalizeForContainment(value);
5956
+ if (!key || seen.has(key))
5957
+ continue;
5958
+ seen.add(key);
5959
+ result.push(value);
5960
+ }
5961
+ return result;
5962
+ }
5963
+
4894
5964
  // src/core/retrieval-quality.ts
4895
5965
  var COMMAND_ARTIFACT_PATTERNS = [
4896
5966
  /<\/?(?:local-command-(?:stdout|stderr)|command-(?:name|message))\b/i,
@@ -5253,6 +6323,7 @@ var Retriever = class {
5253
6323
  sharedVectorStore;
5254
6324
  graduation;
5255
6325
  queryRewriter;
6326
+ queryGraphExpansionEnabled;
5256
6327
  constructor(eventStore, vectorStore, embedder, matcher, sharedOptions) {
5257
6328
  this.eventStore = eventStore;
5258
6329
  this.vectorStore = vectorStore;
@@ -5260,6 +6331,7 @@ var Retriever = class {
5260
6331
  this.matcher = matcher;
5261
6332
  this.sharedStore = sharedOptions?.sharedStore;
5262
6333
  this.sharedVectorStore = sharedOptions?.sharedVectorStore;
6334
+ this.queryGraphExpansionEnabled = sharedOptions?.queryGraphExpansionEnabled === true;
5263
6335
  }
5264
6336
  setGraduationPipeline(graduation) {
5265
6337
  this.graduation = graduation;
@@ -5305,7 +6377,8 @@ var Retriever = class {
5305
6377
  graphHop: opts.graphHop,
5306
6378
  projectScopeMode: opts.projectScopeMode,
5307
6379
  projectHash: opts.projectHash,
5308
- allowedProjectHashes: opts.allowedProjectHashes
6380
+ allowedProjectHashes: opts.allowedProjectHashes,
6381
+ facets: opts.facets
5309
6382
  });
5310
6383
  fallbackTrace.push(`stage:primary:${primaryStrategy}`);
5311
6384
  if (fallbackEnabled && this.shouldFallback(current.matchResult, current.results) && primaryStrategy !== "deep") {
@@ -5322,7 +6395,8 @@ var Retriever = class {
5322
6395
  graphHop: opts.graphHop,
5323
6396
  projectScopeMode: opts.projectScopeMode,
5324
6397
  projectHash: opts.projectHash,
5325
- allowedProjectHashes: opts.allowedProjectHashes
6398
+ allowedProjectHashes: opts.allowedProjectHashes,
6399
+ facets: opts.facets
5326
6400
  });
5327
6401
  fallbackTrace.push("fallback:deep");
5328
6402
  }
@@ -5340,7 +6414,8 @@ var Retriever = class {
5340
6414
  graphHop: opts.graphHop,
5341
6415
  projectScopeMode: opts.projectScopeMode,
5342
6416
  projectHash: opts.projectHash,
5343
- allowedProjectHashes: opts.allowedProjectHashes
6417
+ allowedProjectHashes: opts.allowedProjectHashes,
6418
+ facets: opts.facets
5344
6419
  });
5345
6420
  fallbackTrace.push("fallback:scope-expanded");
5346
6421
  }
@@ -5350,7 +6425,8 @@ var Retriever = class {
5350
6425
  scope: opts.scope,
5351
6426
  projectScopeMode: opts.projectScopeMode,
5352
6427
  projectHash: opts.projectHash,
5353
- allowedProjectHashes: opts.allowedProjectHashes
6428
+ allowedProjectHashes: opts.allowedProjectHashes,
6429
+ facets: opts.facets
5354
6430
  });
5355
6431
  const filteredSummary = this.applyQualityFilters(scopedSummary, {
5356
6432
  query,
@@ -5371,20 +6447,8 @@ var Retriever = class {
5371
6447
  totalTokens: this.estimateTokens(context),
5372
6448
  context,
5373
6449
  fallbackTrace,
5374
- selectedDebug: current.results.slice(0, opts.topK).map((r) => ({
5375
- eventId: r.eventId,
5376
- score: r.score,
5377
- semanticScore: r.semanticScore,
5378
- lexicalScore: r.lexicalScore,
5379
- recencyScore: r.recencyScore
5380
- })),
5381
- candidateDebug: (current.candidateResults || []).slice(0, Math.max(opts.topK * 3, 20)).map((r) => ({
5382
- eventId: r.eventId,
5383
- score: r.score,
5384
- semanticScore: r.semanticScore,
5385
- lexicalScore: r.lexicalScore,
5386
- recencyScore: r.recencyScore
5387
- })),
6450
+ selectedDebug: current.results.slice(0, opts.topK).map((r) => this.debugDetailForResult(r)),
6451
+ candidateDebug: (current.candidateResults || []).slice(0, Math.max(opts.topK * 3, 20)).map((r) => this.debugDetailForResult(r)),
5388
6452
  rawQueryText: current.queryRewriteKind ? query : void 0,
5389
6453
  effectiveQueryText: current.effectiveQueryText,
5390
6454
  queryRewriteKind: current.queryRewriteKind
@@ -5452,7 +6516,9 @@ var Retriever = class {
5452
6516
  }
5453
6517
  }
5454
6518
  const expandedResults = input.graphHop?.enabled === false ? initialResults : await this.expandGraphHops(initialResults, {
5455
- maxHops: Math.max(1, input.graphHop?.maxHops ?? 1),
6519
+ query,
6520
+ queryGraphEnabled: this.queryGraphExpansionEnabled,
6521
+ maxHops: clampGraphHops(input.graphHop?.maxHops ?? 1),
5456
6522
  hopPenalty: Math.max(0, input.graphHop?.hopPenalty ?? 0.08),
5457
6523
  limit: input.topK * 4
5458
6524
  });
@@ -5461,7 +6527,8 @@ var Retriever = class {
5461
6527
  scope: input.scope,
5462
6528
  projectScopeMode: input.projectScopeMode,
5463
6529
  projectHash: input.projectHash,
5464
- allowedProjectHashes: input.allowedProjectHashes
6530
+ allowedProjectHashes: input.allowedProjectHashes,
6531
+ facets: input.facets
5465
6532
  });
5466
6533
  const qualityFiltered = this.applyQualityFilters(filtered, {
5467
6534
  query,
@@ -5476,9 +6543,13 @@ var Retriever = class {
5476
6543
  if (isCurrentStateQuery(options.query)) {
5477
6544
  filtered = filtered.filter((result) => !isStaleOrSupersededContent(result.content));
5478
6545
  }
5479
- filtered = filtered.filter((result) => hasDiscriminativeTermOverlap(options.query, result.content));
6546
+ filtered = filtered.filter(
6547
+ (result) => this.isGraphPathResult(result) || hasDiscriminativeTermOverlap(options.query, result.content)
6548
+ );
5480
6549
  if (shouldApplyTechnicalGuard(options.query)) {
5481
- filtered = filtered.filter((result) => hasTechnicalTermOverlap(options.query, result.content));
6550
+ filtered = filtered.filter(
6551
+ (result) => this.isGraphPathResult(result) || hasTechnicalTermOverlap(options.query, result.content)
6552
+ );
5482
6553
  }
5483
6554
  if (filtered.length <= 2)
5484
6555
  return filtered;
@@ -5541,7 +6612,61 @@ var Retriever = class {
5541
6612
  if (frontier.length === 0 || byId.size >= opts.limit)
5542
6613
  break;
5543
6614
  }
5544
- return [...byId.values()].sort((a, b) => b.score - a.score).slice(0, opts.limit);
6615
+ if (opts.queryGraphEnabled) {
6616
+ await this.expandQueryGraphPaths(opts.query, byId, opts);
6617
+ }
6618
+ return [...byId.values()].sort((a, b) => b.score - a.score || compareStable(a.eventId, b.eventId)).slice(0, opts.limit);
6619
+ }
6620
+ async expandQueryGraphPaths(query, byId, opts) {
6621
+ if (!query.trim() || !this.eventStore.getDatabase)
6622
+ return;
6623
+ try {
6624
+ const db = this.eventStore.getDatabase();
6625
+ const extraction = new QueryEntityExtractor(db).extract(query, {
6626
+ maxCandidates: Math.min(8, opts.limit),
6627
+ includeAliases: true
6628
+ });
6629
+ const startCandidates = extraction.candidates.filter((candidate) => candidate.entityId).slice(0, 8);
6630
+ const startNodes = uniqueEntityStartNodes(startCandidates);
6631
+ if (startNodes.length === 0)
6632
+ return;
6633
+ const expansion = new GraphPathService(db).expand({
6634
+ startNodes: startNodes.map((node) => ({ type: "entity", id: node.entityId })),
6635
+ maxHops: opts.maxHops,
6636
+ maxResults: opts.limit,
6637
+ direction: "both"
6638
+ });
6639
+ const titleByEntityId = new Map(startNodes.map((node) => [node.entityId, node.title]));
6640
+ for (const path13 of expansion.paths) {
6641
+ if (path13.target.type !== "event")
6642
+ continue;
6643
+ const target = await this.eventStore.getEvent(path13.target.id);
6644
+ if (!target)
6645
+ continue;
6646
+ const graphPath = toRetrievalGraphPathDebug(path13, titleByEntityId);
6647
+ const score = graphPathScore(path13, opts.hopPenalty);
6648
+ const existing = byId.get(target.id);
6649
+ const graphPaths = mergeGraphPaths(existing?.graphPaths ?? [], [graphPath]);
6650
+ const row = {
6651
+ id: existing?.id ?? `graph-path-${path13.hops}-${target.id}`,
6652
+ eventId: target.id,
6653
+ content: target.content,
6654
+ score: Math.max(existing?.score ?? 0, score),
6655
+ sessionId: target.sessionId,
6656
+ eventType: target.eventType,
6657
+ timestamp: target.timestamp.toISOString(),
6658
+ semanticScore: existing?.semanticScore,
6659
+ lexicalScore: existing?.lexicalScore,
6660
+ recencyScore: existing?.recencyScore,
6661
+ facetMatches: existing?.facetMatches,
6662
+ graphPaths
6663
+ };
6664
+ byId.set(row.eventId, row);
6665
+ if (byId.size >= opts.limit)
6666
+ break;
6667
+ }
6668
+ } catch {
6669
+ }
5545
6670
  }
5546
6671
  shouldFallback(matchResult, results) {
5547
6672
  if (results.length === 0)
@@ -5632,12 +6757,13 @@ var Retriever = class {
5632
6757
  async applyScopeFilters(results, options) {
5633
6758
  const scope = options?.scope;
5634
6759
  const projectScopeMode = options?.projectScopeMode ?? "global";
6760
+ const facetFilters = this.normalizeFacetFilters(options?.facets);
5635
6761
  const allowedProjectHashes = new Set(
5636
6762
  [options?.projectHash, ...options?.allowedProjectHashes || []].filter(
5637
6763
  (value) => typeof value === "string" && value.length > 0
5638
6764
  )
5639
6765
  );
5640
- if (!scope && projectScopeMode === "global")
6766
+ if (!scope && projectScopeMode === "global" && facetFilters === null)
5641
6767
  return results;
5642
6768
  const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
5643
6769
  const filtered = [];
@@ -5663,14 +6789,83 @@ var Retriever = class {
5663
6789
  const projectHash = this.extractProjectHash(event.metadata);
5664
6790
  filtered.push({ result, projectHash });
5665
6791
  }
6792
+ let scopedResults;
5666
6793
  if (projectScopeMode === "global" || allowedProjectHashes.size === 0) {
5667
- return filtered.map((x) => x.result);
6794
+ scopedResults = filtered.map((x) => x.result);
6795
+ } else {
6796
+ const projectMatched = filtered.filter((x) => x.projectHash && allowedProjectHashes.has(x.projectHash));
6797
+ scopedResults = projectScopeMode === "strict" ? projectMatched.map((x) => x.result) : (projectMatched.length > 0 ? projectMatched : filtered).map((x) => x.result);
6798
+ }
6799
+ return this.applyFacetFilters(scopedResults, {
6800
+ facets: facetFilters,
6801
+ projectHash: options?.projectHash
6802
+ });
6803
+ }
6804
+ normalizeFacetFilters(facets) {
6805
+ if (!facets || facets.length === 0)
6806
+ return null;
6807
+ const normalized = [];
6808
+ for (const facet of facets) {
6809
+ const parsedDimension = FacetDimensionSchema.safeParse(facet.dimension);
6810
+ const value = typeof facet.value === "string" ? facet.value.trim() : "";
6811
+ if (!parsedDimension.success || !value)
6812
+ return [];
6813
+ normalized.push({ dimension: parsedDimension.data, value });
6814
+ }
6815
+ return normalized;
6816
+ }
6817
+ async applyFacetFilters(results, options) {
6818
+ if (options.facets === null)
6819
+ return results;
6820
+ if (options.facets.length === 0)
6821
+ return [];
6822
+ if (!options.projectHash)
6823
+ return [];
6824
+ if (!this.eventStore.getDatabase)
6825
+ return [];
6826
+ const repo = new FacetRepository(this.eventStore.getDatabase());
6827
+ const filtered = [];
6828
+ for (const result of results) {
6829
+ const matches = [];
6830
+ let matchedAll = true;
6831
+ for (const facet of options.facets) {
6832
+ const rows = await repo.query({
6833
+ targetType: "event",
6834
+ targetId: result.eventId,
6835
+ dimension: facet.dimension,
6836
+ value: facet.value,
6837
+ projectHash: options.projectHash
6838
+ });
6839
+ if (rows.length === 0) {
6840
+ matchedAll = false;
6841
+ break;
6842
+ }
6843
+ matches.push(facet);
6844
+ }
6845
+ if (matchedAll) {
6846
+ filtered.push({ ...result, facetMatches: matches });
6847
+ }
6848
+ }
6849
+ return filtered;
6850
+ }
6851
+ debugDetailForResult(result) {
6852
+ const detail = {
6853
+ eventId: result.eventId,
6854
+ score: result.score,
6855
+ semanticScore: result.semanticScore,
6856
+ lexicalScore: result.lexicalScore,
6857
+ recencyScore: result.recencyScore
6858
+ };
6859
+ if (result.facetMatches && result.facetMatches.length > 0) {
6860
+ detail.facetMatches = result.facetMatches;
5668
6861
  }
5669
- const projectMatched = filtered.filter((x) => x.projectHash && allowedProjectHashes.has(x.projectHash));
5670
- if (projectScopeMode === "strict") {
5671
- return projectMatched.map((x) => x.result);
6862
+ if (result.graphPaths && result.graphPaths.length > 0) {
6863
+ detail.graphPaths = result.graphPaths;
5672
6864
  }
5673
- return (projectMatched.length > 0 ? projectMatched : filtered).map((x) => x.result);
6865
+ return detail;
6866
+ }
6867
+ isGraphPathResult(result) {
6868
+ return (result.graphPaths || []).length > 0;
5674
6869
  }
5675
6870
  extractProjectHash(metadata) {
5676
6871
  if (!metadata || typeof metadata !== "object")
@@ -5814,8 +7009,60 @@ _Context:_ ${sessionContext}`;
5814
7009
  return Math.ceil(text.length / 4);
5815
7010
  }
5816
7011
  };
5817
- function createRetriever(eventStore, vectorStore, embedder, matcher) {
5818
- return new Retriever(eventStore, vectorStore, embedder, matcher);
7012
+ function uniqueEntityStartNodes(candidates) {
7013
+ const seen = /* @__PURE__ */ new Set();
7014
+ const nodes = [];
7015
+ for (const candidate of candidates) {
7016
+ if (!candidate.entityId || seen.has(candidate.entityId))
7017
+ continue;
7018
+ seen.add(candidate.entityId);
7019
+ nodes.push({ entityId: candidate.entityId, title: candidate.text });
7020
+ }
7021
+ return nodes;
7022
+ }
7023
+ function toRetrievalGraphPathDebug(path13, titleByEntityId) {
7024
+ const firstStep = path13.steps[0];
7025
+ const startNode = firstStep?.direction === "incoming" ? firstStep.to : firstStep?.from;
7026
+ const startEntityId = startNode?.type === "entity" ? startNode.id : "";
7027
+ return {
7028
+ startEntityId,
7029
+ startEntityTitle: titleByEntityId.get(startEntityId) ?? startNode?.name,
7030
+ targetId: path13.target.id,
7031
+ targetType: path13.target.type,
7032
+ hops: path13.hops,
7033
+ relationPath: path13.steps.map((step) => step.relationType)
7034
+ };
7035
+ }
7036
+ function graphPathScore(path13, hopPenalty) {
7037
+ const base = Math.min(0.95, Math.max(0, path13.scoreContribution));
7038
+ return Math.max(0.05, base - hopPenalty * Math.max(0, path13.hops - 1));
7039
+ }
7040
+ function clampGraphHops(maxHops) {
7041
+ if (!Number.isFinite(maxHops))
7042
+ return 2;
7043
+ return Math.min(Math.max(0, Math.trunc(maxHops)), 2);
7044
+ }
7045
+ function mergeGraphPaths(existing, incoming) {
7046
+ const byKey = /* @__PURE__ */ new Map();
7047
+ for (const path13 of [...existing, ...incoming]) {
7048
+ const key = [path13.startEntityId, path13.targetType, path13.targetId, path13.hops, ...path13.relationPath].join("\0");
7049
+ if (!byKey.has(key))
7050
+ byKey.set(key, path13);
7051
+ }
7052
+ return [...byKey.values()].sort((a, b) => a.hops - b.hops || compareStable(graphPathSignature(a), graphPathSignature(b))).slice(0, 3);
7053
+ }
7054
+ function graphPathSignature(path13) {
7055
+ return [path13.startEntityId, path13.targetType, path13.targetId, path13.hops, ...path13.relationPath].join("|");
7056
+ }
7057
+ function compareStable(a, b) {
7058
+ if (a < b)
7059
+ return -1;
7060
+ if (a > b)
7061
+ return 1;
7062
+ return 0;
7063
+ }
7064
+ function createRetriever(eventStore, vectorStore, embedder, matcher, sharedOptions) {
7065
+ return new Retriever(eventStore, vectorStore, embedder, matcher, sharedOptions);
5819
7066
  }
5820
7067
 
5821
7068
  // src/core/engine/retrieval-analytics-service.ts
@@ -6001,10 +7248,15 @@ var RetrievalDisclosureService = class {
6001
7248
  {
6002
7249
  semanticScore: debug?.semanticScore,
6003
7250
  lexicalScore: debug?.lexicalScore,
6004
- recencyScore: debug?.recencyScore
7251
+ recencyScore: debug?.recencyScore,
7252
+ ...debug?.facetMatches && debug.facetMatches.length > 0 ? { facetMatches: debug.facetMatches } : {},
7253
+ ...debug?.graphPaths && debug.graphPaths.length > 0 ? { graphPaths: this.sanitizeGraphPaths(debug.graphPaths) } : {}
6005
7254
  }
6006
7255
  );
6007
7256
  }
7257
+ sanitizeGraphPaths(graphPaths) {
7258
+ return sanitizeGovernanceAuditValue(graphPaths ?? []);
7259
+ }
6008
7260
  eventToEnvelope(event, score, reasons, extraMetadata) {
6009
7261
  return {
6010
7262
  id: toDisclosureResultId(event.id),
@@ -6064,6 +7316,10 @@ var RetrievalDisclosureService = class {
6064
7316
  reasons.add("keyword_match");
6065
7317
  if ((debug?.recencyScore ?? 0) > 0)
6066
7318
  reasons.add("recent_relevance");
7319
+ if ((debug?.facetMatches || []).length > 0)
7320
+ reasons.add("facet_match");
7321
+ if ((debug?.graphPaths || []).length > 0)
7322
+ reasons.add("entity_overlap");
6067
7323
  if ((result.fallbackTrace || []).some((step) => step === "fallback:summary"))
6068
7324
  reasons.add("summary_fallback");
6069
7325
  if (memory.sessionContext)
@@ -6191,6 +7447,7 @@ var RetrievalOrchestrator = class {
6191
7447
  const rerankWeights = lightweightFastRead ? void 0 : await this.getRerankWeights(options?.adaptiveRerank === true);
6192
7448
  const projectHash = this.deps.getProjectHash();
6193
7449
  const projectScopeMode = retrieverOptions.projectScopeMode ?? (projectHash ? "strict" : "global");
7450
+ const graphHop = this.resolveGraphHopOptions(retrieverOptions.graphHop);
6194
7451
  let result;
6195
7452
  if (retrieverOptions.includeShared && this.deps.hasSharedStore()) {
6196
7453
  result = await this.deps.retriever.retrieveUnified(query, {
@@ -6198,6 +7455,7 @@ var RetrievalOrchestrator = class {
6198
7455
  intentRewrite: retrieverOptions.intentRewrite === true,
6199
7456
  rerankWeights,
6200
7457
  includeShared: true,
7458
+ graphHop,
6201
7459
  projectHash: projectHash || void 0,
6202
7460
  projectScopeMode,
6203
7461
  allowedProjectHashes: retrieverOptions.allowedProjectHashes
@@ -6207,6 +7465,7 @@ var RetrievalOrchestrator = class {
6207
7465
  ...retrieverOptions,
6208
7466
  intentRewrite: retrieverOptions.intentRewrite === true,
6209
7467
  rerankWeights,
7468
+ graphHop,
6210
7469
  projectHash: projectHash || void 0,
6211
7470
  projectScopeMode,
6212
7471
  allowedProjectHashes: retrieverOptions.allowedProjectHashes
@@ -6268,6 +7527,29 @@ var RetrievalOrchestrator = class {
6268
7527
  await this.deps.initialize();
6269
7528
  await this.deps.accessStore.recordRetrieval(eventId, sessionId, score, query);
6270
7529
  }
7530
+ resolveGraphHopOptions(callerOptions) {
7531
+ const graphExpansion = this.deps.memoryOperationsConfig?.graphExpansion;
7532
+ const durableOptions = graphExpansion?.enabled === true ? { enabled: true, maxHops: graphExpansion.maxHops } : void 0;
7533
+ if (!callerOptions)
7534
+ return durableOptions;
7535
+ if (!graphExpansion)
7536
+ return callerOptions;
7537
+ if (graphExpansion.enabled !== true) {
7538
+ return {
7539
+ ...callerOptions,
7540
+ enabled: false,
7541
+ maxHops: graphExpansion.maxHops ?? callerOptions.maxHops
7542
+ };
7543
+ }
7544
+ return {
7545
+ enabled: callerOptions.enabled === false ? false : true,
7546
+ maxHops: Math.min(
7547
+ graphExpansion.maxHops ?? Number.POSITIVE_INFINITY,
7548
+ callerOptions.maxHops ?? graphExpansion.maxHops ?? 1
7549
+ ),
7550
+ hopPenalty: callerOptions.hopPenalty
7551
+ };
7552
+ }
6271
7553
  async recordAutomaticTrace(query, result, options, projectHash) {
6272
7554
  const selectedEventIds = result.memories.map((memory) => memory.event.id);
6273
7555
  const selectedDetails = (result.selectedDebug || []).map((detail) => ({
@@ -6409,7 +7691,8 @@ function createRetrievalServices(deps) {
6409
7691
  deps.eventStore,
6410
7692
  deps.vectorStore,
6411
7693
  deps.embedder,
6412
- deps.matcher
7694
+ deps.matcher,
7695
+ { queryGraphExpansionEnabled: deps.memoryOperationsConfig?.graphExpansion?.enabled === true }
6413
7696
  );
6414
7697
  const retrievalOrchestrator = createRetrievalOrchestrator({
6415
7698
  initialize: deps.initialize,
@@ -6417,7 +7700,8 @@ function createRetrievalServices(deps) {
6417
7700
  traceStore: deps.eventStore,
6418
7701
  accessStore: deps.eventStore,
6419
7702
  getProjectHash: deps.getProjectHash,
6420
- hasSharedStore: deps.hasSharedStore
7703
+ hasSharedStore: deps.hasSharedStore,
7704
+ memoryOperationsConfig: deps.memoryOperationsConfig
6421
7705
  });
6422
7706
  const retrievalDisclosureService = createRetrievalDisclosureService({
6423
7707
  initialize: deps.initialize,
@@ -6436,13 +7720,14 @@ function createRetrievalServices(deps) {
6436
7720
  retrievalAnalyticsService
6437
7721
  };
6438
7722
  }
6439
- function defaultCreateRetriever(eventStore, vectorStore, embedder, matcher) {
7723
+ function defaultCreateRetriever(eventStore, vectorStore, embedder, matcher, options) {
6440
7724
  assertDefaultRetrieverStore(eventStore);
6441
7725
  return createRetriever(
6442
7726
  eventStore,
6443
7727
  vectorStore,
6444
7728
  embedder,
6445
- matcher
7729
+ matcher,
7730
+ options
6446
7731
  );
6447
7732
  }
6448
7733
  function assertDefaultRetrieverStore(eventStore) {
@@ -6488,6 +7773,7 @@ function createMemoryEngineServices(options) {
6488
7773
  matcher,
6489
7774
  getProjectHash: options.getProjectHash,
6490
7775
  hasSharedStore: options.hasSharedStore,
7776
+ memoryOperationsConfig: options.memoryOperationsConfig,
6491
7777
  sharedStore: options.sharedStore
6492
7778
  });
6493
7779
  const ingestService = new MemoryIngestService({
@@ -6974,7 +8260,7 @@ function createSharedEventStore(dbPath) {
6974
8260
  }
6975
8261
 
6976
8262
  // src/core/shared-promoter.ts
6977
- import { randomUUID as randomUUID6 } from "crypto";
8263
+ import { randomUUID as randomUUID8 } from "crypto";
6978
8264
  var SharedPromoter = class {
6979
8265
  constructor(sharedStore, sharedVectorStore, embedder, config) {
6980
8266
  this.sharedStore = sharedStore;
@@ -7042,7 +8328,7 @@ var SharedPromoter = class {
7042
8328
  const embeddingContent = this.createEmbeddingContent(input);
7043
8329
  const embedding = await this.embedder.embed(embeddingContent);
7044
8330
  await this.sharedVectorStore.upsert({
7045
- id: randomUUID6(),
8331
+ id: randomUUID8(),
7046
8332
  entryId,
7047
8333
  entryType: "troubleshooting",
7048
8334
  content: embeddingContent,
@@ -7158,7 +8444,7 @@ function createSharedPromoter(sharedStore, sharedVectorStore, embedder, config)
7158
8444
  }
7159
8445
 
7160
8446
  // src/core/shared-store.ts
7161
- import { randomUUID as randomUUID7 } from "crypto";
8447
+ import { randomUUID as randomUUID9 } from "crypto";
7162
8448
  var SharedStore = class {
7163
8449
  constructor(sharedEventStore) {
7164
8450
  this.sharedEventStore = sharedEventStore;
@@ -7170,7 +8456,7 @@ var SharedStore = class {
7170
8456
  * Promote a verified troubleshooting entry to shared storage
7171
8457
  */
7172
8458
  async promoteEntry(input) {
7173
- const entryId = randomUUID7();
8459
+ const entryId = randomUUID9();
7174
8460
  await dbRun(
7175
8461
  this.db,
7176
8462
  `INSERT INTO shared_troubleshooting (
@@ -7704,6 +8990,7 @@ function createMemoryServiceComposition(options) {
7704
8990
  getProjectHash: options.getProjectHash,
7705
8991
  getProjectPath: options.getProjectPath,
7706
8992
  hasSharedStore: () => sharedMemoryServices?.isEnabled() ?? false,
8993
+ memoryOperationsConfig: options.config.operations,
7707
8994
  sharedStore: {
7708
8995
  get: (entryId) => sharedMemoryServices?.getEntryForDisclosure(entryId) ?? Promise.resolve(null)
7709
8996
  },
@@ -7824,6 +9111,18 @@ var DEFAULT_ENABLED_SHARED_STORE_CONFIG = {
7824
9111
  sharedStoragePath: SHARED_STORAGE_PATH
7825
9112
  };
7826
9113
  var DEFAULT_SHARED_STORAGE_PATH = SHARED_STORAGE_PATH;
9114
+ var DISABLED_MEMORY_OPERATIONS_CONFIG = {
9115
+ enabled: false,
9116
+ facets: { enabled: true },
9117
+ actions: { enabled: true },
9118
+ retention: { enabled: false, policyVersion: "v1" },
9119
+ graphExpansion: { enabled: false, maxHops: 1 },
9120
+ lessons: { enabled: false }
9121
+ };
9122
+ var DEFAULT_ENABLED_MEMORY_OPERATIONS_CONFIG = {
9123
+ ...DISABLED_MEMORY_OPERATIONS_CONFIG,
9124
+ enabled: true
9125
+ };
7827
9126
 
7828
9127
  // src/services/memory-service-registry.ts
7829
9128
  import * as path10 from "path";
@@ -8786,8 +10085,172 @@ searchRouter.get("/", async (c) => {
8786
10085
  // src/apps/server/api/stats.ts
8787
10086
  import { Hono as Hono4 } from "hono";
8788
10087
  import * as fs9 from "fs";
10088
+ import * as os6 from "os";
8789
10089
  import * as path11 from "path";
8790
10090
  var statsRouter = new Hono4();
10091
+ var OPERATION_STATS_TABLES = [
10092
+ "memory_facets",
10093
+ "memory_actions",
10094
+ "memory_leases",
10095
+ "memory_retention_scores",
10096
+ "memory_governance_audit",
10097
+ "memory_lessons"
10098
+ ];
10099
+ var LESSON_CONFIDENCE_BUCKETS = [
10100
+ { bucket: "0.00-0.25", min: 0, max: 0.25 },
10101
+ { bucket: "0.25-0.50", min: 0.25, max: 0.5 },
10102
+ { bucket: "0.50-0.75", min: 0.5, max: 0.75 },
10103
+ { bucket: "0.75-1.00", min: 0.75, max: 1.0000001 }
10104
+ ];
10105
+ function operationsStatsHomeDir() {
10106
+ return process.env.HOME || os6.homedir();
10107
+ }
10108
+ function projectStoragePathForOperationsStats(projectOrHash) {
10109
+ const projectHash = /^[a-f0-9]{8}$/.test(projectOrHash) ? projectOrHash : hashProjectPath(projectOrHash);
10110
+ return path11.join(operationsStatsHomeDir(), ".claude-code", "memory", "projects", projectHash);
10111
+ }
10112
+ function getOperationsStatsContext(project) {
10113
+ if (project && project.trim().length > 0) {
10114
+ const normalizedProject = project.trim();
10115
+ const projectHash = /^[a-f0-9]{8}$/.test(normalizedProject) ? normalizedProject : hashProjectPath(normalizedProject);
10116
+ const storagePath2 = projectStoragePathForOperationsStats(normalizedProject);
10117
+ return {
10118
+ projectHash,
10119
+ storagePath: storagePath2,
10120
+ dbPath: path11.join(storagePath2, "events.sqlite")
10121
+ };
10122
+ }
10123
+ const storagePath = path11.join(operationsStatsHomeDir(), ".claude-code", "memory");
10124
+ return {
10125
+ storagePath,
10126
+ dbPath: path11.join(storagePath, "events.sqlite")
10127
+ };
10128
+ }
10129
+ function countRowValue(db, sql, params = []) {
10130
+ const row = sqliteGet(db, sql, params);
10131
+ return Number(row?.count ?? 0);
10132
+ }
10133
+ function projectFilter(projectHash, column = "project_hash") {
10134
+ if (!projectHash)
10135
+ return { clause: "", params: [] };
10136
+ return { clause: `WHERE ${column} = ?`, params: [projectHash] };
10137
+ }
10138
+ function sanitizeAggregateLabel(value) {
10139
+ const raw = typeof value === "string" ? value : String(value ?? "unknown");
10140
+ const sanitized = String(sanitizeGovernanceAuditValue(raw));
10141
+ if (sanitized !== raw || sanitized.includes("[REDACTED]"))
10142
+ return "[REDACTED]";
10143
+ const trimmed = sanitized.trim();
10144
+ return trimmed.length > 0 ? trimmed.slice(0, 96) : "unknown";
10145
+ }
10146
+ function emptyOperationsStatsPayload(context, databaseExists, missingTables, windowDays) {
10147
+ return {
10148
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
10149
+ windowDays,
10150
+ projectHash: context.projectHash,
10151
+ projection: {
10152
+ databaseExists,
10153
+ available: databaseExists && missingTables.length === 0,
10154
+ missingTables
10155
+ },
10156
+ facets: { totalAssignments: 0, distribution: [] },
10157
+ actions: { total: 0, byStatus: [] },
10158
+ leases: { totalActive: 0, activeByTargetType: [] },
10159
+ retention: { total: 0, byDecision: [] },
10160
+ governanceAudit: { total: 0, operationsByDay: [] },
10161
+ lessons: {
10162
+ total: 0,
10163
+ confidenceBuckets: LESSON_CONFIDENCE_BUCKETS.map((bucket) => ({ bucket: bucket.bucket, count: 0 }))
10164
+ }
10165
+ };
10166
+ }
10167
+ function getMissingOperationTables(db) {
10168
+ const placeholders = OPERATION_STATS_TABLES.map(() => "?").join(", ");
10169
+ const rows = sqliteAll(
10170
+ db,
10171
+ `SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (${placeholders})`,
10172
+ [...OPERATION_STATS_TABLES]
10173
+ );
10174
+ const present = new Set(rows.map((row) => row.name));
10175
+ return OPERATION_STATS_TABLES.filter((table) => !present.has(table));
10176
+ }
10177
+ function sortCountRows(rows, labelKey) {
10178
+ return [...rows].sort((a, b) => {
10179
+ const countDiff = Number(b.count ?? 0) - Number(a.count ?? 0);
10180
+ if (countDiff !== 0)
10181
+ return countDiff;
10182
+ return String(a[labelKey] ?? "").localeCompare(String(b[labelKey] ?? ""));
10183
+ });
10184
+ }
10185
+ function buildFacetDistribution(db, projectHash, topFacetValues) {
10186
+ const filter = projectFilter(projectHash);
10187
+ const rows = sqliteAll(
10188
+ db,
10189
+ `SELECT dimension, value, COUNT(*) AS count
10190
+ FROM memory_facets
10191
+ ${filter.clause}
10192
+ GROUP BY dimension, value
10193
+ ORDER BY dimension ASC, count DESC, value ASC`,
10194
+ filter.params
10195
+ );
10196
+ const dimensionMap = /* @__PURE__ */ new Map();
10197
+ for (const row of rows) {
10198
+ const dimension = sanitizeAggregateLabel(row.dimension);
10199
+ const value = sanitizeAggregateLabel(row.value);
10200
+ const values = dimensionMap.get(dimension) ?? /* @__PURE__ */ new Map();
10201
+ values.set(value, (values.get(value) ?? 0) + Number(row.count ?? 0));
10202
+ dimensionMap.set(dimension, values);
10203
+ }
10204
+ return Array.from(dimensionMap.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([dimension, values]) => {
10205
+ const sortedValues = Array.from(values.entries()).map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
10206
+ const visible = sortedValues.slice(0, topFacetValues);
10207
+ const other = sortedValues.slice(topFacetValues).reduce((sum, row) => sum + row.count, 0);
10208
+ return { dimension, values: visible, other };
10209
+ });
10210
+ }
10211
+ function buildCountRows(db, sql, params, labelKey) {
10212
+ const rows = sqliteAll(db, sql, params);
10213
+ return sortCountRows(rows, "label").map((row) => ({ [labelKey]: sanitizeAggregateLabel(row.label), count: Number(row.count ?? 0) }));
10214
+ }
10215
+ function buildOperationsByDay(db, projectHash, windowStartIso) {
10216
+ const clauses = ["created_at >= ?"];
10217
+ const params = [windowStartIso];
10218
+ if (projectHash) {
10219
+ clauses.push("project_hash = ?");
10220
+ params.push(projectHash);
10221
+ }
10222
+ const rows = sqliteAll(
10223
+ db,
10224
+ `SELECT date(created_at) AS date, operation, COUNT(*) AS count
10225
+ FROM memory_governance_audit
10226
+ WHERE ${clauses.join(" AND ")}
10227
+ GROUP BY date(created_at), operation
10228
+ ORDER BY date ASC, operation ASC`,
10229
+ params
10230
+ );
10231
+ const byDay = /* @__PURE__ */ new Map();
10232
+ for (const row of rows) {
10233
+ const operations = byDay.get(row.date) ?? [];
10234
+ operations.push({ operation: sanitizeAggregateLabel(row.operation), count: Number(row.count ?? 0) });
10235
+ byDay.set(row.date, operations);
10236
+ }
10237
+ return Array.from(byDay.entries()).map(([date, operations]) => ({
10238
+ date,
10239
+ total: operations.reduce((sum, row) => sum + row.count, 0),
10240
+ operations
10241
+ }));
10242
+ }
10243
+ function buildLessonConfidenceBuckets(db, projectHash) {
10244
+ const filter = projectFilter(projectHash);
10245
+ const rows = sqliteAll(db, `SELECT confidence FROM memory_lessons ${filter.clause}`, filter.params);
10246
+ return LESSON_CONFIDENCE_BUCKETS.map((bucket) => ({
10247
+ bucket: bucket.bucket,
10248
+ count: rows.filter((row) => {
10249
+ const confidence = Number(row.confidence ?? 0);
10250
+ return confidence >= bucket.min && confidence < bucket.max;
10251
+ }).length
10252
+ }));
10253
+ }
8791
10254
  var DEFAULT_KPI_THRESHOLDS = {
8792
10255
  usefulRecallRateMin: 0.45,
8793
10256
  reworkRateMax: 0.25,
@@ -9396,6 +10859,93 @@ statsRouter.get("/levels/:level", async (c) => {
9396
10859
  await memoryService.shutdown();
9397
10860
  }
9398
10861
  });
10862
+ statsRouter.get("/operations", async (c) => {
10863
+ const context = getOperationsStatsContext(c.req.query("project") || c.req.query("projectId"));
10864
+ const windowDays = parseStatsLimit(c.req.query("windowDays"), 30, 365);
10865
+ const topFacetValues = parseStatsLimit(c.req.query("topFacetValues"), 5, 25);
10866
+ const databaseExists = fs9.existsSync(context.dbPath);
10867
+ if (!databaseExists) {
10868
+ return c.json(emptyOperationsStatsPayload(context, false, [...OPERATION_STATS_TABLES], windowDays));
10869
+ }
10870
+ let db = null;
10871
+ try {
10872
+ db = createSQLiteDatabase(context.dbPath, { readonly: true, walMode: false });
10873
+ const missingTables = getMissingOperationTables(db);
10874
+ if (missingTables.length > 0) {
10875
+ return c.json(emptyOperationsStatsPayload(context, true, missingTables, windowDays));
10876
+ }
10877
+ const now = Date.now();
10878
+ const windowStartIso = new Date(now - windowDays * 24 * 60 * 60 * 1e3).toISOString();
10879
+ const projectScoped = projectFilter(context.projectHash);
10880
+ const auditClauses = ["created_at >= ?"];
10881
+ const auditParams = [windowStartIso];
10882
+ if (context.projectHash) {
10883
+ auditClauses.push("project_hash = ?");
10884
+ auditParams.push(context.projectHash);
10885
+ }
10886
+ const auditWhere = `WHERE ${auditClauses.join(" AND ")}`;
10887
+ const facets = {
10888
+ totalAssignments: countRowValue(db, `SELECT COUNT(*) AS count FROM memory_facets ${projectScoped.clause}`, projectScoped.params),
10889
+ distribution: buildFacetDistribution(db, context.projectHash, topFacetValues)
10890
+ };
10891
+ const actions = {
10892
+ total: countRowValue(db, `SELECT COUNT(*) AS count FROM memory_actions ${projectScoped.clause}`, projectScoped.params),
10893
+ byStatus: buildCountRows(
10894
+ db,
10895
+ `SELECT status AS label, COUNT(*) AS count FROM memory_actions ${projectScoped.clause} GROUP BY status`,
10896
+ projectScoped.params,
10897
+ "status"
10898
+ )
10899
+ };
10900
+ const leases = {
10901
+ totalActive: countRowValue(db, "SELECT COUNT(*) AS count FROM memory_leases WHERE released_at IS NULL AND expires_at > ?", [new Date(now).toISOString()]),
10902
+ activeByTargetType: buildCountRows(
10903
+ db,
10904
+ "SELECT target_type AS label, COUNT(*) AS count FROM memory_leases WHERE released_at IS NULL AND expires_at > ? GROUP BY target_type",
10905
+ [new Date(now).toISOString()],
10906
+ "targetType"
10907
+ )
10908
+ };
10909
+ const retention = {
10910
+ total: countRowValue(db, `SELECT COUNT(*) AS count FROM memory_retention_scores ${projectScoped.clause}`, projectScoped.params),
10911
+ byDecision: buildCountRows(
10912
+ db,
10913
+ `SELECT decision AS label, COUNT(*) AS count FROM memory_retention_scores ${projectScoped.clause} GROUP BY decision`,
10914
+ projectScoped.params,
10915
+ "decision"
10916
+ )
10917
+ };
10918
+ const governanceAudit = {
10919
+ total: countRowValue(db, `SELECT COUNT(*) AS count FROM memory_governance_audit ${auditWhere}`, auditParams),
10920
+ operationsByDay: buildOperationsByDay(db, context.projectHash, windowStartIso)
10921
+ };
10922
+ const lessons = {
10923
+ total: countRowValue(db, `SELECT COUNT(*) AS count FROM memory_lessons ${projectScoped.clause}`, projectScoped.params),
10924
+ confidenceBuckets: buildLessonConfidenceBuckets(db, context.projectHash)
10925
+ };
10926
+ return c.json({
10927
+ generatedAt: new Date(now).toISOString(),
10928
+ windowDays,
10929
+ projectHash: context.projectHash,
10930
+ projection: {
10931
+ databaseExists: true,
10932
+ available: true,
10933
+ missingTables: []
10934
+ },
10935
+ facets,
10936
+ actions,
10937
+ leases,
10938
+ retention,
10939
+ governanceAudit,
10940
+ lessons
10941
+ });
10942
+ } catch (error) {
10943
+ console.error("[stats/operations] Failed to load aggregate stats:", error);
10944
+ return c.json({ error: "Failed to load operations stats" }, 500);
10945
+ } finally {
10946
+ db?.close();
10947
+ }
10948
+ });
9399
10949
  statsRouter.get("/", async (c) => {
9400
10950
  const memoryService = getLightweightServiceFromQuery(c);
9401
10951
  try {
@@ -10020,11 +11570,11 @@ turnsRouter.post("/backfill", async (c) => {
10020
11570
  import { Hono as Hono7 } from "hono";
10021
11571
  import * as fs10 from "fs";
10022
11572
  import * as path12 from "path";
10023
- import * as os6 from "os";
11573
+ import * as os7 from "os";
10024
11574
  var projectsRouter = new Hono7();
10025
11575
  projectsRouter.get("/", async (c) => {
10026
11576
  try {
10027
- const projectsDir = path12.join(os6.homedir(), ".claude-code", "memory", "projects");
11577
+ const projectsDir = path12.join(os7.homedir(), ".claude-code", "memory", "projects");
10028
11578
  if (!fs10.existsSync(projectsDir)) {
10029
11579
  return c.json({ projects: [] });
10030
11580
  }