claude-memory-layer 1.0.39 → 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.
@@ -2787,6 +2787,127 @@ var SQLiteEventStore = class {
2787
2787
  updated_at TEXT DEFAULT (datetime('now'))
2788
2788
  );
2789
2789
 
2790
+ -- Memory Operations: facet assignments (derived, rebuildable projection)
2791
+ CREATE TABLE IF NOT EXISTS memory_facets (
2792
+ id TEXT PRIMARY KEY,
2793
+ target_type TEXT NOT NULL,
2794
+ target_id TEXT NOT NULL,
2795
+ dimension TEXT NOT NULL,
2796
+ value TEXT NOT NULL,
2797
+ confidence REAL NOT NULL DEFAULT 1.0,
2798
+ source TEXT NOT NULL DEFAULT 'manual',
2799
+ evidence_event_ids TEXT NOT NULL DEFAULT '[]',
2800
+ project_hash TEXT NOT NULL DEFAULT '',
2801
+ created_at TEXT NOT NULL,
2802
+ updated_at TEXT NOT NULL,
2803
+ UNIQUE(target_type, target_id, dimension, value, source, project_hash)
2804
+ );
2805
+
2806
+ -- Memory Operations: operational action projection
2807
+ CREATE TABLE IF NOT EXISTS memory_actions (
2808
+ action_id TEXT PRIMARY KEY,
2809
+ project_hash TEXT NOT NULL,
2810
+ title TEXT NOT NULL,
2811
+ status TEXT NOT NULL DEFAULT 'pending',
2812
+ priority INTEGER NOT NULL DEFAULT 0,
2813
+ source_event_ids TEXT NOT NULL DEFAULT '[]',
2814
+ related_entity_ids TEXT NOT NULL DEFAULT '[]',
2815
+ current_checkpoint_id TEXT,
2816
+ lease_id TEXT,
2817
+ created_at TEXT NOT NULL,
2818
+ updated_at TEXT NOT NULL
2819
+ );
2820
+
2821
+ -- Memory Operations: action dependency/reference edges
2822
+ CREATE TABLE IF NOT EXISTS memory_action_edges (
2823
+ edge_id TEXT PRIMARY KEY,
2824
+ src_action_id TEXT NOT NULL,
2825
+ rel_type TEXT NOT NULL,
2826
+ dst_type TEXT NOT NULL,
2827
+ dst_id TEXT NOT NULL,
2828
+ confidence REAL NOT NULL DEFAULT 1.0,
2829
+ source TEXT NOT NULL DEFAULT 'manual',
2830
+ created_at TEXT NOT NULL,
2831
+ UNIQUE(src_action_id, rel_type, dst_type, dst_id, source)
2832
+ );
2833
+
2834
+ -- Memory Operations: short-lived leases for operational work
2835
+ CREATE TABLE IF NOT EXISTS memory_leases (
2836
+ lease_id TEXT PRIMARY KEY,
2837
+ target_type TEXT NOT NULL,
2838
+ target_id TEXT NOT NULL,
2839
+ holder TEXT NOT NULL,
2840
+ expires_at TEXT NOT NULL,
2841
+ metadata_json TEXT,
2842
+ created_at TEXT NOT NULL,
2843
+ renewed_at TEXT,
2844
+ released_at TEXT
2845
+ );
2846
+
2847
+ -- Memory Operations: resumable checkpoints for delegated or long-running work
2848
+ CREATE TABLE IF NOT EXISTS memory_checkpoints (
2849
+ checkpoint_id TEXT PRIMARY KEY,
2850
+ project_hash TEXT NOT NULL,
2851
+ action_id TEXT,
2852
+ session_id TEXT,
2853
+ title TEXT NOT NULL,
2854
+ summary TEXT NOT NULL,
2855
+ state_json TEXT NOT NULL,
2856
+ source_event_ids TEXT NOT NULL DEFAULT '[]',
2857
+ created_at TEXT NOT NULL,
2858
+ expires_at TEXT
2859
+ );
2860
+
2861
+ -- Memory Operations: retention lifecycle score projection
2862
+ CREATE TABLE IF NOT EXISTS memory_retention_scores (
2863
+ score_id TEXT PRIMARY KEY,
2864
+ target_type TEXT NOT NULL,
2865
+ target_id TEXT NOT NULL,
2866
+ project_hash TEXT NOT NULL,
2867
+ policy_version TEXT NOT NULL,
2868
+ decision TEXT NOT NULL,
2869
+ lifecycle_score REAL NOT NULL,
2870
+ factors_json TEXT NOT NULL,
2871
+ reasons_json TEXT NOT NULL,
2872
+ dry_run_diff_json TEXT NOT NULL,
2873
+ source_event_ids TEXT NOT NULL DEFAULT '[]',
2874
+ evaluated_at TEXT NOT NULL,
2875
+ created_at TEXT NOT NULL,
2876
+ updated_at TEXT NOT NULL,
2877
+ UNIQUE(target_type, target_id, project_hash, policy_version)
2878
+ );
2879
+
2880
+ -- Memory Operations: procedural lessons derived from successful workflows
2881
+ CREATE TABLE IF NOT EXISTS memory_lessons (
2882
+ lesson_id TEXT PRIMARY KEY,
2883
+ project_hash TEXT NOT NULL DEFAULT '',
2884
+ name TEXT NOT NULL,
2885
+ trigger TEXT NOT NULL,
2886
+ steps_json TEXT NOT NULL,
2887
+ confidence REAL NOT NULL,
2888
+ source_session_ids TEXT NOT NULL DEFAULT '[]',
2889
+ source_event_ids TEXT NOT NULL DEFAULT '[]',
2890
+ failure_modes_json TEXT NOT NULL DEFAULT '[]',
2891
+ skill_candidate INTEGER NOT NULL DEFAULT 0,
2892
+ created_at TEXT NOT NULL,
2893
+ updated_at TEXT NOT NULL,
2894
+ UNIQUE(project_hash, name)
2895
+ );
2896
+
2897
+ -- Memory Operations: governance/audit trail for state-changing operations
2898
+ CREATE TABLE IF NOT EXISTS memory_governance_audit (
2899
+ audit_id TEXT PRIMARY KEY,
2900
+ operation TEXT NOT NULL,
2901
+ actor TEXT NOT NULL,
2902
+ project_hash TEXT,
2903
+ target_type TEXT NOT NULL,
2904
+ target_id TEXT NOT NULL,
2905
+ before_json TEXT,
2906
+ after_json TEXT,
2907
+ source_event_ids TEXT NOT NULL DEFAULT '[]',
2908
+ created_at TEXT NOT NULL
2909
+ );
2910
+
2790
2911
  -- Create indexes
2791
2912
  CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
2792
2913
  CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
@@ -2813,6 +2934,23 @@ var SQLiteEventStore = class {
2813
2934
  CREATE INDEX IF NOT EXISTS idx_retrieval_traces_created_at ON retrieval_traces(created_at DESC);
2814
2935
  CREATE INDEX IF NOT EXISTS idx_retrieval_traces_project_hash ON retrieval_traces(project_hash);
2815
2936
  CREATE INDEX IF NOT EXISTS idx_retrieval_traces_session_id ON retrieval_traces(session_id);
2937
+ CREATE INDEX IF NOT EXISTS idx_memory_facets_project_dimension_value ON memory_facets(project_hash, dimension, value);
2938
+ CREATE INDEX IF NOT EXISTS idx_memory_facets_target ON memory_facets(target_type, target_id);
2939
+ CREATE INDEX IF NOT EXISTS idx_memory_facets_dimension_value_confidence ON memory_facets(dimension, value, confidence DESC);
2940
+ CREATE INDEX IF NOT EXISTS idx_memory_actions_project_status_priority ON memory_actions(project_hash, status, priority DESC, updated_at DESC);
2941
+ CREATE INDEX IF NOT EXISTS idx_memory_action_edges_src ON memory_action_edges(src_action_id, rel_type);
2942
+ CREATE INDEX IF NOT EXISTS idx_memory_action_edges_dst ON memory_action_edges(dst_type, dst_id);
2943
+ CREATE INDEX IF NOT EXISTS idx_memory_leases_target_expires ON memory_leases(target_type, target_id, expires_at);
2944
+ CREATE INDEX IF NOT EXISTS idx_memory_checkpoints_project_action_created ON memory_checkpoints(project_hash, action_id, created_at DESC);
2945
+ CREATE INDEX IF NOT EXISTS idx_memory_checkpoints_project_session_created ON memory_checkpoints(project_hash, session_id, created_at DESC);
2946
+ 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);
2947
+ CREATE INDEX IF NOT EXISTS idx_memory_retention_scores_target ON memory_retention_scores(target_type, target_id, project_hash);
2948
+ CREATE INDEX IF NOT EXISTS idx_memory_retention_scores_policy_evaluated ON memory_retention_scores(policy_version, evaluated_at DESC);
2949
+ CREATE INDEX IF NOT EXISTS idx_memory_lessons_project_confidence ON memory_lessons(project_hash, confidence DESC, updated_at DESC);
2950
+ CREATE INDEX IF NOT EXISTS idx_memory_lessons_skill_candidate ON memory_lessons(project_hash, skill_candidate, confidence DESC);
2951
+ CREATE INDEX IF NOT EXISTS idx_memory_lessons_updated ON memory_lessons(updated_at DESC);
2952
+ CREATE INDEX IF NOT EXISTS idx_memory_governance_audit_project_operation ON memory_governance_audit(project_hash, operation, created_at DESC);
2953
+ CREATE INDEX IF NOT EXISTS idx_memory_governance_audit_target ON memory_governance_audit(target_type, target_id, created_at DESC);
2816
2954
 
2817
2955
  -- FTS5 Full-Text Search for fast keyword search
2818
2956
  CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
@@ -2835,6 +2973,48 @@ var SQLiteEventStore = class {
2835
2973
  INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
2836
2974
  END;
2837
2975
  `);
2976
+ try {
2977
+ sqliteExec(this.db, `ALTER TABLE memory_action_edges ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';`);
2978
+ } catch {
2979
+ }
2980
+ try {
2981
+ const edgeIndexes = sqliteAll(this.db, `PRAGMA index_list(memory_action_edges)`, []);
2982
+ const hasSourceAwareUnique = edgeIndexes.some((index) => {
2983
+ if (Number(index.unique) !== 1)
2984
+ return false;
2985
+ if (!/^[A-Za-z0-9_]+$/.test(index.name))
2986
+ return false;
2987
+ const escapedName = index.name.replace(/"/g, '""');
2988
+ const columns = sqliteAll(this.db, 'PRAGMA index_info("' + escapedName + '")', []).map((column) => column.name);
2989
+ return columns.length === 5 && columns[0] === "src_action_id" && columns[1] === "rel_type" && columns[2] === "dst_type" && columns[3] === "dst_id" && columns[4] === "source";
2990
+ });
2991
+ if (!hasSourceAwareUnique) {
2992
+ sqliteExec(this.db, `
2993
+ DROP TABLE IF EXISTS memory_action_edges_v2;
2994
+ CREATE TABLE memory_action_edges_v2 (
2995
+ edge_id TEXT PRIMARY KEY,
2996
+ src_action_id TEXT NOT NULL,
2997
+ rel_type TEXT NOT NULL,
2998
+ dst_type TEXT NOT NULL,
2999
+ dst_id TEXT NOT NULL,
3000
+ confidence REAL NOT NULL DEFAULT 1.0,
3001
+ source TEXT NOT NULL DEFAULT 'manual',
3002
+ created_at TEXT NOT NULL,
3003
+ UNIQUE(src_action_id, rel_type, dst_type, dst_id, source)
3004
+ );
3005
+ INSERT OR IGNORE INTO memory_action_edges_v2 (
3006
+ edge_id, src_action_id, rel_type, dst_type, dst_id, confidence, source, created_at
3007
+ )
3008
+ SELECT edge_id, src_action_id, rel_type, dst_type, dst_id, confidence, source, created_at
3009
+ FROM memory_action_edges;
3010
+ DROP TABLE memory_action_edges;
3011
+ ALTER TABLE memory_action_edges_v2 RENAME TO memory_action_edges;
3012
+ CREATE INDEX IF NOT EXISTS idx_memory_action_edges_src ON memory_action_edges(src_action_id, rel_type);
3013
+ CREATE INDEX IF NOT EXISTS idx_memory_action_edges_dst ON memory_action_edges(dst_type, dst_id);
3014
+ `);
3015
+ }
3016
+ } catch {
3017
+ }
2838
3018
  try {
2839
3019
  sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN selected_details_json TEXT;`);
2840
3020
  } catch {
@@ -4903,6 +5083,896 @@ var MemoryQueryService = class {
4903
5083
  }
4904
5084
  };
4905
5085
 
5086
+ // src/core/operations/facet-repository.ts
5087
+ import { randomUUID as randomUUID7 } from "crypto";
5088
+
5089
+ // src/core/operations/facets.ts
5090
+ import { z } from "zod";
5091
+ var FacetTargetTypeSchema = z.enum([
5092
+ "event",
5093
+ "entity",
5094
+ "edge",
5095
+ "consolidated_memory",
5096
+ "lesson",
5097
+ "action"
5098
+ ]);
5099
+ var BUILT_IN_FACET_DIMENSIONS = [
5100
+ "kind",
5101
+ "workflow",
5102
+ "artifact",
5103
+ "source",
5104
+ "privacy",
5105
+ "quality",
5106
+ "retention",
5107
+ "project"
5108
+ ];
5109
+ var customDimensionPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
5110
+ var TrimmedStringSchema = z.preprocess(
5111
+ (value) => typeof value === "string" ? value.trim() : value,
5112
+ z.string().min(1)
5113
+ );
5114
+ var FacetDimensionSchema = z.preprocess(
5115
+ (value) => typeof value === "string" ? value.trim() : value,
5116
+ z.string().min(1).max(64).refine((value) => BUILT_IN_FACET_DIMENSIONS.indexOf(value) !== -1 || customDimensionPattern.test(value), {
5117
+ message: "Facet dimension must be built-in or lowercase kebab-case"
5118
+ })
5119
+ );
5120
+ var FacetSourceSchema = z.enum(["manual", "imported", "derived", "llm", "system"]).default("manual");
5121
+ var EvidenceEventIdsSchema = z.preprocess(
5122
+ (value) => Array.isArray(value) ? value.map((item) => typeof item === "string" ? item.trim() : item).filter(Boolean) : value,
5123
+ z.array(z.string().min(1)).default([])
5124
+ );
5125
+ var OptionalTrimmedStringSchema = z.preprocess(
5126
+ (value) => typeof value === "string" ? value.trim() : value,
5127
+ z.string().min(1).optional()
5128
+ );
5129
+ var MemoryFacetAssignmentInputSchema = z.object({
5130
+ targetType: FacetTargetTypeSchema,
5131
+ targetId: TrimmedStringSchema,
5132
+ dimension: FacetDimensionSchema,
5133
+ value: TrimmedStringSchema,
5134
+ confidence: z.number().min(0).max(1).default(1),
5135
+ source: FacetSourceSchema,
5136
+ evidenceEventIds: EvidenceEventIdsSchema,
5137
+ projectHash: OptionalTrimmedStringSchema,
5138
+ actor: OptionalTrimmedStringSchema
5139
+ });
5140
+ var MemoryFacetAssignmentSchema = MemoryFacetAssignmentInputSchema.extend({
5141
+ id: z.string().min(1),
5142
+ createdAt: z.date(),
5143
+ updatedAt: z.date()
5144
+ });
5145
+ var FacetRemoveInputSchema = z.object({
5146
+ targetType: FacetTargetTypeSchema,
5147
+ targetId: TrimmedStringSchema,
5148
+ dimension: FacetDimensionSchema,
5149
+ value: TrimmedStringSchema,
5150
+ source: FacetSourceSchema,
5151
+ projectHash: OptionalTrimmedStringSchema,
5152
+ actor: OptionalTrimmedStringSchema
5153
+ });
5154
+ var FacetQuerySchema = z.object({
5155
+ targetType: FacetTargetTypeSchema.optional(),
5156
+ targetId: OptionalTrimmedStringSchema,
5157
+ dimension: FacetDimensionSchema.optional(),
5158
+ value: OptionalTrimmedStringSchema,
5159
+ source: FacetSourceSchema.optional(),
5160
+ projectHash: OptionalTrimmedStringSchema,
5161
+ limit: z.number().int().positive().max(500).default(100)
5162
+ });
5163
+ function parseFacetAssignmentInput(input) {
5164
+ return MemoryFacetAssignmentInputSchema.parse(input);
5165
+ }
5166
+ function parseFacetRemoveInput(input) {
5167
+ return FacetRemoveInputSchema.parse(input);
5168
+ }
5169
+ function parseFacetQuery(input) {
5170
+ return FacetQuerySchema.parse(input ?? {});
5171
+ }
5172
+
5173
+ // src/core/operations/governance-audit.ts
5174
+ import { randomUUID as randomUUID6 } from "crypto";
5175
+ var MEMORY_GOVERNANCE_AUDIT_OPERATIONS = [
5176
+ "facet_tag",
5177
+ "action_update",
5178
+ "lease_acquire",
5179
+ "checkpoint_create",
5180
+ "retention_score",
5181
+ "quarantine",
5182
+ "verify",
5183
+ "lesson_promote"
5184
+ ];
5185
+ function normalizeRequiredString(value, fieldName) {
5186
+ const normalized = value.trim();
5187
+ if (!normalized) {
5188
+ throw new Error(`${fieldName} is required`);
5189
+ }
5190
+ return normalized;
5191
+ }
5192
+ function normalizeOptionalString(value) {
5193
+ const normalized = value?.trim();
5194
+ return normalized ? normalized : void 0;
5195
+ }
5196
+ var REDACTED = "[REDACTED]";
5197
+ var sensitiveKeyPattern = /(?:api[_-]?key|secret|password|passwd|token|access[_-]?token|client[_-]?secret|crtfc[_-]?key|hashkey|serviceKey)/i;
5198
+ var POSIX_ABSOLUTE_PATH_PATTERN = /(^|[^A-Za-z0-9._\/\\-])\/(?!\/)[^\n\r"'<>|`]*/g;
5199
+ var WINDOWS_DRIVE_PATH_PATTERN = /(^|[^A-Za-z0-9._\/\\-])(?:[A-Za-z]:[\\/][^\n\r"'<>|`]*)/g;
5200
+ var WINDOWS_UNC_PATH_PATTERN = /(^|[^A-Za-z0-9._\/\\-])(?:\\\\[^\\\n\r"'<>|`]+\\[^\n\r"'<>|`]*)/g;
5201
+ var credentialQueryPattern = /\b((?:api[_-]?key|token|access[_-]?token|client[_-]?secret|crtfc[_-]?key|hashkey|serviceKey)=)[^&\s`"'<>]+/gi;
5202
+ var credentialAssignmentPattern = /\b((?:api[_-]?key|secret|password|passwd|token|access[_-]?token|client[_-]?secret|crtfc[_-]?key|hashkey|serviceKey)\s*[:=]\s*)[^\s`"'<>},]+/gi;
5203
+ function redactAbsolutePaths(value) {
5204
+ 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}`);
5205
+ }
5206
+ function sanitizeAuditString(value) {
5207
+ return redactAbsolutePaths(value).replace(credentialQueryPattern, `$1${REDACTED}`).replace(credentialAssignmentPattern, `$1${REDACTED}`);
5208
+ }
5209
+ function sanitizeGovernanceAuditValue(value, key) {
5210
+ if (key && sensitiveKeyPattern.test(key)) {
5211
+ return REDACTED;
5212
+ }
5213
+ if (typeof value === "string") {
5214
+ return sanitizeAuditString(value);
5215
+ }
5216
+ if (value instanceof Date) {
5217
+ return sanitizeAuditString(value.toISOString());
5218
+ }
5219
+ if (Array.isArray(value)) {
5220
+ return value.map((item) => sanitizeGovernanceAuditValue(item));
5221
+ }
5222
+ if (value && typeof value === "object") {
5223
+ const sanitized = {};
5224
+ for (const [entryKey, entryValue] of Object.entries(value)) {
5225
+ sanitized[sanitizeAuditString(entryKey)] = sanitizeGovernanceAuditValue(entryValue, entryKey);
5226
+ }
5227
+ return sanitized;
5228
+ }
5229
+ return value;
5230
+ }
5231
+ function sanitizeAuditJson(value) {
5232
+ if (value === void 0)
5233
+ return void 0;
5234
+ return sanitizeGovernanceAuditValue(value);
5235
+ }
5236
+ function normalizeSourceEventIds(sourceEventIds) {
5237
+ return (sourceEventIds || []).map((sourceEventId) => sanitizeAuditString(sourceEventId.trim())).filter((sourceEventId) => sourceEventId.length > 0);
5238
+ }
5239
+ function normalizeOperation(operation) {
5240
+ if (MEMORY_GOVERNANCE_AUDIT_OPERATIONS.indexOf(operation) === -1) {
5241
+ throw new Error(`Unsupported governance audit operation: ${operation}`);
5242
+ }
5243
+ return operation;
5244
+ }
5245
+ async function writeGovernanceAuditEntry(db, input) {
5246
+ const entry = {
5247
+ auditId: randomUUID6(),
5248
+ operation: normalizeOperation(input.operation),
5249
+ actor: sanitizeAuditString(normalizeRequiredString(input.actor, "actor")),
5250
+ projectHash: normalizeOptionalString(input.projectHash),
5251
+ targetType: normalizeRequiredString(input.targetType, "targetType"),
5252
+ targetId: sanitizeAuditString(normalizeRequiredString(input.targetId, "targetId")),
5253
+ beforeJson: sanitizeAuditJson(input.beforeJson),
5254
+ afterJson: sanitizeAuditJson(input.afterJson),
5255
+ sourceEventIds: normalizeSourceEventIds(input.sourceEventIds),
5256
+ createdAt: /* @__PURE__ */ new Date()
5257
+ };
5258
+ sqliteRun(
5259
+ db,
5260
+ `INSERT INTO memory_governance_audit (
5261
+ audit_id, operation, actor, project_hash, target_type, target_id,
5262
+ before_json, after_json, source_event_ids, created_at
5263
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5264
+ [
5265
+ entry.auditId,
5266
+ entry.operation,
5267
+ entry.actor,
5268
+ entry.projectHash ?? null,
5269
+ entry.targetType,
5270
+ entry.targetId,
5271
+ entry.beforeJson === void 0 ? null : JSON.stringify(entry.beforeJson),
5272
+ entry.afterJson === void 0 ? null : JSON.stringify(entry.afterJson),
5273
+ JSON.stringify(entry.sourceEventIds),
5274
+ entry.createdAt.toISOString()
5275
+ ]
5276
+ );
5277
+ return entry;
5278
+ }
5279
+
5280
+ // src/core/operations/facet-repository.ts
5281
+ function parseStringArray(value) {
5282
+ if (typeof value !== "string")
5283
+ return [];
5284
+ try {
5285
+ const parsed = JSON.parse(value);
5286
+ if (!Array.isArray(parsed))
5287
+ return [];
5288
+ return parsed.filter((item) => typeof item === "string" && item.length > 0);
5289
+ } catch {
5290
+ return [];
5291
+ }
5292
+ }
5293
+ function projectHashToStorage(projectHash) {
5294
+ return projectHash ?? "";
5295
+ }
5296
+ function rowToFacet(row) {
5297
+ const projectHash = typeof row.project_hash === "string" && row.project_hash.length > 0 ? row.project_hash : void 0;
5298
+ return {
5299
+ id: row.id,
5300
+ targetType: FacetTargetTypeSchema.parse(row.target_type),
5301
+ targetId: row.target_id,
5302
+ dimension: row.dimension,
5303
+ value: row.value,
5304
+ confidence: Number(row.confidence),
5305
+ source: FacetSourceSchema.parse(row.source),
5306
+ evidenceEventIds: parseStringArray(row.evidence_event_ids),
5307
+ projectHash,
5308
+ createdAt: toDateFromSQLite(row.created_at),
5309
+ updatedAt: toDateFromSQLite(row.updated_at)
5310
+ };
5311
+ }
5312
+ function facetToAuditJson(facet) {
5313
+ return {
5314
+ id: facet.id,
5315
+ targetType: facet.targetType,
5316
+ targetId: facet.targetId,
5317
+ dimension: facet.dimension,
5318
+ value: facet.value,
5319
+ confidence: facet.confidence,
5320
+ source: facet.source,
5321
+ evidenceEventIds: facet.evidenceEventIds,
5322
+ projectHash: facet.projectHash,
5323
+ createdAt: facet.createdAt.toISOString(),
5324
+ updatedAt: facet.updatedAt.toISOString()
5325
+ };
5326
+ }
5327
+ var FacetRepository = class {
5328
+ constructor(db) {
5329
+ this.db = db;
5330
+ }
5331
+ async assign(input) {
5332
+ const assignment = parseFacetAssignmentInput(input);
5333
+ const existing = this.findByUniqueKey(assignment);
5334
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5335
+ if (existing) {
5336
+ sqliteRun(
5337
+ this.db,
5338
+ `UPDATE memory_facets
5339
+ SET confidence = ?, evidence_event_ids = ?, project_hash = ?, updated_at = ?
5340
+ WHERE id = ?`,
5341
+ [
5342
+ assignment.confidence,
5343
+ JSON.stringify(assignment.evidenceEventIds),
5344
+ projectHashToStorage(assignment.projectHash),
5345
+ now,
5346
+ existing.id
5347
+ ]
5348
+ );
5349
+ const saved2 = this.getById(existing.id);
5350
+ await this.auditAssignment(assignment, existing, saved2);
5351
+ return saved2;
5352
+ }
5353
+ const id = randomUUID7();
5354
+ sqliteRun(
5355
+ this.db,
5356
+ `INSERT INTO memory_facets (
5357
+ id, target_type, target_id, dimension, value, confidence, source,
5358
+ evidence_event_ids, project_hash, created_at, updated_at
5359
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5360
+ [
5361
+ id,
5362
+ assignment.targetType,
5363
+ assignment.targetId,
5364
+ assignment.dimension,
5365
+ assignment.value,
5366
+ assignment.confidence,
5367
+ assignment.source,
5368
+ JSON.stringify(assignment.evidenceEventIds),
5369
+ projectHashToStorage(assignment.projectHash),
5370
+ now,
5371
+ now
5372
+ ]
5373
+ );
5374
+ const saved = this.getById(id);
5375
+ await this.auditAssignment(assignment, null, saved);
5376
+ return saved;
5377
+ }
5378
+ async remove(input) {
5379
+ const removeInput = parseFacetRemoveInput(input);
5380
+ const { sql, params } = this.removeSql(removeInput);
5381
+ const result = sqliteRun(this.db, sql, params);
5382
+ return result.changes > 0;
5383
+ }
5384
+ async query(input) {
5385
+ const query = parseFacetQuery(input);
5386
+ const { sql, params } = this.querySql(query);
5387
+ const rows = sqliteAll(this.db, sql, params);
5388
+ return rows.map(rowToFacet);
5389
+ }
5390
+ async listForTarget(targetType, targetId) {
5391
+ const parsedTargetType = FacetTargetTypeSchema.parse(targetType);
5392
+ const trimmedTargetId = targetId.trim();
5393
+ if (!trimmedTargetId) {
5394
+ throw new Error("targetId is required");
5395
+ }
5396
+ return this.query({ targetType: parsedTargetType, targetId: trimmedTargetId, limit: 500 });
5397
+ }
5398
+ getById(id) {
5399
+ const row = sqliteGet(this.db, `SELECT * FROM memory_facets WHERE id = ?`, [id]);
5400
+ if (!row) {
5401
+ throw new Error(`Memory facet not found after write: ${id}`);
5402
+ }
5403
+ return rowToFacet(row);
5404
+ }
5405
+ findByUniqueKey(input) {
5406
+ const row = sqliteGet(
5407
+ this.db,
5408
+ `SELECT * FROM memory_facets
5409
+ WHERE target_type = ? AND target_id = ? AND dimension = ? AND value = ? AND source = ? AND project_hash = ?`,
5410
+ [
5411
+ input.targetType,
5412
+ input.targetId,
5413
+ input.dimension,
5414
+ input.value,
5415
+ input.source,
5416
+ projectHashToStorage(input.projectHash)
5417
+ ]
5418
+ );
5419
+ return row ? rowToFacet(row) : null;
5420
+ }
5421
+ async auditAssignment(input, before, after) {
5422
+ await writeGovernanceAuditEntry(this.db, {
5423
+ operation: "facet_tag",
5424
+ actor: input.actor ?? "cml-core",
5425
+ projectHash: input.projectHash,
5426
+ targetType: input.targetType,
5427
+ targetId: input.targetId,
5428
+ beforeJson: before ? facetToAuditJson(before) : void 0,
5429
+ afterJson: facetToAuditJson(after),
5430
+ sourceEventIds: input.evidenceEventIds
5431
+ });
5432
+ }
5433
+ querySql(query) {
5434
+ const clauses = [];
5435
+ const params = [];
5436
+ if (query.targetType) {
5437
+ clauses.push("target_type = ?");
5438
+ params.push(query.targetType);
5439
+ }
5440
+ if (query.targetId) {
5441
+ clauses.push("target_id = ?");
5442
+ params.push(query.targetId);
5443
+ }
5444
+ if (query.dimension) {
5445
+ clauses.push("dimension = ?");
5446
+ params.push(query.dimension);
5447
+ }
5448
+ if (query.value) {
5449
+ clauses.push("value = ?");
5450
+ params.push(query.value);
5451
+ }
5452
+ if (query.source) {
5453
+ clauses.push("source = ?");
5454
+ params.push(query.source);
5455
+ }
5456
+ if (query.projectHash) {
5457
+ clauses.push("project_hash = ?");
5458
+ params.push(query.projectHash);
5459
+ }
5460
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
5461
+ params.push(query.limit);
5462
+ return {
5463
+ sql: `SELECT * FROM memory_facets ${where} ORDER BY confidence DESC, updated_at DESC LIMIT ?`,
5464
+ params
5465
+ };
5466
+ }
5467
+ removeSql(input) {
5468
+ const clauses = [
5469
+ "target_type = ?",
5470
+ "target_id = ?",
5471
+ "dimension = ?",
5472
+ "value = ?",
5473
+ "source = ?",
5474
+ "project_hash = ?"
5475
+ ];
5476
+ const params = [
5477
+ input.targetType,
5478
+ input.targetId,
5479
+ input.dimension,
5480
+ input.value,
5481
+ input.source,
5482
+ projectHashToStorage(input.projectHash)
5483
+ ];
5484
+ return {
5485
+ sql: `DELETE FROM memory_facets WHERE ${clauses.join(" AND ")}`,
5486
+ params
5487
+ };
5488
+ }
5489
+ };
5490
+
5491
+ // src/core/operations/graph-path-service.ts
5492
+ var DEFAULT_WEIGHT = 0.5;
5493
+ var MAX_HOPS = 2;
5494
+ var DEFAULT_MAX_RESULTS = 20;
5495
+ var MAX_RESULTS = 100;
5496
+ var GraphPathService = class {
5497
+ constructor(db) {
5498
+ this.db = db;
5499
+ }
5500
+ expand(input) {
5501
+ const graph = this.loadGraph(input.direction ?? "both");
5502
+ const effectiveMaxHops = normalizeMaxHops(input.maxHops);
5503
+ const maxResults = normalizeMaxResults(input.maxResults);
5504
+ const startNodes = input.startNodes.map((node) => graph.node(node));
5505
+ const startKeys = new Set(input.startNodes.map(nodeKey));
5506
+ const bestByTarget = /* @__PURE__ */ new Map();
5507
+ const queue = startNodes.map((node) => ({
5508
+ key: nodeKey(node),
5509
+ hops: 0,
5510
+ totalCost: 0,
5511
+ steps: [],
5512
+ visited: /* @__PURE__ */ new Set([nodeKey(node)])
5513
+ }));
5514
+ while (queue.length > 0) {
5515
+ queue.sort((a, b) => a.totalCost - b.totalCost || a.hops - b.hops || a.key.localeCompare(b.key));
5516
+ const current = queue.shift();
5517
+ if (current.hops >= effectiveMaxHops)
5518
+ continue;
5519
+ for (const edge of graph.adjacency.get(current.key) ?? []) {
5520
+ if (current.visited.has(edge.toKey))
5521
+ continue;
5522
+ const nextHops = current.hops + 1;
5523
+ const nextTotalCost = current.totalCost + edge.step.cost;
5524
+ const nextSteps = [...current.steps, edge.step];
5525
+ const nextSignature = pathSignature(nextSteps);
5526
+ const existing = bestByTarget.get(edge.toKey);
5527
+ if (!existing || isBetterPath(nextTotalCost, nextHops, nextSignature, existing)) {
5528
+ if (!startKeys.has(edge.toKey)) {
5529
+ bestByTarget.set(edge.toKey, { hops: nextHops, totalCost: nextTotalCost, signature: nextSignature, steps: nextSteps });
5530
+ }
5531
+ const nextVisited = new Set(current.visited);
5532
+ nextVisited.add(edge.toKey);
5533
+ queue.push({
5534
+ key: edge.toKey,
5535
+ hops: nextHops,
5536
+ totalCost: nextTotalCost,
5537
+ steps: nextSteps,
5538
+ visited: nextVisited
5539
+ });
5540
+ }
5541
+ }
5542
+ }
5543
+ const paths = Array.from(bestByTarget.entries()).map(([key, path14]) => ({
5544
+ target: graph.node(nodeFromKey(key)),
5545
+ hops: path14.hops,
5546
+ totalCost: path14.totalCost,
5547
+ scoreContribution: path14.totalCost > 0 ? 1 / path14.totalCost : 0,
5548
+ steps: path14.steps
5549
+ })).sort((a, b) => b.scoreContribution - a.scoreContribution || a.hops - b.hops || a.target.name.localeCompare(b.target.name)).slice(0, maxResults);
5550
+ return { startNodes, effectiveMaxHops, paths };
5551
+ }
5552
+ loadGraph(direction) {
5553
+ const entityLabels = new Map(
5554
+ sqliteAll(this.db, `SELECT entity_id, title FROM entities WHERE status = 'active'`).map((row) => [row.entity_id, row.title])
5555
+ );
5556
+ const labelNode = (node) => ({
5557
+ ...node,
5558
+ name: node.type === "entity" ? entityLabels.get(node.id) ?? node.id : node.id
5559
+ });
5560
+ const adjacency = /* @__PURE__ */ new Map();
5561
+ const edges = sqliteAll(
5562
+ this.db,
5563
+ `SELECT edge_id, src_type, src_id, rel_type, dst_type, dst_id, meta_json FROM edges`
5564
+ );
5565
+ for (const edge of edges) {
5566
+ const src = labelNode({ type: edge.src_type, id: edge.src_id });
5567
+ const dst = labelNode({ type: edge.dst_type, id: edge.dst_id });
5568
+ const weight = edgeWeight(edge.meta_json);
5569
+ const cost = 1 / weight;
5570
+ const baseStep = {
5571
+ edgeId: edge.edge_id,
5572
+ relationType: edge.rel_type,
5573
+ from: src,
5574
+ to: dst,
5575
+ weight,
5576
+ cost,
5577
+ scoreContribution: weight
5578
+ };
5579
+ if (direction === "outgoing" || direction === "both") {
5580
+ addTraversal(adjacency, nodeKey(src), {
5581
+ toKey: nodeKey(dst),
5582
+ step: { ...baseStep, direction: "outgoing" }
5583
+ });
5584
+ }
5585
+ if (direction === "incoming" || direction === "both") {
5586
+ addTraversal(adjacency, nodeKey(dst), {
5587
+ toKey: nodeKey(src),
5588
+ step: { ...baseStep, direction: "incoming" }
5589
+ });
5590
+ }
5591
+ }
5592
+ return { adjacency, node: labelNode };
5593
+ }
5594
+ };
5595
+ function addTraversal(adjacency, fromKey, edge) {
5596
+ const edges = adjacency.get(fromKey) ?? [];
5597
+ edges.push(edge);
5598
+ adjacency.set(fromKey, edges);
5599
+ }
5600
+ function normalizeMaxHops(maxHops) {
5601
+ if (maxHops === void 0)
5602
+ return 1;
5603
+ if (!Number.isFinite(maxHops))
5604
+ return MAX_HOPS;
5605
+ return Math.min(Math.max(0, Math.trunc(maxHops)), MAX_HOPS);
5606
+ }
5607
+ function normalizeMaxResults(maxResults) {
5608
+ if (maxResults === void 0)
5609
+ return DEFAULT_MAX_RESULTS;
5610
+ if (!Number.isFinite(maxResults))
5611
+ return DEFAULT_MAX_RESULTS;
5612
+ return Math.min(Math.max(0, Math.trunc(maxResults)), MAX_RESULTS);
5613
+ }
5614
+ function isBetterPath(totalCost, hops, signature, existing) {
5615
+ return totalCost < existing.totalCost || totalCost === existing.totalCost && hops < existing.hops || totalCost === existing.totalCost && hops === existing.hops && signature < existing.signature;
5616
+ }
5617
+ function pathSignature(steps) {
5618
+ return steps.map((step) => `${step.edgeId}:${step.direction}:${nodeKey(step.from)}>${nodeKey(step.to)}`).join("|");
5619
+ }
5620
+ function edgeWeight(metaJson) {
5621
+ const meta = parseMeta(metaJson);
5622
+ const raw = meta.weight;
5623
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0)
5624
+ return raw;
5625
+ if (typeof raw === "string") {
5626
+ const parsed = Number(raw);
5627
+ if (Number.isFinite(parsed) && parsed > 0)
5628
+ return parsed;
5629
+ }
5630
+ return DEFAULT_WEIGHT;
5631
+ }
5632
+ function parseMeta(metaJson) {
5633
+ if (!metaJson)
5634
+ return {};
5635
+ try {
5636
+ const parsed = JSON.parse(metaJson);
5637
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
5638
+ } catch {
5639
+ return {};
5640
+ }
5641
+ }
5642
+ function nodeKey(node) {
5643
+ return `${node.type}:${node.id}`;
5644
+ }
5645
+ function nodeFromKey(key) {
5646
+ const index = key.indexOf(":");
5647
+ if (index === -1)
5648
+ return { type: "entity", id: key };
5649
+ return { type: key.slice(0, index), id: key.slice(index + 1) };
5650
+ }
5651
+
5652
+ // src/core/operations/query-entity-extractor.ts
5653
+ var DEFAULT_MAX_CANDIDATES = 20;
5654
+ var MAX_CANDIDATES = 100;
5655
+ var MAX_CANDIDATE_TEXT_LENGTH = 200;
5656
+ var SOURCE_PRIORITY = {
5657
+ entity_alias: 0,
5658
+ quoted: 1,
5659
+ file_path: 2,
5660
+ package_identifier: 3,
5661
+ capitalized_term: 4
5662
+ };
5663
+ var SOURCE_CONFIDENCE = {
5664
+ entity_alias: 0.95,
5665
+ quoted: 0.85,
5666
+ file_path: 0.8,
5667
+ package_identifier: 0.75,
5668
+ capitalized_term: 0.6
5669
+ };
5670
+ var SENTENCE_START_STOPWORDS = /* @__PURE__ */ new Set([
5671
+ "A",
5672
+ "An",
5673
+ "And",
5674
+ "Are",
5675
+ "Can",
5676
+ "Compare",
5677
+ "Does",
5678
+ "Explain",
5679
+ "Find",
5680
+ "How",
5681
+ "I",
5682
+ "If",
5683
+ "In",
5684
+ "Is",
5685
+ "List",
5686
+ "Please",
5687
+ "Should",
5688
+ "Show",
5689
+ "Tell",
5690
+ "The",
5691
+ "This",
5692
+ "Use",
5693
+ "What",
5694
+ "When",
5695
+ "Where",
5696
+ "Which",
5697
+ "Why",
5698
+ "With"
5699
+ ]);
5700
+ var QueryEntityExtractor = class {
5701
+ constructor(db) {
5702
+ this.db = db;
5703
+ }
5704
+ extract(query, options = {}) {
5705
+ const maxCandidates = normalizeMaxCandidates(options.maxCandidates);
5706
+ const candidates = [];
5707
+ const quotedRanges = this.extractQuoted(query, candidates);
5708
+ if (options.includeAliases !== false) {
5709
+ this.extractKnownAliases(query, candidates);
5710
+ }
5711
+ this.extractFilePaths(query, candidates);
5712
+ this.extractPackageIdentifiers(query, candidates);
5713
+ this.extractCapitalizedTerms(query, candidates, quotedRanges);
5714
+ return {
5715
+ query,
5716
+ candidates: dedupeAndSort(candidates).slice(0, maxCandidates).map(stripPriority)
5717
+ };
5718
+ }
5719
+ extractQuoted(query, candidates) {
5720
+ const ranges = [];
5721
+ const regex = /(["'`])((?:(?!\1)[^\n]){2,200})\1/g;
5722
+ let match;
5723
+ while ((match = regex.exec(query)) !== null) {
5724
+ const text = cleanCandidateText(match[2] ?? "");
5725
+ if (!isUsefulCandidate(text))
5726
+ continue;
5727
+ const start = match.index + 1;
5728
+ const end = start + text.length;
5729
+ ranges.push([match.index, match.index + match[0].length]);
5730
+ pushCandidate(candidates, {
5731
+ text,
5732
+ source: "quoted",
5733
+ start,
5734
+ end
5735
+ });
5736
+ }
5737
+ return ranges;
5738
+ }
5739
+ extractKnownAliases(query, candidates) {
5740
+ if (!this.db)
5741
+ return;
5742
+ const rows = sqliteAll(
5743
+ this.db,
5744
+ `SELECT
5745
+ a.entity_id,
5746
+ a.entity_type,
5747
+ a.canonical_key AS alias_key,
5748
+ e.canonical_key AS entity_canonical_key,
5749
+ e.title
5750
+ FROM entity_aliases a
5751
+ JOIN entities e ON e.entity_id = a.entity_id
5752
+ WHERE e.status = 'active'
5753
+ ORDER BY e.title COLLATE NOCASE, a.canonical_key COLLATE NOCASE`
5754
+ );
5755
+ const normalizedQuery = normalizeForContainment(query);
5756
+ const seenAliases = /* @__PURE__ */ new Set();
5757
+ for (const row of rows) {
5758
+ const aliasLabels = uniqueStrings([
5759
+ row.title,
5760
+ aliasLabelFromCanonicalKey(row.alias_key),
5761
+ aliasLabelFromCanonicalKey(row.entity_canonical_key)
5762
+ ]).filter(isUsefulCandidate);
5763
+ for (const alias of aliasLabels) {
5764
+ const normalizedAlias = normalizeForContainment(alias);
5765
+ if (!normalizedAlias || !containsPhrase(normalizedQuery, normalizedAlias))
5766
+ continue;
5767
+ const aliasKey = `${row.entity_id}:${normalizedAlias}`;
5768
+ if (seenAliases.has(aliasKey))
5769
+ continue;
5770
+ seenAliases.add(aliasKey);
5771
+ const range = findRange(query, alias);
5772
+ pushCandidate(candidates, {
5773
+ text: row.title,
5774
+ source: "entity_alias",
5775
+ start: range.start,
5776
+ end: range.end,
5777
+ entityId: row.entity_id,
5778
+ entityType: row.entity_type,
5779
+ canonicalKey: row.entity_canonical_key,
5780
+ matchedAlias: normalizedAlias
5781
+ });
5782
+ }
5783
+ }
5784
+ }
5785
+ extractFilePaths(query, candidates) {
5786
+ const regex = /(^|[\s([{<])((?:\.{1,2}\/|~\/|\/)?(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\.[A-Za-z0-9][A-Za-z0-9._-]*)(?=$|[\s)\]},>`.,;:!?])/g;
5787
+ let match;
5788
+ while ((match = regex.exec(query)) !== null) {
5789
+ const text = cleanCandidateText(match[2] ?? "");
5790
+ if (!isUsefulCandidate(text))
5791
+ continue;
5792
+ const start = match.index + (match[1]?.length ?? 0);
5793
+ pushCandidate(candidates, {
5794
+ text,
5795
+ source: "file_path",
5796
+ start,
5797
+ end: start + text.length
5798
+ });
5799
+ }
5800
+ }
5801
+ extractPackageIdentifiers(query, candidates) {
5802
+ 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;
5803
+ let match;
5804
+ while ((match = regex.exec(query)) !== null) {
5805
+ const text = cleanCandidateText(match[2] ?? "");
5806
+ if (!isUsefulCandidate(text) || text.includes("/.") || text.includes("./"))
5807
+ continue;
5808
+ const start = match.index + (match[1]?.length ?? 0);
5809
+ pushCandidate(candidates, {
5810
+ text,
5811
+ source: "package_identifier",
5812
+ start,
5813
+ end: start + text.length
5814
+ });
5815
+ }
5816
+ }
5817
+ extractCapitalizedTerms(query, candidates, ignoredRanges) {
5818
+ const tokens = collectCapitalizedTokens(query).filter((token) => !isInsideAnyRange(token.start, ignoredRanges)).filter((token) => !SENTENCE_START_STOPWORDS.has(token.text));
5819
+ const groups = [];
5820
+ let current = [];
5821
+ for (const token of tokens) {
5822
+ const previous = current[current.length - 1];
5823
+ if (previous && query.slice(previous.end, token.start).match(/^\s+$/)) {
5824
+ current.push(token);
5825
+ } else {
5826
+ if (current.length > 0)
5827
+ groups.push(current);
5828
+ current = [token];
5829
+ }
5830
+ }
5831
+ if (current.length > 0)
5832
+ groups.push(current);
5833
+ for (const group of groups) {
5834
+ if (group.length === 1 && !isStrongSingleCapitalized(group[0].text))
5835
+ continue;
5836
+ const start = group[0].start;
5837
+ const end = group[group.length - 1].end;
5838
+ const text = query.slice(start, end);
5839
+ if (!isUsefulCandidate(text))
5840
+ continue;
5841
+ pushCandidate(candidates, {
5842
+ text,
5843
+ source: "capitalized_term",
5844
+ start,
5845
+ end
5846
+ });
5847
+ }
5848
+ }
5849
+ };
5850
+ function collectCapitalizedTokens(query) {
5851
+ const regex = /\b(?:[A-Z]{2,}[A-Z0-9]*|[A-Z][A-Za-z0-9]*(?:[._-][A-Za-z0-9]+)*)\b/g;
5852
+ const tokens = [];
5853
+ let match;
5854
+ while ((match = regex.exec(query)) !== null) {
5855
+ tokens.push({ text: match[0], start: match.index, end: match.index + match[0].length });
5856
+ }
5857
+ return tokens;
5858
+ }
5859
+ function pushCandidate(candidates, input) {
5860
+ const text = cleanCandidateText(input.text);
5861
+ if (!isUsefulCandidate(text))
5862
+ return;
5863
+ const source = input.source;
5864
+ candidates.push({
5865
+ ...input,
5866
+ text,
5867
+ normalized: normalizeCandidate(text),
5868
+ confidence: input.confidence ?? SOURCE_CONFIDENCE[source],
5869
+ priority: SOURCE_PRIORITY[source]
5870
+ });
5871
+ }
5872
+ function dedupeAndSort(candidates) {
5873
+ const sorted = [...candidates].sort(compareCandidates);
5874
+ const seenAliasKeys = /* @__PURE__ */ new Set();
5875
+ const seenNormalized = /* @__PURE__ */ new Set();
5876
+ const result = [];
5877
+ for (const candidate of sorted) {
5878
+ if (candidate.source === "entity_alias") {
5879
+ const aliasKey = `alias:${candidate.entityId ?? ""}:${normalizeCandidate(candidate.matchedAlias ?? candidate.text)}`;
5880
+ if (seenAliasKeys.has(aliasKey))
5881
+ continue;
5882
+ seenAliasKeys.add(aliasKey);
5883
+ seenNormalized.add(candidate.normalized);
5884
+ result.push(candidate);
5885
+ continue;
5886
+ }
5887
+ if (seenNormalized.has(candidate.normalized))
5888
+ continue;
5889
+ seenNormalized.add(candidate.normalized);
5890
+ result.push(candidate);
5891
+ }
5892
+ return result.sort(compareCandidates);
5893
+ }
5894
+ function compareCandidates(a, b) {
5895
+ 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 ?? "");
5896
+ }
5897
+ function compareStrings(a, b) {
5898
+ if (a === b)
5899
+ return 0;
5900
+ return a < b ? -1 : 1;
5901
+ }
5902
+ function stripPriority(candidate) {
5903
+ const { priority: _priority, ...publicCandidate } = candidate;
5904
+ return publicCandidate;
5905
+ }
5906
+ function normalizeMaxCandidates(maxCandidates) {
5907
+ if (maxCandidates === void 0)
5908
+ return DEFAULT_MAX_CANDIDATES;
5909
+ if (!Number.isFinite(maxCandidates))
5910
+ return DEFAULT_MAX_CANDIDATES;
5911
+ return Math.min(Math.max(0, Math.trunc(maxCandidates)), MAX_CANDIDATES);
5912
+ }
5913
+ function cleanCandidateText(text) {
5914
+ return text.normalize("NFKC").replace(/\s+/g, " ").trim().replace(/[.,;:!?]+$/g, "");
5915
+ }
5916
+ function normalizeCandidate(text) {
5917
+ return cleanCandidateText(text).toLowerCase();
5918
+ }
5919
+ function normalizeForContainment(text) {
5920
+ return text.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}@/._-]+/gu, " ").replace(/[._-]+/g, " ").replace(/\s+/g, " ").trim();
5921
+ }
5922
+ function containsPhrase(normalizedHaystack, normalizedNeedle) {
5923
+ return ` ${normalizedHaystack} `.includes(` ${normalizedNeedle} `);
5924
+ }
5925
+ function aliasLabelFromCanonicalKey(canonicalKey) {
5926
+ const raw = canonicalKey.includes(":") ? canonicalKey.slice(canonicalKey.lastIndexOf(":") + 1) : canonicalKey;
5927
+ return raw.replace(/[._-]+/g, " ").replace(/\s+/g, " ").trim();
5928
+ }
5929
+ function findRange(query, alias) {
5930
+ const normalizedAlias = normalizeForContainment(alias);
5931
+ const directIndex = query.toLowerCase().indexOf(alias.toLowerCase());
5932
+ if (directIndex >= 0)
5933
+ return { start: directIndex, end: directIndex + alias.length };
5934
+ const normalizedQuery = normalizeForContainment(query);
5935
+ const normalizedIndex = normalizedQuery.indexOf(normalizedAlias);
5936
+ if (normalizedIndex < 0)
5937
+ return { start: 0, end: 0 };
5938
+ const queryLower = query.toLowerCase();
5939
+ const words = normalizedAlias.split(" ").filter(Boolean);
5940
+ if (words.length === 0)
5941
+ return { start: 0, end: 0 };
5942
+ const first = queryLower.indexOf(words[0]);
5943
+ const lastWord = words[words.length - 1];
5944
+ const last = queryLower.indexOf(lastWord, first >= 0 ? first : 0);
5945
+ if (first >= 0 && last >= 0)
5946
+ return { start: first, end: last + lastWord.length };
5947
+ return { start: normalizedIndex, end: normalizedIndex + normalizedAlias.length };
5948
+ }
5949
+ function isUsefulCandidate(text) {
5950
+ const cleaned = cleanCandidateText(text);
5951
+ return cleaned.length >= 2 && cleaned.length <= MAX_CANDIDATE_TEXT_LENGTH && /[\p{L}\p{N}]/u.test(cleaned);
5952
+ }
5953
+ function isInsideAnyRange(index, ranges) {
5954
+ return ranges.some(([start, end]) => index >= start && index < end);
5955
+ }
5956
+ function isStrongSingleCapitalized(text) {
5957
+ if (/^[A-Z]{2,}[A-Z0-9]*$/.test(text))
5958
+ return true;
5959
+ if (/^[A-Z][a-z]+[A-Z][A-Za-z0-9]*$/.test(text))
5960
+ return true;
5961
+ return text.length >= 4 && !SENTENCE_START_STOPWORDS.has(text);
5962
+ }
5963
+ function uniqueStrings(values) {
5964
+ const seen = /* @__PURE__ */ new Set();
5965
+ const result = [];
5966
+ for (const value of values) {
5967
+ const key = normalizeForContainment(value);
5968
+ if (!key || seen.has(key))
5969
+ continue;
5970
+ seen.add(key);
5971
+ result.push(value);
5972
+ }
5973
+ return result;
5974
+ }
5975
+
4906
5976
  // src/core/retrieval-quality.ts
4907
5977
  var COMMAND_ARTIFACT_PATTERNS = [
4908
5978
  /<\/?(?:local-command-(?:stdout|stderr)|command-(?:name|message))\b/i,
@@ -5265,6 +6335,7 @@ var Retriever = class {
5265
6335
  sharedVectorStore;
5266
6336
  graduation;
5267
6337
  queryRewriter;
6338
+ queryGraphExpansionEnabled;
5268
6339
  constructor(eventStore, vectorStore, embedder, matcher, sharedOptions) {
5269
6340
  this.eventStore = eventStore;
5270
6341
  this.vectorStore = vectorStore;
@@ -5272,6 +6343,7 @@ var Retriever = class {
5272
6343
  this.matcher = matcher;
5273
6344
  this.sharedStore = sharedOptions?.sharedStore;
5274
6345
  this.sharedVectorStore = sharedOptions?.sharedVectorStore;
6346
+ this.queryGraphExpansionEnabled = sharedOptions?.queryGraphExpansionEnabled === true;
5275
6347
  }
5276
6348
  setGraduationPipeline(graduation) {
5277
6349
  this.graduation = graduation;
@@ -5317,7 +6389,8 @@ var Retriever = class {
5317
6389
  graphHop: opts.graphHop,
5318
6390
  projectScopeMode: opts.projectScopeMode,
5319
6391
  projectHash: opts.projectHash,
5320
- allowedProjectHashes: opts.allowedProjectHashes
6392
+ allowedProjectHashes: opts.allowedProjectHashes,
6393
+ facets: opts.facets
5321
6394
  });
5322
6395
  fallbackTrace.push(`stage:primary:${primaryStrategy}`);
5323
6396
  if (fallbackEnabled && this.shouldFallback(current.matchResult, current.results) && primaryStrategy !== "deep") {
@@ -5334,7 +6407,8 @@ var Retriever = class {
5334
6407
  graphHop: opts.graphHop,
5335
6408
  projectScopeMode: opts.projectScopeMode,
5336
6409
  projectHash: opts.projectHash,
5337
- allowedProjectHashes: opts.allowedProjectHashes
6410
+ allowedProjectHashes: opts.allowedProjectHashes,
6411
+ facets: opts.facets
5338
6412
  });
5339
6413
  fallbackTrace.push("fallback:deep");
5340
6414
  }
@@ -5352,7 +6426,8 @@ var Retriever = class {
5352
6426
  graphHop: opts.graphHop,
5353
6427
  projectScopeMode: opts.projectScopeMode,
5354
6428
  projectHash: opts.projectHash,
5355
- allowedProjectHashes: opts.allowedProjectHashes
6429
+ allowedProjectHashes: opts.allowedProjectHashes,
6430
+ facets: opts.facets
5356
6431
  });
5357
6432
  fallbackTrace.push("fallback:scope-expanded");
5358
6433
  }
@@ -5362,7 +6437,8 @@ var Retriever = class {
5362
6437
  scope: opts.scope,
5363
6438
  projectScopeMode: opts.projectScopeMode,
5364
6439
  projectHash: opts.projectHash,
5365
- allowedProjectHashes: opts.allowedProjectHashes
6440
+ allowedProjectHashes: opts.allowedProjectHashes,
6441
+ facets: opts.facets
5366
6442
  });
5367
6443
  const filteredSummary = this.applyQualityFilters(scopedSummary, {
5368
6444
  query,
@@ -5383,20 +6459,8 @@ var Retriever = class {
5383
6459
  totalTokens: this.estimateTokens(context),
5384
6460
  context,
5385
6461
  fallbackTrace,
5386
- selectedDebug: current.results.slice(0, opts.topK).map((r) => ({
5387
- eventId: r.eventId,
5388
- score: r.score,
5389
- semanticScore: r.semanticScore,
5390
- lexicalScore: r.lexicalScore,
5391
- recencyScore: r.recencyScore
5392
- })),
5393
- candidateDebug: (current.candidateResults || []).slice(0, Math.max(opts.topK * 3, 20)).map((r) => ({
5394
- eventId: r.eventId,
5395
- score: r.score,
5396
- semanticScore: r.semanticScore,
5397
- lexicalScore: r.lexicalScore,
5398
- recencyScore: r.recencyScore
5399
- })),
6462
+ selectedDebug: current.results.slice(0, opts.topK).map((r) => this.debugDetailForResult(r)),
6463
+ candidateDebug: (current.candidateResults || []).slice(0, Math.max(opts.topK * 3, 20)).map((r) => this.debugDetailForResult(r)),
5400
6464
  rawQueryText: current.queryRewriteKind ? query : void 0,
5401
6465
  effectiveQueryText: current.effectiveQueryText,
5402
6466
  queryRewriteKind: current.queryRewriteKind
@@ -5464,7 +6528,9 @@ var Retriever = class {
5464
6528
  }
5465
6529
  }
5466
6530
  const expandedResults = input.graphHop?.enabled === false ? initialResults : await this.expandGraphHops(initialResults, {
5467
- maxHops: Math.max(1, input.graphHop?.maxHops ?? 1),
6531
+ query,
6532
+ queryGraphEnabled: this.queryGraphExpansionEnabled,
6533
+ maxHops: clampGraphHops(input.graphHop?.maxHops ?? 1),
5468
6534
  hopPenalty: Math.max(0, input.graphHop?.hopPenalty ?? 0.08),
5469
6535
  limit: input.topK * 4
5470
6536
  });
@@ -5473,7 +6539,8 @@ var Retriever = class {
5473
6539
  scope: input.scope,
5474
6540
  projectScopeMode: input.projectScopeMode,
5475
6541
  projectHash: input.projectHash,
5476
- allowedProjectHashes: input.allowedProjectHashes
6542
+ allowedProjectHashes: input.allowedProjectHashes,
6543
+ facets: input.facets
5477
6544
  });
5478
6545
  const qualityFiltered = this.applyQualityFilters(filtered, {
5479
6546
  query,
@@ -5488,9 +6555,13 @@ var Retriever = class {
5488
6555
  if (isCurrentStateQuery(options.query)) {
5489
6556
  filtered = filtered.filter((result) => !isStaleOrSupersededContent(result.content));
5490
6557
  }
5491
- filtered = filtered.filter((result) => hasDiscriminativeTermOverlap(options.query, result.content));
6558
+ filtered = filtered.filter(
6559
+ (result) => this.isGraphPathResult(result) || hasDiscriminativeTermOverlap(options.query, result.content)
6560
+ );
5492
6561
  if (shouldApplyTechnicalGuard(options.query)) {
5493
- filtered = filtered.filter((result) => hasTechnicalTermOverlap(options.query, result.content));
6562
+ filtered = filtered.filter(
6563
+ (result) => this.isGraphPathResult(result) || hasTechnicalTermOverlap(options.query, result.content)
6564
+ );
5494
6565
  }
5495
6566
  if (filtered.length <= 2)
5496
6567
  return filtered;
@@ -5553,7 +6624,61 @@ var Retriever = class {
5553
6624
  if (frontier.length === 0 || byId.size >= opts.limit)
5554
6625
  break;
5555
6626
  }
5556
- return [...byId.values()].sort((a, b) => b.score - a.score).slice(0, opts.limit);
6627
+ if (opts.queryGraphEnabled) {
6628
+ await this.expandQueryGraphPaths(opts.query, byId, opts);
6629
+ }
6630
+ return [...byId.values()].sort((a, b) => b.score - a.score || compareStable(a.eventId, b.eventId)).slice(0, opts.limit);
6631
+ }
6632
+ async expandQueryGraphPaths(query, byId, opts) {
6633
+ if (!query.trim() || !this.eventStore.getDatabase)
6634
+ return;
6635
+ try {
6636
+ const db = this.eventStore.getDatabase();
6637
+ const extraction = new QueryEntityExtractor(db).extract(query, {
6638
+ maxCandidates: Math.min(8, opts.limit),
6639
+ includeAliases: true
6640
+ });
6641
+ const startCandidates = extraction.candidates.filter((candidate) => candidate.entityId).slice(0, 8);
6642
+ const startNodes = uniqueEntityStartNodes(startCandidates);
6643
+ if (startNodes.length === 0)
6644
+ return;
6645
+ const expansion = new GraphPathService(db).expand({
6646
+ startNodes: startNodes.map((node) => ({ type: "entity", id: node.entityId })),
6647
+ maxHops: opts.maxHops,
6648
+ maxResults: opts.limit,
6649
+ direction: "both"
6650
+ });
6651
+ const titleByEntityId = new Map(startNodes.map((node) => [node.entityId, node.title]));
6652
+ for (const path14 of expansion.paths) {
6653
+ if (path14.target.type !== "event")
6654
+ continue;
6655
+ const target = await this.eventStore.getEvent(path14.target.id);
6656
+ if (!target)
6657
+ continue;
6658
+ const graphPath = toRetrievalGraphPathDebug(path14, titleByEntityId);
6659
+ const score = graphPathScore(path14, opts.hopPenalty);
6660
+ const existing = byId.get(target.id);
6661
+ const graphPaths = mergeGraphPaths(existing?.graphPaths ?? [], [graphPath]);
6662
+ const row = {
6663
+ id: existing?.id ?? `graph-path-${path14.hops}-${target.id}`,
6664
+ eventId: target.id,
6665
+ content: target.content,
6666
+ score: Math.max(existing?.score ?? 0, score),
6667
+ sessionId: target.sessionId,
6668
+ eventType: target.eventType,
6669
+ timestamp: target.timestamp.toISOString(),
6670
+ semanticScore: existing?.semanticScore,
6671
+ lexicalScore: existing?.lexicalScore,
6672
+ recencyScore: existing?.recencyScore,
6673
+ facetMatches: existing?.facetMatches,
6674
+ graphPaths
6675
+ };
6676
+ byId.set(row.eventId, row);
6677
+ if (byId.size >= opts.limit)
6678
+ break;
6679
+ }
6680
+ } catch {
6681
+ }
5557
6682
  }
5558
6683
  shouldFallback(matchResult, results) {
5559
6684
  if (results.length === 0)
@@ -5644,12 +6769,13 @@ var Retriever = class {
5644
6769
  async applyScopeFilters(results, options) {
5645
6770
  const scope = options?.scope;
5646
6771
  const projectScopeMode = options?.projectScopeMode ?? "global";
6772
+ const facetFilters = this.normalizeFacetFilters(options?.facets);
5647
6773
  const allowedProjectHashes = new Set(
5648
6774
  [options?.projectHash, ...options?.allowedProjectHashes || []].filter(
5649
6775
  (value) => typeof value === "string" && value.length > 0
5650
6776
  )
5651
6777
  );
5652
- if (!scope && projectScopeMode === "global")
6778
+ if (!scope && projectScopeMode === "global" && facetFilters === null)
5653
6779
  return results;
5654
6780
  const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
5655
6781
  const filtered = [];
@@ -5675,14 +6801,83 @@ var Retriever = class {
5675
6801
  const projectHash = this.extractProjectHash(event.metadata);
5676
6802
  filtered.push({ result, projectHash });
5677
6803
  }
6804
+ let scopedResults;
5678
6805
  if (projectScopeMode === "global" || allowedProjectHashes.size === 0) {
5679
- return filtered.map((x) => x.result);
6806
+ scopedResults = filtered.map((x) => x.result);
6807
+ } else {
6808
+ const projectMatched = filtered.filter((x) => x.projectHash && allowedProjectHashes.has(x.projectHash));
6809
+ scopedResults = projectScopeMode === "strict" ? projectMatched.map((x) => x.result) : (projectMatched.length > 0 ? projectMatched : filtered).map((x) => x.result);
5680
6810
  }
5681
- const projectMatched = filtered.filter((x) => x.projectHash && allowedProjectHashes.has(x.projectHash));
5682
- if (projectScopeMode === "strict") {
5683
- return projectMatched.map((x) => x.result);
6811
+ return this.applyFacetFilters(scopedResults, {
6812
+ facets: facetFilters,
6813
+ projectHash: options?.projectHash
6814
+ });
6815
+ }
6816
+ normalizeFacetFilters(facets) {
6817
+ if (!facets || facets.length === 0)
6818
+ return null;
6819
+ const normalized = [];
6820
+ for (const facet of facets) {
6821
+ const parsedDimension = FacetDimensionSchema.safeParse(facet.dimension);
6822
+ const value = typeof facet.value === "string" ? facet.value.trim() : "";
6823
+ if (!parsedDimension.success || !value)
6824
+ return [];
6825
+ normalized.push({ dimension: parsedDimension.data, value });
6826
+ }
6827
+ return normalized;
6828
+ }
6829
+ async applyFacetFilters(results, options) {
6830
+ if (options.facets === null)
6831
+ return results;
6832
+ if (options.facets.length === 0)
6833
+ return [];
6834
+ if (!options.projectHash)
6835
+ return [];
6836
+ if (!this.eventStore.getDatabase)
6837
+ return [];
6838
+ const repo = new FacetRepository(this.eventStore.getDatabase());
6839
+ const filtered = [];
6840
+ for (const result of results) {
6841
+ const matches = [];
6842
+ let matchedAll = true;
6843
+ for (const facet of options.facets) {
6844
+ const rows = await repo.query({
6845
+ targetType: "event",
6846
+ targetId: result.eventId,
6847
+ dimension: facet.dimension,
6848
+ value: facet.value,
6849
+ projectHash: options.projectHash
6850
+ });
6851
+ if (rows.length === 0) {
6852
+ matchedAll = false;
6853
+ break;
6854
+ }
6855
+ matches.push(facet);
6856
+ }
6857
+ if (matchedAll) {
6858
+ filtered.push({ ...result, facetMatches: matches });
6859
+ }
5684
6860
  }
5685
- return (projectMatched.length > 0 ? projectMatched : filtered).map((x) => x.result);
6861
+ return filtered;
6862
+ }
6863
+ debugDetailForResult(result) {
6864
+ const detail = {
6865
+ eventId: result.eventId,
6866
+ score: result.score,
6867
+ semanticScore: result.semanticScore,
6868
+ lexicalScore: result.lexicalScore,
6869
+ recencyScore: result.recencyScore
6870
+ };
6871
+ if (result.facetMatches && result.facetMatches.length > 0) {
6872
+ detail.facetMatches = result.facetMatches;
6873
+ }
6874
+ if (result.graphPaths && result.graphPaths.length > 0) {
6875
+ detail.graphPaths = result.graphPaths;
6876
+ }
6877
+ return detail;
6878
+ }
6879
+ isGraphPathResult(result) {
6880
+ return (result.graphPaths || []).length > 0;
5686
6881
  }
5687
6882
  extractProjectHash(metadata) {
5688
6883
  if (!metadata || typeof metadata !== "object")
@@ -5826,8 +7021,60 @@ _Context:_ ${sessionContext}`;
5826
7021
  return Math.ceil(text.length / 4);
5827
7022
  }
5828
7023
  };
5829
- function createRetriever(eventStore, vectorStore, embedder, matcher) {
5830
- return new Retriever(eventStore, vectorStore, embedder, matcher);
7024
+ function uniqueEntityStartNodes(candidates) {
7025
+ const seen = /* @__PURE__ */ new Set();
7026
+ const nodes = [];
7027
+ for (const candidate of candidates) {
7028
+ if (!candidate.entityId || seen.has(candidate.entityId))
7029
+ continue;
7030
+ seen.add(candidate.entityId);
7031
+ nodes.push({ entityId: candidate.entityId, title: candidate.text });
7032
+ }
7033
+ return nodes;
7034
+ }
7035
+ function toRetrievalGraphPathDebug(path14, titleByEntityId) {
7036
+ const firstStep = path14.steps[0];
7037
+ const startNode = firstStep?.direction === "incoming" ? firstStep.to : firstStep?.from;
7038
+ const startEntityId = startNode?.type === "entity" ? startNode.id : "";
7039
+ return {
7040
+ startEntityId,
7041
+ startEntityTitle: titleByEntityId.get(startEntityId) ?? startNode?.name,
7042
+ targetId: path14.target.id,
7043
+ targetType: path14.target.type,
7044
+ hops: path14.hops,
7045
+ relationPath: path14.steps.map((step) => step.relationType)
7046
+ };
7047
+ }
7048
+ function graphPathScore(path14, hopPenalty) {
7049
+ const base = Math.min(0.95, Math.max(0, path14.scoreContribution));
7050
+ return Math.max(0.05, base - hopPenalty * Math.max(0, path14.hops - 1));
7051
+ }
7052
+ function clampGraphHops(maxHops) {
7053
+ if (!Number.isFinite(maxHops))
7054
+ return 2;
7055
+ return Math.min(Math.max(0, Math.trunc(maxHops)), 2);
7056
+ }
7057
+ function mergeGraphPaths(existing, incoming) {
7058
+ const byKey = /* @__PURE__ */ new Map();
7059
+ for (const path14 of [...existing, ...incoming]) {
7060
+ const key = [path14.startEntityId, path14.targetType, path14.targetId, path14.hops, ...path14.relationPath].join("\0");
7061
+ if (!byKey.has(key))
7062
+ byKey.set(key, path14);
7063
+ }
7064
+ return [...byKey.values()].sort((a, b) => a.hops - b.hops || compareStable(graphPathSignature(a), graphPathSignature(b))).slice(0, 3);
7065
+ }
7066
+ function graphPathSignature(path14) {
7067
+ return [path14.startEntityId, path14.targetType, path14.targetId, path14.hops, ...path14.relationPath].join("|");
7068
+ }
7069
+ function compareStable(a, b) {
7070
+ if (a < b)
7071
+ return -1;
7072
+ if (a > b)
7073
+ return 1;
7074
+ return 0;
7075
+ }
7076
+ function createRetriever(eventStore, vectorStore, embedder, matcher, sharedOptions) {
7077
+ return new Retriever(eventStore, vectorStore, embedder, matcher, sharedOptions);
5831
7078
  }
5832
7079
 
5833
7080
  // src/core/engine/retrieval-analytics-service.ts
@@ -6013,10 +7260,15 @@ var RetrievalDisclosureService = class {
6013
7260
  {
6014
7261
  semanticScore: debug?.semanticScore,
6015
7262
  lexicalScore: debug?.lexicalScore,
6016
- recencyScore: debug?.recencyScore
7263
+ recencyScore: debug?.recencyScore,
7264
+ ...debug?.facetMatches && debug.facetMatches.length > 0 ? { facetMatches: debug.facetMatches } : {},
7265
+ ...debug?.graphPaths && debug.graphPaths.length > 0 ? { graphPaths: this.sanitizeGraphPaths(debug.graphPaths) } : {}
6017
7266
  }
6018
7267
  );
6019
7268
  }
7269
+ sanitizeGraphPaths(graphPaths) {
7270
+ return sanitizeGovernanceAuditValue(graphPaths ?? []);
7271
+ }
6020
7272
  eventToEnvelope(event, score, reasons, extraMetadata) {
6021
7273
  return {
6022
7274
  id: toDisclosureResultId(event.id),
@@ -6076,6 +7328,10 @@ var RetrievalDisclosureService = class {
6076
7328
  reasons.add("keyword_match");
6077
7329
  if ((debug?.recencyScore ?? 0) > 0)
6078
7330
  reasons.add("recent_relevance");
7331
+ if ((debug?.facetMatches || []).length > 0)
7332
+ reasons.add("facet_match");
7333
+ if ((debug?.graphPaths || []).length > 0)
7334
+ reasons.add("entity_overlap");
6079
7335
  if ((result.fallbackTrace || []).some((step) => step === "fallback:summary"))
6080
7336
  reasons.add("summary_fallback");
6081
7337
  if (memory.sessionContext)
@@ -6203,6 +7459,7 @@ var RetrievalOrchestrator = class {
6203
7459
  const rerankWeights = lightweightFastRead ? void 0 : await this.getRerankWeights(options?.adaptiveRerank === true);
6204
7460
  const projectHash = this.deps.getProjectHash();
6205
7461
  const projectScopeMode = retrieverOptions.projectScopeMode ?? (projectHash ? "strict" : "global");
7462
+ const graphHop = this.resolveGraphHopOptions(retrieverOptions.graphHop);
6206
7463
  let result;
6207
7464
  if (retrieverOptions.includeShared && this.deps.hasSharedStore()) {
6208
7465
  result = await this.deps.retriever.retrieveUnified(query, {
@@ -6210,6 +7467,7 @@ var RetrievalOrchestrator = class {
6210
7467
  intentRewrite: retrieverOptions.intentRewrite === true,
6211
7468
  rerankWeights,
6212
7469
  includeShared: true,
7470
+ graphHop,
6213
7471
  projectHash: projectHash || void 0,
6214
7472
  projectScopeMode,
6215
7473
  allowedProjectHashes: retrieverOptions.allowedProjectHashes
@@ -6219,6 +7477,7 @@ var RetrievalOrchestrator = class {
6219
7477
  ...retrieverOptions,
6220
7478
  intentRewrite: retrieverOptions.intentRewrite === true,
6221
7479
  rerankWeights,
7480
+ graphHop,
6222
7481
  projectHash: projectHash || void 0,
6223
7482
  projectScopeMode,
6224
7483
  allowedProjectHashes: retrieverOptions.allowedProjectHashes
@@ -6280,6 +7539,29 @@ var RetrievalOrchestrator = class {
6280
7539
  await this.deps.initialize();
6281
7540
  await this.deps.accessStore.recordRetrieval(eventId, sessionId, score, query);
6282
7541
  }
7542
+ resolveGraphHopOptions(callerOptions) {
7543
+ const graphExpansion = this.deps.memoryOperationsConfig?.graphExpansion;
7544
+ const durableOptions = graphExpansion?.enabled === true ? { enabled: true, maxHops: graphExpansion.maxHops } : void 0;
7545
+ if (!callerOptions)
7546
+ return durableOptions;
7547
+ if (!graphExpansion)
7548
+ return callerOptions;
7549
+ if (graphExpansion.enabled !== true) {
7550
+ return {
7551
+ ...callerOptions,
7552
+ enabled: false,
7553
+ maxHops: graphExpansion.maxHops ?? callerOptions.maxHops
7554
+ };
7555
+ }
7556
+ return {
7557
+ enabled: callerOptions.enabled === false ? false : true,
7558
+ maxHops: Math.min(
7559
+ graphExpansion.maxHops ?? Number.POSITIVE_INFINITY,
7560
+ callerOptions.maxHops ?? graphExpansion.maxHops ?? 1
7561
+ ),
7562
+ hopPenalty: callerOptions.hopPenalty
7563
+ };
7564
+ }
6283
7565
  async recordAutomaticTrace(query, result, options, projectHash) {
6284
7566
  const selectedEventIds = result.memories.map((memory) => memory.event.id);
6285
7567
  const selectedDetails = (result.selectedDebug || []).map((detail) => ({
@@ -6421,7 +7703,8 @@ function createRetrievalServices(deps) {
6421
7703
  deps.eventStore,
6422
7704
  deps.vectorStore,
6423
7705
  deps.embedder,
6424
- deps.matcher
7706
+ deps.matcher,
7707
+ { queryGraphExpansionEnabled: deps.memoryOperationsConfig?.graphExpansion?.enabled === true }
6425
7708
  );
6426
7709
  const retrievalOrchestrator = createRetrievalOrchestrator({
6427
7710
  initialize: deps.initialize,
@@ -6429,7 +7712,8 @@ function createRetrievalServices(deps) {
6429
7712
  traceStore: deps.eventStore,
6430
7713
  accessStore: deps.eventStore,
6431
7714
  getProjectHash: deps.getProjectHash,
6432
- hasSharedStore: deps.hasSharedStore
7715
+ hasSharedStore: deps.hasSharedStore,
7716
+ memoryOperationsConfig: deps.memoryOperationsConfig
6433
7717
  });
6434
7718
  const retrievalDisclosureService = createRetrievalDisclosureService({
6435
7719
  initialize: deps.initialize,
@@ -6448,13 +7732,14 @@ function createRetrievalServices(deps) {
6448
7732
  retrievalAnalyticsService
6449
7733
  };
6450
7734
  }
6451
- function defaultCreateRetriever(eventStore, vectorStore, embedder, matcher) {
7735
+ function defaultCreateRetriever(eventStore, vectorStore, embedder, matcher, options) {
6452
7736
  assertDefaultRetrieverStore(eventStore);
6453
7737
  return createRetriever(
6454
7738
  eventStore,
6455
7739
  vectorStore,
6456
7740
  embedder,
6457
- matcher
7741
+ matcher,
7742
+ options
6458
7743
  );
6459
7744
  }
6460
7745
  function assertDefaultRetrieverStore(eventStore) {
@@ -6500,6 +7785,7 @@ function createMemoryEngineServices(options) {
6500
7785
  matcher,
6501
7786
  getProjectHash: options.getProjectHash,
6502
7787
  hasSharedStore: options.hasSharedStore,
7788
+ memoryOperationsConfig: options.memoryOperationsConfig,
6503
7789
  sharedStore: options.sharedStore
6504
7790
  });
6505
7791
  const ingestService = new MemoryIngestService({
@@ -6986,7 +8272,7 @@ function createSharedEventStore(dbPath) {
6986
8272
  }
6987
8273
 
6988
8274
  // src/core/shared-promoter.ts
6989
- import { randomUUID as randomUUID6 } from "crypto";
8275
+ import { randomUUID as randomUUID8 } from "crypto";
6990
8276
  var SharedPromoter = class {
6991
8277
  constructor(sharedStore, sharedVectorStore, embedder, config) {
6992
8278
  this.sharedStore = sharedStore;
@@ -7054,7 +8340,7 @@ var SharedPromoter = class {
7054
8340
  const embeddingContent = this.createEmbeddingContent(input);
7055
8341
  const embedding = await this.embedder.embed(embeddingContent);
7056
8342
  await this.sharedVectorStore.upsert({
7057
- id: randomUUID6(),
8343
+ id: randomUUID8(),
7058
8344
  entryId,
7059
8345
  entryType: "troubleshooting",
7060
8346
  content: embeddingContent,
@@ -7170,7 +8456,7 @@ function createSharedPromoter(sharedStore, sharedVectorStore, embedder, config)
7170
8456
  }
7171
8457
 
7172
8458
  // src/core/shared-store.ts
7173
- import { randomUUID as randomUUID7 } from "crypto";
8459
+ import { randomUUID as randomUUID9 } from "crypto";
7174
8460
  var SharedStore = class {
7175
8461
  constructor(sharedEventStore) {
7176
8462
  this.sharedEventStore = sharedEventStore;
@@ -7182,7 +8468,7 @@ var SharedStore = class {
7182
8468
  * Promote a verified troubleshooting entry to shared storage
7183
8469
  */
7184
8470
  async promoteEntry(input) {
7185
- const entryId = randomUUID7();
8471
+ const entryId = randomUUID9();
7186
8472
  await dbRun(
7187
8473
  this.db,
7188
8474
  `INSERT INTO shared_troubleshooting (
@@ -7716,6 +9002,7 @@ function createMemoryServiceComposition(options) {
7716
9002
  getProjectHash: options.getProjectHash,
7717
9003
  getProjectPath: options.getProjectPath,
7718
9004
  hasSharedStore: () => sharedMemoryServices?.isEnabled() ?? false,
9005
+ memoryOperationsConfig: options.config.operations,
7719
9006
  sharedStore: {
7720
9007
  get: (entryId) => sharedMemoryServices?.getEntryForDisclosure(entryId) ?? Promise.resolve(null)
7721
9008
  },
@@ -7836,6 +9123,18 @@ var DEFAULT_ENABLED_SHARED_STORE_CONFIG = {
7836
9123
  sharedStoragePath: SHARED_STORAGE_PATH
7837
9124
  };
7838
9125
  var DEFAULT_SHARED_STORAGE_PATH = SHARED_STORAGE_PATH;
9126
+ var DISABLED_MEMORY_OPERATIONS_CONFIG = {
9127
+ enabled: false,
9128
+ facets: { enabled: true },
9129
+ actions: { enabled: true },
9130
+ retention: { enabled: false, policyVersion: "v1" },
9131
+ graphExpansion: { enabled: false, maxHops: 1 },
9132
+ lessons: { enabled: false }
9133
+ };
9134
+ var DEFAULT_ENABLED_MEMORY_OPERATIONS_CONFIG = {
9135
+ ...DISABLED_MEMORY_OPERATIONS_CONFIG,
9136
+ enabled: true
9137
+ };
7839
9138
 
7840
9139
  // src/services/memory-service-registry.ts
7841
9140
  import * as path10 from "path";
@@ -8798,8 +10097,172 @@ searchRouter.get("/", async (c) => {
8798
10097
  // src/apps/server/api/stats.ts
8799
10098
  import { Hono as Hono4 } from "hono";
8800
10099
  import * as fs9 from "fs";
10100
+ import * as os6 from "os";
8801
10101
  import * as path11 from "path";
8802
10102
  var statsRouter = new Hono4();
10103
+ var OPERATION_STATS_TABLES = [
10104
+ "memory_facets",
10105
+ "memory_actions",
10106
+ "memory_leases",
10107
+ "memory_retention_scores",
10108
+ "memory_governance_audit",
10109
+ "memory_lessons"
10110
+ ];
10111
+ var LESSON_CONFIDENCE_BUCKETS = [
10112
+ { bucket: "0.00-0.25", min: 0, max: 0.25 },
10113
+ { bucket: "0.25-0.50", min: 0.25, max: 0.5 },
10114
+ { bucket: "0.50-0.75", min: 0.5, max: 0.75 },
10115
+ { bucket: "0.75-1.00", min: 0.75, max: 1.0000001 }
10116
+ ];
10117
+ function operationsStatsHomeDir() {
10118
+ return process.env.HOME || os6.homedir();
10119
+ }
10120
+ function projectStoragePathForOperationsStats(projectOrHash) {
10121
+ const projectHash = /^[a-f0-9]{8}$/.test(projectOrHash) ? projectOrHash : hashProjectPath(projectOrHash);
10122
+ return path11.join(operationsStatsHomeDir(), ".claude-code", "memory", "projects", projectHash);
10123
+ }
10124
+ function getOperationsStatsContext(project) {
10125
+ if (project && project.trim().length > 0) {
10126
+ const normalizedProject = project.trim();
10127
+ const projectHash = /^[a-f0-9]{8}$/.test(normalizedProject) ? normalizedProject : hashProjectPath(normalizedProject);
10128
+ const storagePath2 = projectStoragePathForOperationsStats(normalizedProject);
10129
+ return {
10130
+ projectHash,
10131
+ storagePath: storagePath2,
10132
+ dbPath: path11.join(storagePath2, "events.sqlite")
10133
+ };
10134
+ }
10135
+ const storagePath = path11.join(operationsStatsHomeDir(), ".claude-code", "memory");
10136
+ return {
10137
+ storagePath,
10138
+ dbPath: path11.join(storagePath, "events.sqlite")
10139
+ };
10140
+ }
10141
+ function countRowValue(db, sql, params = []) {
10142
+ const row = sqliteGet(db, sql, params);
10143
+ return Number(row?.count ?? 0);
10144
+ }
10145
+ function projectFilter(projectHash, column = "project_hash") {
10146
+ if (!projectHash)
10147
+ return { clause: "", params: [] };
10148
+ return { clause: `WHERE ${column} = ?`, params: [projectHash] };
10149
+ }
10150
+ function sanitizeAggregateLabel(value) {
10151
+ const raw = typeof value === "string" ? value : String(value ?? "unknown");
10152
+ const sanitized = String(sanitizeGovernanceAuditValue(raw));
10153
+ if (sanitized !== raw || sanitized.includes("[REDACTED]"))
10154
+ return "[REDACTED]";
10155
+ const trimmed = sanitized.trim();
10156
+ return trimmed.length > 0 ? trimmed.slice(0, 96) : "unknown";
10157
+ }
10158
+ function emptyOperationsStatsPayload(context, databaseExists, missingTables, windowDays) {
10159
+ return {
10160
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
10161
+ windowDays,
10162
+ projectHash: context.projectHash,
10163
+ projection: {
10164
+ databaseExists,
10165
+ available: databaseExists && missingTables.length === 0,
10166
+ missingTables
10167
+ },
10168
+ facets: { totalAssignments: 0, distribution: [] },
10169
+ actions: { total: 0, byStatus: [] },
10170
+ leases: { totalActive: 0, activeByTargetType: [] },
10171
+ retention: { total: 0, byDecision: [] },
10172
+ governanceAudit: { total: 0, operationsByDay: [] },
10173
+ lessons: {
10174
+ total: 0,
10175
+ confidenceBuckets: LESSON_CONFIDENCE_BUCKETS.map((bucket) => ({ bucket: bucket.bucket, count: 0 }))
10176
+ }
10177
+ };
10178
+ }
10179
+ function getMissingOperationTables(db) {
10180
+ const placeholders = OPERATION_STATS_TABLES.map(() => "?").join(", ");
10181
+ const rows = sqliteAll(
10182
+ db,
10183
+ `SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (${placeholders})`,
10184
+ [...OPERATION_STATS_TABLES]
10185
+ );
10186
+ const present = new Set(rows.map((row) => row.name));
10187
+ return OPERATION_STATS_TABLES.filter((table) => !present.has(table));
10188
+ }
10189
+ function sortCountRows(rows, labelKey) {
10190
+ return [...rows].sort((a, b) => {
10191
+ const countDiff = Number(b.count ?? 0) - Number(a.count ?? 0);
10192
+ if (countDiff !== 0)
10193
+ return countDiff;
10194
+ return String(a[labelKey] ?? "").localeCompare(String(b[labelKey] ?? ""));
10195
+ });
10196
+ }
10197
+ function buildFacetDistribution(db, projectHash, topFacetValues) {
10198
+ const filter = projectFilter(projectHash);
10199
+ const rows = sqliteAll(
10200
+ db,
10201
+ `SELECT dimension, value, COUNT(*) AS count
10202
+ FROM memory_facets
10203
+ ${filter.clause}
10204
+ GROUP BY dimension, value
10205
+ ORDER BY dimension ASC, count DESC, value ASC`,
10206
+ filter.params
10207
+ );
10208
+ const dimensionMap = /* @__PURE__ */ new Map();
10209
+ for (const row of rows) {
10210
+ const dimension = sanitizeAggregateLabel(row.dimension);
10211
+ const value = sanitizeAggregateLabel(row.value);
10212
+ const values = dimensionMap.get(dimension) ?? /* @__PURE__ */ new Map();
10213
+ values.set(value, (values.get(value) ?? 0) + Number(row.count ?? 0));
10214
+ dimensionMap.set(dimension, values);
10215
+ }
10216
+ return Array.from(dimensionMap.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([dimension, values]) => {
10217
+ const sortedValues = Array.from(values.entries()).map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
10218
+ const visible = sortedValues.slice(0, topFacetValues);
10219
+ const other = sortedValues.slice(topFacetValues).reduce((sum, row) => sum + row.count, 0);
10220
+ return { dimension, values: visible, other };
10221
+ });
10222
+ }
10223
+ function buildCountRows(db, sql, params, labelKey) {
10224
+ const rows = sqliteAll(db, sql, params);
10225
+ return sortCountRows(rows, "label").map((row) => ({ [labelKey]: sanitizeAggregateLabel(row.label), count: Number(row.count ?? 0) }));
10226
+ }
10227
+ function buildOperationsByDay(db, projectHash, windowStartIso) {
10228
+ const clauses = ["created_at >= ?"];
10229
+ const params = [windowStartIso];
10230
+ if (projectHash) {
10231
+ clauses.push("project_hash = ?");
10232
+ params.push(projectHash);
10233
+ }
10234
+ const rows = sqliteAll(
10235
+ db,
10236
+ `SELECT date(created_at) AS date, operation, COUNT(*) AS count
10237
+ FROM memory_governance_audit
10238
+ WHERE ${clauses.join(" AND ")}
10239
+ GROUP BY date(created_at), operation
10240
+ ORDER BY date ASC, operation ASC`,
10241
+ params
10242
+ );
10243
+ const byDay = /* @__PURE__ */ new Map();
10244
+ for (const row of rows) {
10245
+ const operations = byDay.get(row.date) ?? [];
10246
+ operations.push({ operation: sanitizeAggregateLabel(row.operation), count: Number(row.count ?? 0) });
10247
+ byDay.set(row.date, operations);
10248
+ }
10249
+ return Array.from(byDay.entries()).map(([date, operations]) => ({
10250
+ date,
10251
+ total: operations.reduce((sum, row) => sum + row.count, 0),
10252
+ operations
10253
+ }));
10254
+ }
10255
+ function buildLessonConfidenceBuckets(db, projectHash) {
10256
+ const filter = projectFilter(projectHash);
10257
+ const rows = sqliteAll(db, `SELECT confidence FROM memory_lessons ${filter.clause}`, filter.params);
10258
+ return LESSON_CONFIDENCE_BUCKETS.map((bucket) => ({
10259
+ bucket: bucket.bucket,
10260
+ count: rows.filter((row) => {
10261
+ const confidence = Number(row.confidence ?? 0);
10262
+ return confidence >= bucket.min && confidence < bucket.max;
10263
+ }).length
10264
+ }));
10265
+ }
8803
10266
  var DEFAULT_KPI_THRESHOLDS = {
8804
10267
  usefulRecallRateMin: 0.45,
8805
10268
  reworkRateMax: 0.25,
@@ -9408,6 +10871,93 @@ statsRouter.get("/levels/:level", async (c) => {
9408
10871
  await memoryService.shutdown();
9409
10872
  }
9410
10873
  });
10874
+ statsRouter.get("/operations", async (c) => {
10875
+ const context = getOperationsStatsContext(c.req.query("project") || c.req.query("projectId"));
10876
+ const windowDays = parseStatsLimit(c.req.query("windowDays"), 30, 365);
10877
+ const topFacetValues = parseStatsLimit(c.req.query("topFacetValues"), 5, 25);
10878
+ const databaseExists = fs9.existsSync(context.dbPath);
10879
+ if (!databaseExists) {
10880
+ return c.json(emptyOperationsStatsPayload(context, false, [...OPERATION_STATS_TABLES], windowDays));
10881
+ }
10882
+ let db = null;
10883
+ try {
10884
+ db = createSQLiteDatabase(context.dbPath, { readonly: true, walMode: false });
10885
+ const missingTables = getMissingOperationTables(db);
10886
+ if (missingTables.length > 0) {
10887
+ return c.json(emptyOperationsStatsPayload(context, true, missingTables, windowDays));
10888
+ }
10889
+ const now = Date.now();
10890
+ const windowStartIso = new Date(now - windowDays * 24 * 60 * 60 * 1e3).toISOString();
10891
+ const projectScoped = projectFilter(context.projectHash);
10892
+ const auditClauses = ["created_at >= ?"];
10893
+ const auditParams = [windowStartIso];
10894
+ if (context.projectHash) {
10895
+ auditClauses.push("project_hash = ?");
10896
+ auditParams.push(context.projectHash);
10897
+ }
10898
+ const auditWhere = `WHERE ${auditClauses.join(" AND ")}`;
10899
+ const facets = {
10900
+ totalAssignments: countRowValue(db, `SELECT COUNT(*) AS count FROM memory_facets ${projectScoped.clause}`, projectScoped.params),
10901
+ distribution: buildFacetDistribution(db, context.projectHash, topFacetValues)
10902
+ };
10903
+ const actions = {
10904
+ total: countRowValue(db, `SELECT COUNT(*) AS count FROM memory_actions ${projectScoped.clause}`, projectScoped.params),
10905
+ byStatus: buildCountRows(
10906
+ db,
10907
+ `SELECT status AS label, COUNT(*) AS count FROM memory_actions ${projectScoped.clause} GROUP BY status`,
10908
+ projectScoped.params,
10909
+ "status"
10910
+ )
10911
+ };
10912
+ const leases = {
10913
+ totalActive: countRowValue(db, "SELECT COUNT(*) AS count FROM memory_leases WHERE released_at IS NULL AND expires_at > ?", [new Date(now).toISOString()]),
10914
+ activeByTargetType: buildCountRows(
10915
+ db,
10916
+ "SELECT target_type AS label, COUNT(*) AS count FROM memory_leases WHERE released_at IS NULL AND expires_at > ? GROUP BY target_type",
10917
+ [new Date(now).toISOString()],
10918
+ "targetType"
10919
+ )
10920
+ };
10921
+ const retention = {
10922
+ total: countRowValue(db, `SELECT COUNT(*) AS count FROM memory_retention_scores ${projectScoped.clause}`, projectScoped.params),
10923
+ byDecision: buildCountRows(
10924
+ db,
10925
+ `SELECT decision AS label, COUNT(*) AS count FROM memory_retention_scores ${projectScoped.clause} GROUP BY decision`,
10926
+ projectScoped.params,
10927
+ "decision"
10928
+ )
10929
+ };
10930
+ const governanceAudit = {
10931
+ total: countRowValue(db, `SELECT COUNT(*) AS count FROM memory_governance_audit ${auditWhere}`, auditParams),
10932
+ operationsByDay: buildOperationsByDay(db, context.projectHash, windowStartIso)
10933
+ };
10934
+ const lessons = {
10935
+ total: countRowValue(db, `SELECT COUNT(*) AS count FROM memory_lessons ${projectScoped.clause}`, projectScoped.params),
10936
+ confidenceBuckets: buildLessonConfidenceBuckets(db, context.projectHash)
10937
+ };
10938
+ return c.json({
10939
+ generatedAt: new Date(now).toISOString(),
10940
+ windowDays,
10941
+ projectHash: context.projectHash,
10942
+ projection: {
10943
+ databaseExists: true,
10944
+ available: true,
10945
+ missingTables: []
10946
+ },
10947
+ facets,
10948
+ actions,
10949
+ leases,
10950
+ retention,
10951
+ governanceAudit,
10952
+ lessons
10953
+ });
10954
+ } catch (error) {
10955
+ console.error("[stats/operations] Failed to load aggregate stats:", error);
10956
+ return c.json({ error: "Failed to load operations stats" }, 500);
10957
+ } finally {
10958
+ db?.close();
10959
+ }
10960
+ });
9411
10961
  statsRouter.get("/", async (c) => {
9412
10962
  const memoryService = getLightweightServiceFromQuery(c);
9413
10963
  try {
@@ -10032,11 +11582,11 @@ turnsRouter.post("/backfill", async (c) => {
10032
11582
  import { Hono as Hono7 } from "hono";
10033
11583
  import * as fs10 from "fs";
10034
11584
  import * as path12 from "path";
10035
- import * as os6 from "os";
11585
+ import * as os7 from "os";
10036
11586
  var projectsRouter = new Hono7();
10037
11587
  projectsRouter.get("/", async (c) => {
10038
11588
  try {
10039
- const projectsDir = path12.join(os6.homedir(), ".claude-code", "memory", "projects");
11589
+ const projectsDir = path12.join(os7.homedir(), ".claude-code", "memory", "projects");
10040
11590
  if (!fs10.existsSync(projectsDir)) {
10041
11591
  return c.json({ projects: [] });
10042
11592
  }
@@ -10366,7 +11916,7 @@ function streamClaudeResponse(prompt, stream) {
10366
11916
  import { Hono as Hono9 } from "hono";
10367
11917
  var healthRouter = new Hono9();
10368
11918
  healthRouter.get("/", async (c) => {
10369
- const memoryService = getServiceFromQuery(c);
11919
+ const memoryService = getLightweightServiceFromQuery(c);
10370
11920
  try {
10371
11921
  await memoryService.initialize();
10372
11922
  const [stats, outbox] = await Promise.all([