openclaw-cortex-memory 0.1.0-Alpha.9 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +347 -290
  3. package/SIGNATURE.md +7 -0
  4. package/SKILL.md +96 -345
  5. package/dist/index.d.ts +69 -23
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1130 -1330
  8. package/dist/index.js.map +1 -1
  9. package/dist/openclaw.plugin.json +397 -18
  10. package/dist/src/dedup/three_stage_deduplicator.d.ts.map +1 -1
  11. package/dist/src/dedup/three_stage_deduplicator.js +13 -3
  12. package/dist/src/dedup/three_stage_deduplicator.js.map +1 -1
  13. package/dist/src/engine/memory_engine.d.ts +5 -1
  14. package/dist/src/engine/memory_engine.d.ts.map +1 -1
  15. package/dist/src/engine/ts_engine.d.ts +149 -0
  16. package/dist/src/engine/ts_engine.d.ts.map +1 -1
  17. package/dist/src/engine/ts_engine.js +863 -203
  18. package/dist/src/engine/ts_engine.js.map +1 -1
  19. package/dist/src/engine/types.d.ts +20 -0
  20. package/dist/src/engine/types.d.ts.map +1 -1
  21. package/dist/src/graph/ontology.d.ts +87 -15
  22. package/dist/src/graph/ontology.d.ts.map +1 -1
  23. package/dist/src/graph/ontology.js +999 -12
  24. package/dist/src/graph/ontology.js.map +1 -1
  25. package/dist/src/net/http_post.d.ts +17 -0
  26. package/dist/src/net/http_post.d.ts.map +1 -0
  27. package/dist/src/net/http_post.js +56 -0
  28. package/dist/src/net/http_post.js.map +1 -0
  29. package/dist/src/quality/llm_output_validator.d.ts +65 -0
  30. package/dist/src/quality/llm_output_validator.d.ts.map +1 -0
  31. package/dist/src/quality/llm_output_validator.js +635 -0
  32. package/dist/src/quality/llm_output_validator.js.map +1 -0
  33. package/dist/src/reflect/reflector.d.ts.map +1 -1
  34. package/dist/src/reflect/reflector.js +296 -26
  35. package/dist/src/reflect/reflector.js.map +1 -1
  36. package/dist/src/rules/rule_store.d.ts.map +1 -1
  37. package/dist/src/rules/rule_store.js +75 -16
  38. package/dist/src/rules/rule_store.js.map +1 -1
  39. package/dist/src/session/session_end.d.ts +20 -42
  40. package/dist/src/session/session_end.d.ts.map +1 -1
  41. package/dist/src/session/session_end.js +21 -218
  42. package/dist/src/session/session_end.js.map +1 -1
  43. package/dist/src/store/archive_store.d.ts +28 -7
  44. package/dist/src/store/archive_store.d.ts.map +1 -1
  45. package/dist/src/store/archive_store.js +367 -130
  46. package/dist/src/store/archive_store.js.map +1 -1
  47. package/dist/src/store/graph_memory_store.d.ts +115 -0
  48. package/dist/src/store/graph_memory_store.d.ts.map +1 -0
  49. package/dist/src/store/graph_memory_store.js +1061 -0
  50. package/dist/src/store/graph_memory_store.js.map +1 -0
  51. package/dist/src/store/read_store.d.ts +75 -0
  52. package/dist/src/store/read_store.d.ts.map +1 -1
  53. package/dist/src/store/read_store.js +1837 -312
  54. package/dist/src/store/read_store.js.map +1 -1
  55. package/dist/src/store/vector_store.d.ts +2 -0
  56. package/dist/src/store/vector_store.d.ts.map +1 -1
  57. package/dist/src/store/vector_store.js +19 -3
  58. package/dist/src/store/vector_store.js.map +1 -1
  59. package/dist/src/store/write_store.d.ts +11 -0
  60. package/dist/src/store/write_store.d.ts.map +1 -1
  61. package/dist/src/store/write_store.js +242 -42
  62. package/dist/src/store/write_store.js.map +1 -1
  63. package/dist/src/sync/session_sync.d.ts +72 -1
  64. package/dist/src/sync/session_sync.d.ts.map +1 -1
  65. package/dist/src/sync/session_sync.js +2246 -126
  66. package/dist/src/sync/session_sync.js.map +1 -1
  67. package/dist/src/wiki/wiki_linter.d.ts +26 -0
  68. package/dist/src/wiki/wiki_linter.d.ts.map +1 -0
  69. package/dist/src/wiki/wiki_linter.js +339 -0
  70. package/dist/src/wiki/wiki_linter.js.map +1 -0
  71. package/dist/src/wiki/wiki_logger.d.ts +10 -0
  72. package/dist/src/wiki/wiki_logger.d.ts.map +1 -0
  73. package/dist/src/wiki/wiki_logger.js +78 -0
  74. package/dist/src/wiki/wiki_logger.js.map +1 -0
  75. package/dist/src/wiki/wiki_maintainer.d.ts +39 -0
  76. package/dist/src/wiki/wiki_maintainer.d.ts.map +1 -0
  77. package/dist/src/wiki/wiki_maintainer.js +38 -0
  78. package/dist/src/wiki/wiki_maintainer.js.map +1 -0
  79. package/dist/src/wiki/wiki_projector.d.ts +35 -0
  80. package/dist/src/wiki/wiki_projector.d.ts.map +1 -0
  81. package/dist/src/wiki/wiki_projector.js +1151 -0
  82. package/dist/src/wiki/wiki_projector.js.map +1 -0
  83. package/dist/src/wiki/wiki_queue.d.ts +29 -0
  84. package/dist/src/wiki/wiki_queue.d.ts.map +1 -0
  85. package/dist/src/wiki/wiki_queue.js +137 -0
  86. package/dist/src/wiki/wiki_queue.js.map +1 -0
  87. package/openclaw.plugin.json +397 -18
  88. package/package.json +51 -5
  89. package/schema/graph.schema.yaml +330 -0
  90. package/scripts/cli.js +67 -13
  91. package/scripts/repair-memory.js +321 -0
  92. package/skills/cortex-memory/SKILL.md +83 -0
  93. package/skills/cortex-memory/references/agent-manual.md +127 -0
  94. package/skills/cortex-memory/references/configuration.md +109 -0
  95. package/skills/cortex-memory/references/publish-checklist.md +45 -0
  96. package/skills/cortex-memory/references/system-prompt-template.md +27 -0
  97. package/skills/cortex-memory/references/tools.md +191 -0
@@ -33,11 +33,18 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.normalizeEntityName = normalizeEntityName;
37
+ exports.getEntityMatchKeys = getEntityMatchKeys;
36
38
  exports.loadGraphSchema = loadGraphSchema;
37
39
  exports.normalizeEventType = normalizeEventType;
38
40
  exports.normalizeRelationType = normalizeRelationType;
41
+ exports.isCanonicalRelationType = isCanonicalRelationType;
42
+ exports.getDefaultGraphSchema = getDefaultGraphSchema;
43
+ exports.buildRelationPromptHint = buildRelationPromptHint;
39
44
  exports.buildCanonicalId = buildCanonicalId;
40
45
  exports.validateRelations = validateRelations;
46
+ exports.normalizeEntityType = normalizeEntityType;
47
+ exports.validateGraphPayload = validateGraphPayload;
41
48
  const crypto = __importStar(require("crypto"));
42
49
  const fs = __importStar(require("fs"));
43
50
  const path = __importStar(require("path"));
@@ -85,29 +92,217 @@ const DEFAULT_SCHEMA = {
85
92
  followup: "follow_up",
86
93
  next_action: "follow_up",
87
94
  },
95
+ entityTypes: [
96
+ "Person",
97
+ "FamilyMember",
98
+ "Friend",
99
+ "Team",
100
+ "Project",
101
+ "Task",
102
+ "Plan",
103
+ "Milestone",
104
+ "Location",
105
+ "Event",
106
+ "Schedule",
107
+ "Habit",
108
+ "HealthItem",
109
+ "FinanceItem",
110
+ "Issue",
111
+ "Fix",
112
+ "Decision",
113
+ "Action",
114
+ "Risk",
115
+ "Blocker",
116
+ "Assumption",
117
+ "Concept",
118
+ "Resource",
119
+ "Document",
120
+ "ConfigFile",
121
+ "Preference",
122
+ "Case",
123
+ "Pattern",
124
+ "Date",
125
+ ],
126
+ entityAliases: {
127
+ "OpenClaw": ["openclaw", "插件", "该项目", "本项目"],
128
+ "FamilyMember": ["家人", "家庭成员", "亲人"],
129
+ "Friend": ["朋友", "好友"],
130
+ "Team": ["团队", "小组", "组", "班组"],
131
+ "Location": ["地点", "位置", "住址", "地址"],
132
+ "Event": ["活动", "事情", "事项"],
133
+ "Schedule": ["日程", "安排", "计划表"],
134
+ "Habit": ["习惯", "作息"],
135
+ "HealthItem": ["健康", "体检", "药物", "锻炼"],
136
+ "FinanceItem": ["账单", "支出", "收入", "预算"],
137
+ "Plan": ["计划", "方案", "路线图"],
138
+ "Preference": ["偏好", "习惯选择"],
139
+ "Document": ["文档", "说明文档", "手册", "wiki", "README", "PRD", "方案文档"],
140
+ "Resource": ["资源", "物品", "物件", "设备", "工具", "素材", "资产"],
141
+ "ConfigFile": ["配置文件", "config", "配置"],
142
+ "Decision": ["决策", "决定", "拍板"],
143
+ "Action": ["动作", "操作", "执行"],
144
+ "Risk": ["风险", "隐患"],
145
+ "Blocker": ["阻塞", "卡点", "障碍"],
146
+ "Assumption": ["假设", "前提"],
147
+ "Concept": ["概念", "术语"],
148
+ "Case": ["案例", "case"],
149
+ "Pattern": ["模式", "pattern"],
150
+ "Date": ["日期", "时间", "时间点"],
151
+ "Person": ["我", "自己", "本人", "同事", "客户", "用户", "姓名", "名字", "人名", "成员", "联系人"],
152
+ "Project": ["项目", "工程", "项目线"],
153
+ "Task": ["任务", "待办", "todo", "工单", "事项"],
154
+ "Milestone": ["里程碑", "节点"],
155
+ "Issue": ["问题", "故障", "报错"],
156
+ "Fix": ["修复", "解决方案"],
157
+ },
88
158
  relationTypes: [
89
159
  "depends_on",
90
160
  "blocks",
91
- "related_to",
161
+ "unblocks",
92
162
  "causes",
163
+ "impacts",
93
164
  "resolves",
165
+ "encountered_bug",
166
+ "solved_with",
167
+ "uses_tech",
168
+ "integrates_with",
169
+ "migrates_to",
170
+ "replaced_by",
171
+ "has_subtask",
94
172
  "belongs_to",
95
173
  "owned_by",
96
- "references",
97
- "prefers",
98
174
  "implements",
99
175
  "requires",
176
+ "plans_to",
177
+ "planned_for",
178
+ "scheduled_for",
179
+ "references",
180
+ "documents",
181
+ "defined_in",
182
+ "configured_in",
183
+ "supports",
184
+ "conflicts_with",
185
+ "duplicates",
186
+ "supersedes",
187
+ "assigned_to",
188
+ "reviewed_by",
189
+ "approved_by",
190
+ "rejected_by",
191
+ "reported_by",
192
+ "lives_in",
193
+ "cares_for",
194
+ "pays_for",
195
+ "prefers",
196
+ "has_spouse",
197
+ "has_child",
198
+ "birthday_on",
199
+ "anniversary_on",
100
200
  ],
101
201
  relationTypeAliases: {
102
202
  dependency: "depends_on",
103
203
  blocked_by: "blocks",
104
- linked_to: "related_to",
204
+ unblock: "unblocks",
205
+ impact: "impacts",
206
+ plan_to: "plans_to",
207
+ plan_for: "planned_for",
208
+ schedule_for: "scheduled_for",
209
+ located_in: "lives_in",
210
+ care_for: "cares_for",
211
+ pay_for: "pays_for",
212
+ support: "supports",
213
+ conflict_with: "conflicts_with",
214
+ use_tech: "uses_tech",
215
+ tech_stack: "uses_tech",
216
+ integrate_with: "integrates_with",
217
+ migrate_to: "migrates_to",
218
+ replace_by: "replaced_by",
219
+ replace_with: "replaced_by",
220
+ bug: "encountered_bug",
221
+ bug_on: "encountered_bug",
222
+ fix_with: "solved_with",
223
+ solve_with: "solved_with",
224
+ solved_by: "solved_with",
225
+ subtask_of: "has_subtask",
226
+ child_task: "has_subtask",
227
+ documented_by: "documents",
228
+ defined_by: "defined_in",
229
+ config_in: "configured_in",
230
+ duplicate_of: "duplicates",
231
+ superseded_by: "supersedes",
232
+ assign_to: "assigned_to",
233
+ review_by: "reviewed_by",
234
+ approve_by: "approved_by",
235
+ reject_by: "rejected_by",
236
+ report_by: "reported_by",
237
+ "依赖于": "depends_on",
238
+ "依赖": "depends_on",
239
+ "取决于": "depends_on",
240
+ "阻塞": "blocks",
241
+ "卡住": "blocks",
242
+ "解除阻塞": "unblocks",
243
+ "导致": "causes",
244
+ "引起": "causes",
245
+ "影响": "impacts",
246
+ "解决": "resolves",
247
+ "修复": "resolves",
248
+ "遇到报错": "encountered_bug",
249
+ "通过": "solved_with",
250
+ "使用技术": "uses_tech",
251
+ "集成": "integrates_with",
252
+ "迁移到": "migrates_to",
253
+ "被替代": "replaced_by",
254
+ "子任务": "has_subtask",
255
+ "属于": "belongs_to",
256
+ "归属": "belongs_to",
257
+ "负责": "owned_by",
258
+ "由": "owned_by",
259
+ "参考": "references",
260
+ "引用": "references",
261
+ "偏好": "prefers",
262
+ "更喜欢": "prefers",
263
+ "实现": "implements",
264
+ "需要": "requires",
265
+ "计划做": "plans_to",
266
+ "打算": "plans_to",
267
+ "计划于": "planned_for",
268
+ "安排在": "scheduled_for",
269
+ "约在": "scheduled_for",
270
+ "住在": "lives_in",
271
+ "居住在": "lives_in",
272
+ "照顾": "cares_for",
273
+ "看护": "cares_for",
274
+ "支付": "pays_for",
275
+ "付款": "pays_for",
276
+ "支持": "supports",
277
+ "冲突": "conflicts_with",
278
+ "矛盾": "conflicts_with",
279
+ "记录": "documents",
280
+ "定义于": "defined_in",
281
+ "配置于": "configured_in",
282
+ "重复": "duplicates",
283
+ "取代": "supersedes",
284
+ "分配给": "assigned_to",
285
+ "评审": "reviewed_by",
286
+ "批准": "approved_by",
287
+ "拒绝": "rejected_by",
288
+ "报告": "reported_by",
105
289
  belongs: "belongs_to",
106
290
  owner_of: "owned_by",
107
291
  refer_to: "references",
108
292
  preference_for: "prefers",
109
293
  implement: "implements",
110
294
  need: "requires",
295
+ technology: "uses_tech",
296
+ encountered_issue: "encountered_bug",
297
+ spouse: "has_spouse",
298
+ wife_of: "has_spouse",
299
+ husband_of: "has_spouse",
300
+ child_of: "has_child",
301
+ parent_of: "has_child",
302
+ birthday: "birthday_on",
303
+ born_on: "birthday_on",
304
+ anniversary: "anniversary_on",
305
+ married_on: "anniversary_on",
111
306
  },
112
307
  relationRules: [
113
308
  { type: "depends_on", fromTypes: ["Task", "Plan", "Milestone"], toTypes: ["Task", "Plan", "Milestone"], allowSelfLoop: false },
@@ -116,7 +311,38 @@ const DEFAULT_SCHEMA = {
116
311
  { type: "resolves", fromTypes: ["Fix", "Decision", "Action"], toTypes: ["Issue", "Blocker"], allowSelfLoop: false },
117
312
  { type: "belongs_to", fromTypes: ["Task", "Issue", "Fix", "Decision"], toTypes: ["Project", "Plan", "Milestone"], allowSelfLoop: false },
118
313
  { type: "owned_by", fromTypes: ["Task", "Plan", "Project", "Issue"], toTypes: ["Person", "Team"], allowSelfLoop: false },
314
+ { type: "uses_tech", fromTypes: ["Project", "Task", "Fix", "Action"], toTypes: ["Resource", "Document", "Concept", "Project"], allowSelfLoop: false },
315
+ { type: "encountered_bug", fromTypes: ["Project", "Task", "Action"], toTypes: ["Issue", "Blocker"], allowSelfLoop: false },
316
+ { type: "solved_with", fromTypes: ["Issue", "Blocker"], toTypes: ["Fix", "Action", "Decision", "Resource"], allowSelfLoop: false },
317
+ { type: "has_subtask", fromTypes: ["Project", "Plan", "Milestone", "Task"], toTypes: ["Task"], allowSelfLoop: false },
318
+ { type: "planned_for", fromTypes: ["Task", "Plan", "Milestone"], toTypes: ["Date", "Schedule", "Milestone"], allowSelfLoop: false },
319
+ ],
320
+ highValueRelationTypes: [
321
+ "depends_on",
322
+ "blocks",
323
+ "unblocks",
324
+ "causes",
325
+ "impacts",
326
+ "resolves",
327
+ "encountered_bug",
328
+ "solved_with",
329
+ "uses_tech",
330
+ "integrates_with",
331
+ "migrates_to",
332
+ "replaced_by",
333
+ "has_subtask",
334
+ "belongs_to",
335
+ "owned_by",
336
+ "implements",
337
+ "requires",
338
+ "planned_for",
339
+ "scheduled_for",
119
340
  ],
341
+ relatedToMaxRatio: 0,
342
+ relatedToMaxAbsolute: 0,
343
+ minRelationConfidence: 0.35,
344
+ evidenceSpanRequired: true,
345
+ endpointMentionRequired: true,
120
346
  defaultEntityType: "Concept",
121
347
  };
122
348
  function toLowerMap(input) {
@@ -136,6 +362,171 @@ function toKeyedRules(input) {
136
362
  function schemaFilePath(projectRoot) {
137
363
  return path.join(projectRoot, "schema", "graph.schema.yaml");
138
364
  }
365
+ function clampNumber(value, min, max, fallback) {
366
+ if (typeof value !== "number" || !Number.isFinite(value)) {
367
+ return fallback;
368
+ }
369
+ return Math.max(min, Math.min(max, value));
370
+ }
371
+ function sanitizeEntityAliases(input) {
372
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
373
+ return DEFAULT_SCHEMA.entityAliases;
374
+ }
375
+ const output = {};
376
+ for (const [canonical, aliasesRaw] of Object.entries(input)) {
377
+ const canonicalName = canonical.trim();
378
+ if (!canonicalName)
379
+ continue;
380
+ const aliases = Array.isArray(aliasesRaw)
381
+ ? aliasesRaw.map(item => (typeof item === "string" ? item.trim() : "")).filter(Boolean)
382
+ : [];
383
+ output[canonicalName] = aliases;
384
+ }
385
+ return Object.keys(output).length > 0 ? output : DEFAULT_SCHEMA.entityAliases;
386
+ }
387
+ function normalizeAliasKey(value) {
388
+ return value
389
+ .normalize("NFKC")
390
+ .toLowerCase()
391
+ .replace(/[`"'“”‘’]/g, "")
392
+ .replace(/[【】[\]{}()<>]/g, " ")
393
+ .replace(/[_\-\/\\|]+/g, " ")
394
+ .replace(/[,:;!?]+/g, " ")
395
+ .replace(/\s+/g, " ")
396
+ .trim();
397
+ }
398
+ function buildEntityLookupKeys(value) {
399
+ const trimmed = value.trim();
400
+ if (!trimmed) {
401
+ return [];
402
+ }
403
+ const normalized = normalizeAliasKey(trimmed);
404
+ const compact = normalized.replace(/\s+/g, "");
405
+ const keys = new Set();
406
+ keys.add(trimmed.toLowerCase());
407
+ if (normalized) {
408
+ keys.add(normalized);
409
+ }
410
+ if (compact) {
411
+ keys.add(compact);
412
+ }
413
+ return [...keys];
414
+ }
415
+ function chooseCanonicalAlias(leftRaw, rightRaw) {
416
+ const left = leftRaw.trim();
417
+ const right = rightRaw.trim();
418
+ if (!left)
419
+ return right;
420
+ if (!right)
421
+ return left;
422
+ const leftAscii = /[A-Za-z]/.test(left);
423
+ const rightAscii = /[A-Za-z]/.test(right);
424
+ if (leftAscii && !rightAscii)
425
+ return left;
426
+ if (!leftAscii && rightAscii)
427
+ return right;
428
+ return left.length >= right.length ? left : right;
429
+ }
430
+ function buildRuntimeAliasLookup(sourceText) {
431
+ const lookup = new Map();
432
+ const text = (sourceText || "").trim();
433
+ if (!text) {
434
+ return lookup;
435
+ }
436
+ const pairPattern = /([^()\n()]{1,80})\s*[((]\s*([^()\n()]{1,80})\s*[))]/g;
437
+ let matched = pairPattern.exec(text);
438
+ while (matched) {
439
+ const left = (matched[1] || "").trim();
440
+ const right = (matched[2] || "").trim();
441
+ if (left && right && left !== right) {
442
+ const canonical = chooseCanonicalAlias(left, right);
443
+ const alias = canonical === left ? right : left;
444
+ for (const key of buildEntityLookupKeys(alias)) {
445
+ lookup.set(key, canonical);
446
+ }
447
+ for (const key of buildEntityLookupKeys(canonical)) {
448
+ lookup.set(key, canonical);
449
+ }
450
+ }
451
+ matched = pairPattern.exec(text);
452
+ }
453
+ return lookup;
454
+ }
455
+ function buildAliasLookup(schema) {
456
+ const lookup = new Map();
457
+ for (const [canonical, aliases] of Object.entries(schema.entityAliases || {})) {
458
+ const normalizedCanonical = canonical.trim();
459
+ if (!normalizedCanonical)
460
+ continue;
461
+ for (const key of buildEntityLookupKeys(normalizedCanonical)) {
462
+ lookup.set(key, normalizedCanonical);
463
+ }
464
+ for (const alias of aliases || []) {
465
+ for (const key of buildEntityLookupKeys(alias)) {
466
+ lookup.set(key, normalizedCanonical);
467
+ }
468
+ }
469
+ }
470
+ return lookup;
471
+ }
472
+ function normalizeEntityName(raw, schema, runtimeAliasLookup) {
473
+ const value = raw.trim();
474
+ if (!value)
475
+ return "";
476
+ const lookup = buildAliasLookup(schema);
477
+ for (const key of buildEntityLookupKeys(value)) {
478
+ const runtimeMapped = runtimeAliasLookup?.get(key);
479
+ if (runtimeMapped) {
480
+ return runtimeMapped;
481
+ }
482
+ const schemaMapped = lookup.get(key);
483
+ if (schemaMapped) {
484
+ return schemaMapped;
485
+ }
486
+ }
487
+ return value;
488
+ }
489
+ function getEntityMatchKeys(raw, schema) {
490
+ const value = raw.trim();
491
+ if (!value) {
492
+ return [];
493
+ }
494
+ const canonical = normalizeEntityName(value, schema);
495
+ const keys = new Set();
496
+ for (const key of buildEntityLookupKeys(value)) {
497
+ keys.add(key);
498
+ }
499
+ for (const key of buildEntityLookupKeys(canonical)) {
500
+ keys.add(key);
501
+ }
502
+ for (const alias of schema.entityAliases[canonical] || []) {
503
+ for (const key of buildEntityLookupKeys(alias)) {
504
+ keys.add(key);
505
+ }
506
+ }
507
+ return [...keys];
508
+ }
509
+ function tokenizeForMatch(value) {
510
+ return value.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
511
+ }
512
+ function entityMentionedInText(entity, sourceText, schema) {
513
+ const text = tokenizeForMatch(sourceText || "");
514
+ if (!text)
515
+ return false;
516
+ const canonical = normalizeEntityName(entity, schema);
517
+ const target = tokenizeForMatch(canonical || entity);
518
+ if (target && text.includes(target)) {
519
+ return true;
520
+ }
521
+ const aliases = schema.entityAliases[canonical] || [];
522
+ for (const alias of aliases) {
523
+ const normalized = tokenizeForMatch(alias);
524
+ if (normalized && text.includes(normalized)) {
525
+ return true;
526
+ }
527
+ }
528
+ return false;
529
+ }
139
530
  function loadGraphSchema(projectRoot) {
140
531
  const filePath = schemaFilePath(projectRoot);
141
532
  if (!fs.existsSync(filePath)) {
@@ -150,9 +541,19 @@ function loadGraphSchema(projectRoot) {
150
541
  return {
151
542
  eventTypes: Array.isArray(parsed.eventTypes) && parsed.eventTypes.length > 0 ? parsed.eventTypes : DEFAULT_SCHEMA.eventTypes,
152
543
  eventTypeAliases: parsed.eventTypeAliases && typeof parsed.eventTypeAliases === "object" ? parsed.eventTypeAliases : DEFAULT_SCHEMA.eventTypeAliases,
544
+ entityTypes: Array.isArray(parsed.entityTypes) && parsed.entityTypes.length > 0 ? parsed.entityTypes : DEFAULT_SCHEMA.entityTypes,
545
+ entityAliases: sanitizeEntityAliases(parsed.entityAliases),
153
546
  relationTypes: Array.isArray(parsed.relationTypes) && parsed.relationTypes.length > 0 ? parsed.relationTypes : DEFAULT_SCHEMA.relationTypes,
154
547
  relationTypeAliases: parsed.relationTypeAliases && typeof parsed.relationTypeAliases === "object" ? parsed.relationTypeAliases : DEFAULT_SCHEMA.relationTypeAliases,
155
548
  relationRules: Array.isArray(parsed.relationRules) && parsed.relationRules.length > 0 ? parsed.relationRules : DEFAULT_SCHEMA.relationRules,
549
+ highValueRelationTypes: Array.isArray(parsed.highValueRelationTypes) && parsed.highValueRelationTypes.length > 0
550
+ ? parsed.highValueRelationTypes.map(item => String(item).trim()).filter(Boolean)
551
+ : DEFAULT_SCHEMA.highValueRelationTypes,
552
+ relatedToMaxRatio: clampNumber(parsed.relatedToMaxRatio, 0, 1, DEFAULT_SCHEMA.relatedToMaxRatio),
553
+ relatedToMaxAbsolute: Math.max(0, Math.floor(clampNumber(parsed.relatedToMaxAbsolute, 0, 20, DEFAULT_SCHEMA.relatedToMaxAbsolute))),
554
+ minRelationConfidence: clampNumber(parsed.minRelationConfidence, 0, 1, DEFAULT_SCHEMA.minRelationConfidence),
555
+ evidenceSpanRequired: typeof parsed.evidenceSpanRequired === "boolean" ? parsed.evidenceSpanRequired : DEFAULT_SCHEMA.evidenceSpanRequired,
556
+ endpointMentionRequired: typeof parsed.endpointMentionRequired === "boolean" ? parsed.endpointMentionRequired : DEFAULT_SCHEMA.endpointMentionRequired,
156
557
  defaultEntityType: typeof parsed.defaultEntityType === "string" && parsed.defaultEntityType.trim()
157
558
  ? parsed.defaultEntityType.trim()
158
559
  : DEFAULT_SCHEMA.defaultEntityType,
@@ -177,6 +578,9 @@ function normalizeEventType(raw, schema) {
177
578
  }
178
579
  function normalizeRelationType(raw, schema) {
179
580
  const value = raw.trim().toLowerCase();
581
+ if (!value) {
582
+ return "";
583
+ }
180
584
  const relationTypes = new Set(schema.relationTypes.map(item => item.toLowerCase()));
181
585
  const aliases = toLowerMap(schema.relationTypeAliases);
182
586
  if (relationTypes.has(value)) {
@@ -186,7 +590,349 @@ function normalizeRelationType(raw, schema) {
186
590
  if (mapped) {
187
591
  return mapped;
188
592
  }
189
- return "related_to";
593
+ const snakeCase = value.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
594
+ if (/^[a-z][a-z0-9_]*$/.test(snakeCase)) {
595
+ return snakeCase;
596
+ }
597
+ return "";
598
+ }
599
+ function isCanonicalRelationType(type, schema) {
600
+ const value = type.trim().toLowerCase();
601
+ if (!value) {
602
+ return false;
603
+ }
604
+ const relationTypes = new Set(schema.relationTypes.map(item => item.toLowerCase()));
605
+ return relationTypes.has(value);
606
+ }
607
+ function getDefaultGraphSchema() {
608
+ return DEFAULT_SCHEMA;
609
+ }
610
+ function buildRelationPromptHint(schema) {
611
+ return [
612
+ `Allowed canonical relation types: ${schema.relationTypes.join(", ")}.`,
613
+ "Never use related_to.",
614
+ "If no canonical relation fits, create a snake_case custom relation, set relation_origin=llm_custom, and include relation_definition.",
615
+ ].join(" ");
616
+ }
617
+ const GENERIC_ENTITY_BLOCKLIST = new Set([
618
+ "用户",
619
+ "我",
620
+ "我们",
621
+ "你",
622
+ "你们",
623
+ "他",
624
+ "她",
625
+ "他们",
626
+ "问题",
627
+ "方案",
628
+ "实体",
629
+ "系统",
630
+ "task",
631
+ "issue",
632
+ "solution",
633
+ "system",
634
+ "person",
635
+ "user",
636
+ "thing",
637
+ ]);
638
+ function isGenericEntityName(raw) {
639
+ const value = normalizeAliasKey(String(raw || ""));
640
+ return value ? GENERIC_ENTITY_BLOCKLIST.has(value) : false;
641
+ }
642
+ function collectEntitiesFromRelations(relations, schema, runtimeAliasLookup) {
643
+ const output = new Set();
644
+ for (const relation of relations) {
645
+ const source = normalizeEntityName(relation.source || "", schema, runtimeAliasLookup);
646
+ const target = normalizeEntityName(relation.target || "", schema, runtimeAliasLookup);
647
+ if (source)
648
+ output.add(source);
649
+ if (target)
650
+ output.add(target);
651
+ }
652
+ return [...output];
653
+ }
654
+ function extractResourceReferences(sourceText) {
655
+ const text = (sourceText || "").trim();
656
+ if (!text) {
657
+ return [];
658
+ }
659
+ const output = new Set();
660
+ const urlMatches = text.match(/https?:\/\/[^\s)>"'`]+|www\.[^\s)>"'`]+/gi) || [];
661
+ const normalizedUrls = urlMatches.map(item => item.trim()).filter(Boolean);
662
+ for (const item of urlMatches) {
663
+ output.add(item.trim());
664
+ }
665
+ const pathMatches = text.match(/[A-Za-z]:\\[^\s"']+|(?:\.{0,2}\/)?(?:[\w.-]+\/)+[\w.-]+\.[A-Za-z0-9]{1,12}/g) || [];
666
+ for (const item of pathMatches) {
667
+ const value = item.trim();
668
+ const compact = value.replace(/^\.\/+/, "").replace(/^\/+/, "");
669
+ const coveredByUrl = normalizedUrls.some(url => url.includes(value) || (compact && url.includes(compact)));
670
+ if (coveredByUrl) {
671
+ continue;
672
+ }
673
+ if (value.length >= 4) {
674
+ output.add(value);
675
+ }
676
+ }
677
+ return [...output].slice(0, 12);
678
+ }
679
+ function inferEntityTypeFromName(entity, schema) {
680
+ const valid = new Set(schema.entityTypes);
681
+ const value = entity.trim();
682
+ if (!value) {
683
+ return schema.defaultEntityType;
684
+ }
685
+ if (valid.has("Date") && /(?:\d{4}-\d{2}-\d{2}|\d{1,2}月\d{1,2}日|\d{1,2}[/-]\d{1,2})/.test(value)) {
686
+ return "Date";
687
+ }
688
+ if (valid.has("Resource") && /^(https?:\/\/|www\.)/i.test(value)) {
689
+ return "Resource";
690
+ }
691
+ if (valid.has("Document")
692
+ && (/([/\\].+\.[A-Za-z0-9]{1,12})$/.test(value) || /\.(md|txt|pdf|docx?|pptx?|xlsx?|json|yaml|yml|xml|html?)$/i.test(value))) {
693
+ return "Document";
694
+ }
695
+ if (valid.has("Team") && /(team|org|organization|团队|组织|公司)/i.test(value)) {
696
+ return "Team";
697
+ }
698
+ if (valid.has("Project") && /(project|repo|仓库|项目|工程)/i.test(value)) {
699
+ return "Project";
700
+ }
701
+ return schema.defaultEntityType;
702
+ }
703
+ function inferEvidenceSpanFromSource(sourceText, candidates) {
704
+ const text = (sourceText || "").trim();
705
+ if (!text) {
706
+ return undefined;
707
+ }
708
+ const normalizedText = tokenizeForMatch(text);
709
+ const uniqueCandidates = [...new Set(candidates.map(item => item.trim()).filter(Boolean))]
710
+ .sort((a, b) => b.length - a.length);
711
+ for (const candidate of uniqueCandidates) {
712
+ const normalized = tokenizeForMatch(candidate);
713
+ if (normalized && normalizedText.includes(normalized)) {
714
+ return candidate;
715
+ }
716
+ }
717
+ return undefined;
718
+ }
719
+ function inferContextChunkFromSource(sourceText, anchors) {
720
+ const text = (sourceText || "").trim().replace(/\s+/g, " ");
721
+ if (!text)
722
+ return undefined;
723
+ const normalizedAnchors = anchors.map(item => String(item || "").trim()).filter(Boolean);
724
+ let hitIndex = -1;
725
+ let hitAnchor = "";
726
+ for (const anchor of normalizedAnchors) {
727
+ const idx = text.indexOf(anchor);
728
+ if (idx >= 0) {
729
+ hitIndex = idx;
730
+ hitAnchor = anchor;
731
+ break;
732
+ }
733
+ }
734
+ if (hitIndex < 0) {
735
+ const fallback = text.slice(0, Math.min(text.length, 100)).trim();
736
+ return fallback || undefined;
737
+ }
738
+ const targetLength = 80;
739
+ const minLength = 50;
740
+ const maxLength = 120;
741
+ let start = Math.max(0, hitIndex - Math.floor((targetLength - hitAnchor.length) / 2));
742
+ let end = Math.min(text.length, start + targetLength);
743
+ if ((end - start) < minLength) {
744
+ end = Math.min(text.length, start + minLength);
745
+ }
746
+ if ((end - start) > maxLength) {
747
+ end = start + maxLength;
748
+ }
749
+ if (end >= text.length && (end - start) < minLength) {
750
+ start = Math.max(0, end - minLength);
751
+ }
752
+ const chunk = text.slice(start, end).trim();
753
+ return chunk || undefined;
754
+ }
755
+ function summaryMentionsEntity(summary, entity, schema, runtimeAliasLookup) {
756
+ const normalizedSummary = tokenizeForMatch(summary || "");
757
+ if (!normalizedSummary) {
758
+ return false;
759
+ }
760
+ const canonical = normalizeEntityName(entity, schema, runtimeAliasLookup);
761
+ const candidates = new Set([
762
+ entity,
763
+ canonical,
764
+ ...(schema.entityAliases[canonical] || []),
765
+ ]);
766
+ for (const candidateRaw of candidates) {
767
+ const candidate = tokenizeForMatch(candidateRaw || "");
768
+ if (candidate && normalizedSummary.includes(candidate)) {
769
+ return true;
770
+ }
771
+ }
772
+ return false;
773
+ }
774
+ function missingEntitiesInSummary(args) {
775
+ const missing = [];
776
+ for (const entity of args.entities) {
777
+ if (!summaryMentionsEntity(args.summary, entity, args.schema, args.runtimeAliasLookup)) {
778
+ missing.push(entity);
779
+ }
780
+ }
781
+ return missing;
782
+ }
783
+ function normalizeSourceTextNav(args) {
784
+ const nav = args.sourceTextNav || {};
785
+ const layerRaw = typeof nav.layer === "string" ? nav.layer.trim() : "";
786
+ const layer = layerRaw === "archive_event" || layerRaw === "active_only"
787
+ ? layerRaw
788
+ : args.sourceLayer;
789
+ const sourceEventId = (typeof nav.source_event_id === "string" ? nav.source_event_id : "").trim()
790
+ || args.sourceEventId.trim()
791
+ || (typeof args.archiveEventId === "string" ? args.archiveEventId.trim() : "");
792
+ const sourceMemoryId = (typeof nav.source_memory_id === "string" ? nav.source_memory_id : "").trim()
793
+ || sourceEventId;
794
+ const sessionId = (typeof nav.session_id === "string" ? nav.session_id : "").trim()
795
+ || args.sessionId.trim();
796
+ const sourceFile = (typeof nav.source_file === "string" ? nav.source_file : "").trim()
797
+ || (typeof args.sourceFile === "string" ? args.sourceFile.trim() : "");
798
+ const fulltextAnchor = typeof nav.fulltext_anchor === "string" ? nav.fulltext_anchor.trim() : "";
799
+ if (!layer || !sessionId || !sourceFile || !sourceEventId || !sourceMemoryId) {
800
+ return null;
801
+ }
802
+ return {
803
+ layer,
804
+ session_id: sessionId,
805
+ source_file: sourceFile,
806
+ source_memory_id: sourceMemoryId,
807
+ source_event_id: sourceEventId,
808
+ fulltext_anchor: fulltextAnchor || undefined,
809
+ };
810
+ }
811
+ function shouldRetryWithFallbackRelations(rejectedReasons) {
812
+ const hardStopReasons = new Set([
813
+ "missing_relation_confidence",
814
+ "missing_evidence_span",
815
+ "low_relation_confidence",
816
+ "empty_edge",
817
+ ]);
818
+ for (const reason of rejectedReasons) {
819
+ if (hardStopReasons.has(reason)) {
820
+ return false;
821
+ }
822
+ }
823
+ return true;
824
+ }
825
+ function buildFallbackRelations(args) {
826
+ const output = [];
827
+ const dedupe = new Set();
828
+ const entitySet = new Set(args.entities);
829
+ const sourceText = (args.sourceText || "").trim();
830
+ const fallbackConfidence = Math.max(args.schema.minRelationConfidence + 0.05, 0.55);
831
+ const pushRelation = (relation) => {
832
+ const source = normalizeEntityName(relation.source || "", args.schema, args.runtimeAliasLookup);
833
+ const target = normalizeEntityName(relation.target || "", args.schema, args.runtimeAliasLookup);
834
+ const type = normalizeRelationType(relation.type || "", args.schema);
835
+ const isCanonical = isCanonicalRelationType(type, args.schema);
836
+ const relationOrigin = relation.relation_origin || (isCanonical ? "canonical" : "llm_custom");
837
+ const relationDefinitionRaw = typeof relation.relation_definition === "string" ? relation.relation_definition.trim() : "";
838
+ const relationDefinition = relationOrigin === "llm_custom"
839
+ ? (relationDefinitionRaw || `LLM custom relation inferred from type '${type}'.`)
840
+ : relationDefinitionRaw;
841
+ const evidenceSpan = typeof relation.evidence_span === "string" ? relation.evidence_span.trim() : "";
842
+ const contextChunkRaw = typeof relation.context_chunk === "string" ? relation.context_chunk.trim() : "";
843
+ const contextChunk = contextChunkRaw || inferContextChunkFromSource(sourceText, [evidenceSpan, source, target].filter(Boolean));
844
+ const confidence = typeof relation.confidence === "number"
845
+ ? Math.max(0, Math.min(1, relation.confidence))
846
+ : fallbackConfidence;
847
+ if (!source || !target || source === target)
848
+ return;
849
+ if (!entitySet.has(source) || !entitySet.has(target))
850
+ return;
851
+ if (!type || type === "related_to")
852
+ return;
853
+ if (relationOrigin === "llm_custom" && !relationDefinition)
854
+ return;
855
+ if (!evidenceSpan)
856
+ return;
857
+ const key = `${source}|${type}|${target}`;
858
+ if (dedupe.has(key))
859
+ return;
860
+ dedupe.add(key);
861
+ output.push({
862
+ source,
863
+ target,
864
+ type,
865
+ relation_origin: relationOrigin,
866
+ relation_definition: relationDefinition || undefined,
867
+ mapping_hint: typeof relation.mapping_hint === "string" ? relation.mapping_hint.trim() || undefined : undefined,
868
+ evidence_span: evidenceSpan,
869
+ context_chunk: contextChunk,
870
+ confidence,
871
+ });
872
+ };
873
+ for (const relation of args.relations) {
874
+ const sourceRaw = (relation.source || "").trim();
875
+ const targetRaw = (relation.target || "").trim();
876
+ const evidence = (typeof relation.evidence_span === "string" && relation.evidence_span.trim())
877
+ || inferEvidenceSpanFromSource(sourceText, [sourceRaw, targetRaw])
878
+ || "";
879
+ pushRelation({
880
+ source: sourceRaw,
881
+ target: targetRaw,
882
+ type: relation.type || "",
883
+ relation_origin: relation.relation_origin,
884
+ relation_definition: relation.relation_definition,
885
+ mapping_hint: relation.mapping_hint,
886
+ evidence_span: evidence,
887
+ context_chunk: inferContextChunkFromSource(sourceText, [evidence, sourceRaw, targetRaw].filter(Boolean)),
888
+ confidence: typeof relation.confidence === "number" ? relation.confidence : fallbackConfidence,
889
+ });
890
+ }
891
+ if (output.length === 0) {
892
+ const resources = args.entities.filter(entity => {
893
+ const type = (args.entityTypes[entity] || "").trim();
894
+ return type === "Resource" || type === "Document";
895
+ });
896
+ const anchors = args.entities.filter(entity => !resources.includes(entity) && (args.entityTypes[entity] || "").trim() !== "Date");
897
+ const anchor = anchors[0];
898
+ if (anchor) {
899
+ for (const resource of resources.slice(0, 3)) {
900
+ const evidence = inferEvidenceSpanFromSource(sourceText, [resource, anchor]) || "";
901
+ pushRelation({
902
+ source: anchor,
903
+ target: resource,
904
+ type: "references",
905
+ evidence_span: evidence,
906
+ context_chunk: inferContextChunkFromSource(sourceText, [evidence, anchor, resource].filter(Boolean)),
907
+ confidence: fallbackConfidence,
908
+ });
909
+ }
910
+ }
911
+ }
912
+ if (output.length === 0) {
913
+ const nonDateEntities = args.entities.filter(entity => (args.entityTypes[entity] || "").trim() !== "Date");
914
+ if (nonDateEntities.length >= 2) {
915
+ const source = nonDateEntities[0];
916
+ for (const target of nonDateEntities.slice(1)) {
917
+ const evidence = inferEvidenceSpanFromSource(sourceText, [source, target]) || "";
918
+ pushRelation({
919
+ source,
920
+ target,
921
+ type: "co_occurs_with",
922
+ relation_origin: "llm_custom",
923
+ relation_definition: "Source and target are explicitly co-mentioned within the same source chunk.",
924
+ mapping_hint: "references",
925
+ evidence_span: evidence,
926
+ context_chunk: inferContextChunkFromSource(sourceText, [evidence, source, target].filter(Boolean)),
927
+ confidence: fallbackConfidence,
928
+ });
929
+ if (output.length > 0) {
930
+ break;
931
+ }
932
+ }
933
+ }
934
+ }
935
+ return output;
190
936
  }
191
937
  function buildCanonicalId(args) {
192
938
  const entities = (args.entities || []).map(item => item.trim().toLowerCase()).filter(Boolean).sort();
@@ -203,30 +949,75 @@ function buildCanonicalId(args) {
203
949
  return `can_${crypto.createHash("sha1").update(payload).digest("hex").slice(0, 20)}`;
204
950
  }
205
951
  function validateRelations(args) {
952
+ const mode = args.qualityMode || "warn";
206
953
  const accepted = [];
207
954
  const rejected = [];
208
- const entitySet = new Set(args.entities.map(item => item.trim()).filter(Boolean));
955
+ const warnings = [];
956
+ const runtimeAliasLookup = args.runtimeAliasLookup || new Map();
957
+ const normalizedSourceText = (args.sourceText || "").trim().replace(/\s+/g, " ");
958
+ const entitySet = new Set(args.entities
959
+ .map(item => normalizeEntityName(item, args.schema, runtimeAliasLookup))
960
+ .filter(Boolean));
209
961
  const rules = toKeyedRules(args.schema.relationRules);
210
962
  const typeMap = {};
211
963
  for (const [name, type] of Object.entries(args.entityTypes || {})) {
212
964
  if (typeof name === "string" && typeof type === "string" && name.trim() && type.trim()) {
213
- typeMap[name.trim()] = type.trim();
965
+ const normalizedName = normalizeEntityName(name, args.schema, runtimeAliasLookup);
966
+ if (normalizedName) {
967
+ typeMap[normalizedName] = type.trim();
968
+ }
214
969
  }
215
970
  }
216
971
  for (const relation of args.relations) {
217
- const source = relation.source.trim();
218
- const target = relation.target.trim();
972
+ const source = normalizeEntityName(relation.source || "", args.schema, runtimeAliasLookup);
973
+ const target = normalizeEntityName(relation.target || "", args.schema, runtimeAliasLookup);
219
974
  const type = normalizeRelationType(relation.type, args.schema);
220
- const normalized = { source, target, type };
975
+ const typeIsCanonical = isCanonicalRelationType(type, args.schema);
976
+ const relationOriginRaw = typeof relation.relation_origin === "string" ? relation.relation_origin.trim() : "";
977
+ const relationOriginProvided = relationOriginRaw === "canonical" || relationOriginRaw === "llm_custom"
978
+ ? relationOriginRaw
979
+ : "";
980
+ // Keep relation_origin aligned with normalized type to avoid blocking valid custom relations.
981
+ const relationOrigin = typeIsCanonical ? "canonical" : "llm_custom";
982
+ const relationDefinition = typeof relation.relation_definition === "string" ? relation.relation_definition.trim() : "";
983
+ const mappingHint = typeof relation.mapping_hint === "string" ? relation.mapping_hint.trim() : "";
984
+ const confidence = typeof relation.confidence === "number"
985
+ ? Math.max(0, Math.min(1, relation.confidence))
986
+ : undefined;
987
+ const evidenceSpan = typeof relation.evidence_span === "string" ? relation.evidence_span.trim() : "";
988
+ const contextChunkRaw = typeof relation.context_chunk === "string" ? relation.context_chunk.trim() : "";
989
+ const contextChunk = contextChunkRaw || inferContextChunkFromSource(normalizedSourceText, [evidenceSpan, source, target].filter(Boolean));
990
+ const normalized = {
991
+ source,
992
+ target,
993
+ type,
994
+ relation_origin: relationOrigin,
995
+ relation_definition: relationDefinition || undefined,
996
+ mapping_hint: mappingHint || undefined,
997
+ confidence,
998
+ evidence_span: evidenceSpan || undefined,
999
+ context_chunk: contextChunk || undefined,
1000
+ };
221
1001
  if (!source || !target) {
222
1002
  rejected.push({ reason: "empty_edge", relation: normalized });
223
1003
  continue;
224
1004
  }
1005
+ if (!type) {
1006
+ rejected.push({ reason: "invalid_relation_type", relation: normalized });
1007
+ continue;
1008
+ }
1009
+ if (type === "related_to") {
1010
+ rejected.push({ reason: "related_to_detected", relation: normalized });
1011
+ continue;
1012
+ }
1013
+ if (relationOriginProvided && relationOriginProvided !== relationOrigin) {
1014
+ warnings.push({ reason: "relation_origin_autocorrected", relation: normalized });
1015
+ }
225
1016
  if (!entitySet.has(source) || !entitySet.has(target)) {
226
1017
  rejected.push({ reason: "edge_entity_missing", relation: normalized });
227
1018
  continue;
228
1019
  }
229
- const rule = rules.get(type);
1020
+ const rule = typeIsCanonical ? rules.get(type) : undefined;
230
1021
  if (source === target && !(rule?.allowSelfLoop ?? false)) {
231
1022
  rejected.push({ reason: "self_loop_blocked", relation: normalized });
232
1023
  continue;
@@ -245,8 +1036,204 @@ function validateRelations(args) {
245
1036
  continue;
246
1037
  }
247
1038
  }
1039
+ if (typeof confidence === "number" && confidence < args.schema.minRelationConfidence) {
1040
+ rejected.push({ reason: "low_relation_confidence", relation: normalized });
1041
+ continue;
1042
+ }
1043
+ if (typeof confidence !== "number") {
1044
+ rejected.push({ reason: "missing_relation_confidence", relation: normalized });
1045
+ continue;
1046
+ }
1047
+ if (args.schema.evidenceSpanRequired && !evidenceSpan) {
1048
+ rejected.push({ reason: "missing_evidence_span", relation: normalized });
1049
+ continue;
1050
+ }
1051
+ if (mode !== "off" && args.schema.evidenceSpanRequired && args.sourceText) {
1052
+ if (evidenceSpan && !tokenizeForMatch(args.sourceText).includes(tokenizeForMatch(evidenceSpan))) {
1053
+ if (mode === "strict") {
1054
+ rejected.push({ reason: "evidence_span_not_in_source", relation: normalized });
1055
+ continue;
1056
+ }
1057
+ warnings.push({ reason: "evidence_span_not_in_source", relation: normalized });
1058
+ }
1059
+ }
1060
+ if (mode !== "off") {
1061
+ if (!contextChunk) {
1062
+ warnings.push({ reason: "missing_context_chunk", relation: normalized });
1063
+ }
1064
+ else {
1065
+ const length = contextChunk.length;
1066
+ if (length < 50 || length > 120) {
1067
+ warnings.push({ reason: "context_chunk_length_out_of_range", relation: normalized });
1068
+ }
1069
+ if (normalizedSourceText && !normalizedSourceText.includes(contextChunk)) {
1070
+ warnings.push({ reason: "context_chunk_not_in_source", relation: normalized });
1071
+ }
1072
+ }
1073
+ }
1074
+ if (mode !== "off" && args.schema.endpointMentionRequired && args.sourceText) {
1075
+ const sourceHit = entityMentionedInText(source, args.sourceText, args.schema);
1076
+ const targetHit = entityMentionedInText(target, args.sourceText, args.schema);
1077
+ if (!sourceHit || !targetHit) {
1078
+ if (mode === "strict") {
1079
+ rejected.push({ reason: "endpoint_not_in_source_text", relation: normalized });
1080
+ continue;
1081
+ }
1082
+ warnings.push({ reason: "endpoint_not_in_source_text", relation: normalized });
1083
+ }
1084
+ }
248
1085
  accepted.push(normalized);
249
1086
  }
250
- return { accepted, rejected };
1087
+ return { accepted, rejected, warnings };
1088
+ }
1089
+ function normalizeEntityType(raw, schema) {
1090
+ const value = raw.trim();
1091
+ const entityTypes = new Set(schema.entityTypes.map(item => item));
1092
+ if (entityTypes.has(value)) {
1093
+ return value;
1094
+ }
1095
+ return schema.defaultEntityType;
1096
+ }
1097
+ function validateGraphPayload(args) {
1098
+ const sourceEventId = (args.sourceEventId || "").trim();
1099
+ if (!sourceEventId) {
1100
+ return { valid: false, reason: "source_event_id_empty" };
1101
+ }
1102
+ const summary = typeof args.summary === "string" ? args.summary.trim() : "";
1103
+ if (!summary) {
1104
+ return { valid: false, reason: "missing_summary" };
1105
+ }
1106
+ const sourceTextNav = normalizeSourceTextNav({
1107
+ sourceTextNav: args.source_text_nav,
1108
+ sourceLayer: args.sourceLayer,
1109
+ sourceEventId,
1110
+ archiveEventId: args.archiveEventId,
1111
+ sessionId: args.sessionId,
1112
+ sourceFile: args.sourceFile,
1113
+ });
1114
+ if (!sourceTextNav) {
1115
+ return { valid: false, reason: "fulltext_navigation_missing" };
1116
+ }
1117
+ const baseWarnings = [];
1118
+ const runtimeAliasLookup = buildRuntimeAliasLookup(args.sourceText);
1119
+ const normalizedInputEntities = Array.isArray(args.entities)
1120
+ ? args.entities
1121
+ .map(item => normalizeEntityName(typeof item === "string" ? item : "", args.schema, runtimeAliasLookup))
1122
+ .filter(Boolean)
1123
+ : [];
1124
+ const relationEndpoints = collectEntitiesFromRelations(Array.isArray(args.relations) ? args.relations : [], args.schema, runtimeAliasLookup);
1125
+ const resourceEntities = extractResourceReferences(args.sourceText)
1126
+ .map(item => normalizeEntityName(item, args.schema, runtimeAliasLookup))
1127
+ .filter(Boolean);
1128
+ const dedupedEntities = [...new Set([...normalizedInputEntities, ...relationEndpoints, ...resourceEntities])];
1129
+ const entities = dedupedEntities.filter(entity => !isGenericEntityName(entity));
1130
+ if (entities.length !== dedupedEntities.length) {
1131
+ baseWarnings.push("generic_entity_rejected");
1132
+ }
1133
+ if (entities.length === 0) {
1134
+ return { valid: false, reason: "entities_empty" };
1135
+ }
1136
+ const summaryMissingEntities = missingEntitiesInSummary({ summary, entities, schema: args.schema, runtimeAliasLookup });
1137
+ if (summaryMissingEntities.length > 0) {
1138
+ const summaryCoverageMode = args.qualityMode || "warn";
1139
+ const missingAllEntities = summaryMissingEntities.length >= entities.length;
1140
+ if (summaryCoverageMode === "strict" || missingAllEntities) {
1141
+ return { valid: false, reason: "summary_missing_entities" };
1142
+ }
1143
+ baseWarnings.push(`summary_missing_entities_partial:${summaryMissingEntities.length}/${entities.length}`);
1144
+ }
1145
+ const entityTypes = args.entity_types || {};
1146
+ const validEntityTypes = new Set(args.schema.entityTypes);
1147
+ const normalizedEntityTypes = {};
1148
+ for (const [nameRaw, typeRaw] of Object.entries(entityTypes)) {
1149
+ if (typeof typeRaw !== "string")
1150
+ continue;
1151
+ const normalizedName = normalizeEntityName(nameRaw.trim(), args.schema, runtimeAliasLookup);
1152
+ if (!normalizedName)
1153
+ continue;
1154
+ normalizedEntityTypes[normalizedName] = typeRaw.trim();
1155
+ }
1156
+ for (const entity of entities) {
1157
+ const providedType = normalizedEntityTypes[entity];
1158
+ if (providedType && validEntityTypes.has(providedType)) {
1159
+ normalizedEntityTypes[entity] = providedType;
1160
+ }
1161
+ else {
1162
+ normalizedEntityTypes[entity] = inferEntityTypeFromName(entity, args.schema);
1163
+ }
1164
+ }
1165
+ const relationValidation = validateRelations({
1166
+ relations: Array.isArray(args.relations) ? args.relations : [],
1167
+ entities,
1168
+ entityTypes: normalizedEntityTypes,
1169
+ schema: args.schema,
1170
+ sourceText: args.sourceText,
1171
+ qualityMode: args.qualityMode,
1172
+ runtimeAliasLookup,
1173
+ });
1174
+ let acceptedRelations = relationValidation.accepted;
1175
+ const warnings = [...baseWarnings, ...relationValidation.warnings.map(item => item.reason)];
1176
+ if (acceptedRelations.length === 0) {
1177
+ const rejectedReasons = new Set(relationValidation.rejected.map(item => item.reason));
1178
+ if (!shouldRetryWithFallbackRelations(rejectedReasons)) {
1179
+ return { valid: false, reason: "relations_empty_or_invalid" };
1180
+ }
1181
+ const fallbackRelations = buildFallbackRelations({
1182
+ entities,
1183
+ entityTypes: normalizedEntityTypes,
1184
+ relations: Array.isArray(args.relations) ? args.relations : [],
1185
+ sourceText: args.sourceText,
1186
+ schema: args.schema,
1187
+ runtimeAliasLookup,
1188
+ });
1189
+ if (fallbackRelations.length > 0) {
1190
+ const fallbackValidation = validateRelations({
1191
+ relations: fallbackRelations,
1192
+ entities,
1193
+ entityTypes: normalizedEntityTypes,
1194
+ schema: args.schema,
1195
+ sourceText: args.sourceText,
1196
+ qualityMode: args.qualityMode,
1197
+ runtimeAliasLookup,
1198
+ });
1199
+ if (fallbackValidation.accepted.length > 0) {
1200
+ acceptedRelations = fallbackValidation.accepted;
1201
+ warnings.push("fallback_relations_applied");
1202
+ warnings.push(...fallbackValidation.warnings.map(item => item.reason));
1203
+ }
1204
+ }
1205
+ }
1206
+ if (acceptedRelations.length === 0) {
1207
+ return { valid: false, reason: "relations_empty_or_invalid" };
1208
+ }
1209
+ const id = `gph_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
1210
+ return {
1211
+ valid: true,
1212
+ warnings: [...new Set(warnings)],
1213
+ normalized: {
1214
+ id,
1215
+ summary,
1216
+ source_text_nav: sourceTextNav,
1217
+ source_event_id: args.sourceEventId.trim(),
1218
+ source_layer: args.sourceLayer,
1219
+ archive_event_id: typeof args.archiveEventId === "string" && args.archiveEventId.trim()
1220
+ ? args.archiveEventId.trim()
1221
+ : undefined,
1222
+ session_id: args.sessionId,
1223
+ source_file: typeof args.sourceFile === "string" && args.sourceFile.trim()
1224
+ ? args.sourceFile.trim()
1225
+ : undefined,
1226
+ timestamp: new Date().toISOString(),
1227
+ entities,
1228
+ entity_types: normalizedEntityTypes,
1229
+ relations: acceptedRelations,
1230
+ gate_source: args.gateSource,
1231
+ event_type: typeof args.eventType === "string" && args.eventType.trim()
1232
+ ? normalizeEventType(args.eventType, args.schema)
1233
+ : undefined,
1234
+ schema_version: "1.0.0",
1235
+ confidence: args.confidence,
1236
+ },
1237
+ };
251
1238
  }
252
1239
  //# sourceMappingURL=ontology.js.map