stratagate-dsh 0.2.75 → 0.2.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.js CHANGED
@@ -385,6 +385,155 @@ function rrfRank(rankings) {
385
385
  // packages/core/src/graph.ts
386
386
  var NODE_TYPES = /* @__PURE__ */ new Set(["person", "project", "organization", "tool", "place"]);
387
387
  var STATUSES = /* @__PURE__ */ new Set(["active", "superseded", "disputed", "archived"]);
388
+ var GRAPH_PROVENANCE_LIMIT = 6;
389
+ function unique(values) {
390
+ return [...new Set(values)];
391
+ }
392
+ function factText(fact) {
393
+ return `${fact.key}: ${Array.isArray(fact.value) ? fact.value.join("\u3001") : fact.value}`;
394
+ }
395
+ function renderCurrentState(facts) {
396
+ return facts.map((fact) => `${factText(fact)}${fact.status === "disputed" ? " [disputed]" : ""}`).join("\n");
397
+ }
398
+ var EXPOSED_EVENT_STATUSES = /* @__PURE__ */ new Set(["active", "superseded"]);
399
+ function exposedSources(ids, eventById) {
400
+ return unique((ids ?? []).filter((id) => EXPOSED_EVENT_STATUSES.has(eventById.get(id)?.status ?? "forgotten")));
401
+ }
402
+ function metadataEntries(entries, allowedValues, eventById) {
403
+ const allowed = new Set(allowedValues.map((value) => normalizeSearchText(value)));
404
+ return (entries ?? []).flatMap((entry) => {
405
+ const value = typeof entry.value === "string" ? entry.value.trim() : "";
406
+ const sourceEventIds = exposedSources(entry.sourceEventIds, eventById);
407
+ return value && allowed.has(normalizeSearchText(value)) && sourceEventIds.length > 0 ? [{ value, sourceEventIds }] : [];
408
+ });
409
+ }
410
+ function effectiveMetadata(node, eventById) {
411
+ if (!node.metadataProvenance) {
412
+ const potentialSources = unique(node.sourceEventIds ?? []);
413
+ const allSourcesExposed = potentialSources.length > 0 && potentialSources.every((id) => EXPOSED_EVENT_STATUSES.has(eventById.get(id)?.status ?? "forgotten"));
414
+ return allSourcesExposed ? { name: node.name, aliases: [...node.aliases], ...node.tags ? { tags: [...node.tags] } : {} } : { name: "", aliases: [] };
415
+ }
416
+ const nameSources = exposedSources(node.metadataProvenance.name, eventById);
417
+ const aliases = metadataEntries(node.metadataProvenance.aliases, node.aliases, eventById);
418
+ const tags = metadataEntries(node.metadataProvenance.tags, node.tags ?? [], eventById);
419
+ const metadataProvenance = {
420
+ ...nameSources.length > 0 ? { name: nameSources } : {},
421
+ ...aliases.length > 0 ? { aliases } : {},
422
+ ...tags.length > 0 ? { tags } : {}
423
+ };
424
+ return {
425
+ name: nameSources.length > 0 ? node.name : "",
426
+ aliases: aliases.map(({ value }) => value),
427
+ ...tags.length > 0 ? { tags: tags.map(({ value }) => value) } : {},
428
+ metadataProvenance
429
+ };
430
+ }
431
+ function effectiveGraphNodeView(node, edges, events) {
432
+ if (node.status !== "active" && node.status !== "disputed") return null;
433
+ const eventById = new Map(events.map((event) => [event.id, event]));
434
+ const sources2 = (ids, statuses) => unique((ids ?? []).filter((id) => {
435
+ const event = eventById.get(id);
436
+ return event !== void 0 && statuses.has(event.status);
437
+ }));
438
+ const activeStatuses = /* @__PURE__ */ new Set(["active"]);
439
+ const visibleStatuses = /* @__PURE__ */ new Set(["active", "superseded"]);
440
+ const splitRecord = (record2) => {
441
+ if (record2.status === "archived") return {};
442
+ const activeSources = sources2(record2.sourceEventIds, activeStatuses);
443
+ const visibleSources = sources2(record2.sourceEventIds, visibleStatuses);
444
+ if ((record2.status === "active" || record2.status === "disputed") && activeSources.length > 0) {
445
+ return { current: { ...record2, sourceEventIds: activeSources } };
446
+ }
447
+ if (visibleSources.length > 0 && record2.status === "superseded") {
448
+ return { historical: { ...record2, sourceEventIds: visibleSources } };
449
+ }
450
+ return {};
451
+ };
452
+ const currentFacts = [];
453
+ const historicalFacts = [];
454
+ for (const fact of node.facts ?? []) {
455
+ const split = splitRecord(fact);
456
+ if (split.current) currentFacts.push(split.current);
457
+ if (split.historical) historicalFacts.push(split.historical);
458
+ }
459
+ const currentEdges = [];
460
+ const historicalEdges = [];
461
+ for (const edge of edges.filter(({ fromNodeId, toNodeId }) => fromNodeId === node.id || toNodeId === node.id)) {
462
+ const split = splitRecord(edge);
463
+ if (split.current) currentEdges.push(split.current);
464
+ if (split.historical) historicalEdges.push(split.historical);
465
+ }
466
+ const currentNodeEventIds = sources2(node.sourceEventIds, activeStatuses);
467
+ const historicalNodeEventIds = sources2(node.sourceEventIds, /* @__PURE__ */ new Set(["superseded"]));
468
+ if (currentFacts.length === 0 && historicalFacts.length === 0 && currentEdges.length === 0 && historicalEdges.length === 0 && currentNodeEventIds.length === 0 && historicalNodeEventIds.length === 0) return null;
469
+ const visibleNodeSources = unique([...currentNodeEventIds, ...historicalNodeEventIds]);
470
+ const metadata = effectiveMetadata(node, eventById);
471
+ const effectiveNode = { ...node, ...metadata };
472
+ if (!Object.prototype.hasOwnProperty.call(metadata, "tags")) delete effectiveNode.tags;
473
+ return {
474
+ node: { ...effectiveNode, currentState: renderCurrentState(currentFacts), facts: currentFacts, sourceEventIds: visibleNodeSources },
475
+ currentFacts,
476
+ historicalFacts,
477
+ currentEdges,
478
+ historicalEdges,
479
+ currentNodeEventIds,
480
+ historicalNodeEventIds
481
+ };
482
+ }
483
+ function boundEffectiveGraphNodeView(view, eventIds, options = {}) {
484
+ const boundRecord = (record2) => {
485
+ const sourceEventIds = record2.sourceEventIds.filter((id) => eventIds.has(id));
486
+ return sourceEventIds.length > 0 ? { ...record2, sourceEventIds } : null;
487
+ };
488
+ const currentFacts = view.currentFacts.flatMap((record2) => boundRecord(record2) ?? []);
489
+ const historicalFacts = view.historicalFacts.flatMap((record2) => boundRecord(record2) ?? []);
490
+ const currentEdges = view.currentEdges.flatMap((record2) => boundRecord(record2) ?? []);
491
+ const historicalEdges = view.historicalEdges.flatMap((record2) => boundRecord(record2) ?? []);
492
+ const metadata = view.node.metadataProvenance ? (() => {
493
+ const name2 = (view.node.metadataProvenance?.name ?? []).filter((id) => eventIds.has(id));
494
+ const aliases = (view.node.metadataProvenance?.aliases ?? []).flatMap((entry) => {
495
+ const sourceEventIds = entry.sourceEventIds.filter((id) => eventIds.has(id));
496
+ return sourceEventIds.length > 0 ? [{ ...entry, sourceEventIds }] : [];
497
+ });
498
+ const tags = (view.node.metadataProvenance?.tags ?? []).flatMap((entry) => {
499
+ const sourceEventIds = entry.sourceEventIds.filter((id) => eventIds.has(id));
500
+ return sourceEventIds.length > 0 ? [{ ...entry, sourceEventIds }] : [];
501
+ });
502
+ return {
503
+ name: name2.length > 0 ? view.node.name : "",
504
+ aliases: aliases.map(({ value }) => value),
505
+ ...tags.length > 0 ? { tags: tags.map(({ value }) => value) } : {},
506
+ metadataProvenance: {
507
+ ...name2.length > 0 ? { name: name2 } : {},
508
+ ...aliases.length > 0 ? { aliases } : {},
509
+ ...tags.length > 0 ? { tags } : {}
510
+ }
511
+ };
512
+ })() : view.node.sourceEventIds.length > 0 && view.node.sourceEventIds.every((id) => eventIds.has(id)) || options.preserveLegacyMetadata ? { name: view.node.name, aliases: [...view.node.aliases], ...view.node.tags ? { tags: [...view.node.tags] } : {} } : { name: "", aliases: [] };
513
+ const currentNodeEventIds = view.currentNodeEventIds.filter((id) => eventIds.has(id));
514
+ const historicalNodeEventIds = view.historicalNodeEventIds.filter((id) => eventIds.has(id));
515
+ const boundedNode = { ...view.node, ...metadata };
516
+ if (!Object.prototype.hasOwnProperty.call(metadata, "tags")) delete boundedNode.tags;
517
+ return {
518
+ node: { ...boundedNode, currentState: renderCurrentState(currentFacts), facts: currentFacts, sourceEventIds: unique([...currentNodeEventIds, ...historicalNodeEventIds]) },
519
+ currentFacts,
520
+ historicalFacts,
521
+ currentEdges,
522
+ historicalEdges,
523
+ currentNodeEventIds,
524
+ historicalNodeEventIds
525
+ };
526
+ }
527
+ function graphTimeline(eventIds, events, limit = GRAPH_PROVENANCE_LIMIT) {
528
+ const order = new Map(eventIds.map((id, index) => [id, index]));
529
+ return events.filter((event) => order.has(event.id) && event.status !== "forgotten" && event.status !== "archived").sort((left, right) => (order.get(left.id) ?? 0) - (order.get(right.id) ?? 0)).slice(0, Math.max(0, limit)).map((event) => ({
530
+ id: event.id,
531
+ title: event.title,
532
+ summary: event.summary,
533
+ status: event.status,
534
+ ...event.temporal.happenedStart ? { time: event.temporal.happenedStart } : {}
535
+ }));
536
+ }
388
537
  function text(value, limit = 240) {
389
538
  return typeof value === "string" ? value.trim().replace(/\s+/g, " ").slice(0, limit) : "";
390
539
  }
@@ -397,20 +546,48 @@ function confidence(value) {
397
546
  function chronology(events, ids, fallback) {
398
547
  return ids.flatMap((id) => events.find((event) => event.id === id) ?? []).map((event) => event.temporal.happenedStart ?? event.temporal.happenedEnd ?? event.temporal.mentionedAt ?? event.createdAt).sort().at(-1) ?? fallback;
399
548
  }
400
- function sameEntity(node, proposal) {
401
- if (node.type !== proposal.type) return false;
549
+ function entityMatchKind(node, proposal, eventById) {
550
+ if (node.type !== proposal.type) return null;
402
551
  const key = (value) => normalizeSearchText(value).replace(/[\s_-]+/g, "");
403
- const names = new Set([proposal.name, ...proposal.aliases ?? []].map(key));
404
- return [node.name, ...node.aliases].some((name2) => names.has(key(name2)));
552
+ const metadata = effectiveMetadata(node, eventById);
553
+ const proposalNames = new Set([proposal.name, ...proposal.aliases ?? []].map(key));
554
+ if (proposalNames.has(key(metadata.name))) return "canonical-name";
555
+ if (metadata.aliases.some((value) => proposalNames.has(key(value)))) return "alias";
556
+ return null;
405
557
  }
406
558
  function applyGraphProjection(options) {
407
559
  const refs = /* @__PURE__ */ new Map();
408
560
  const touchedNodes = /* @__PURE__ */ new Set();
409
561
  const touchedEdges = /* @__PURE__ */ new Set();
562
+ const warnings = [];
563
+ const eventById = new Map(options.events.map((event) => [event.id, event]));
410
564
  const validSources = (value) => {
411
565
  const requested = strings(value, 64);
412
- return requested.length > 0 && requested.every((id) => options.allowedEventIds.has(id)) ? requested : [];
566
+ return requested.length > 0 && requested.every((id) => options.allowedEventIds.has(id) && EXPOSED_EVENT_STATUSES.has(eventById.get(id)?.status ?? "forgotten")) ? requested : [];
567
+ };
568
+ const activeSources = (ids) => ids.filter((id) => eventById.get(id)?.status === "active");
569
+ const metadataEntriesFromProjection = (entries, fallbackValues) => {
570
+ const byValue = new Map((entries ?? []).map((entry) => [normalizeSearchText(entry.value), entry]));
571
+ return fallbackValues.flatMap((value) => {
572
+ const entry = byValue.get(normalizeSearchText(value));
573
+ const sourceEventIds = validSources(entry?.sourceEventIds);
574
+ if (sourceEventIds.length > 0) return [{ value, sourceEventIds }];
575
+ warnings.push(`Dropped metadata value "${value}" because it lacks matching valid field provenance.`);
576
+ return [];
577
+ });
413
578
  };
579
+ for (const proposal of Array.isArray(options.result.nodes) ? options.result.nodes : []) {
580
+ const ref = text(proposal.ref, 120);
581
+ const name2 = text(proposal.name, 160);
582
+ const sources2 = validSources(proposal.sourceEventIds);
583
+ if (!ref || !name2 || !NODE_TYPES.has(proposal.type)) continue;
584
+ if (sources2.length === 0) {
585
+ throw new Error(`Graph projection validation failed: node "${ref}" has no valid exposable source Event.`);
586
+ }
587
+ if (validSources(proposal.metadataProvenance?.name).length === 0) {
588
+ throw new Error(`Graph projection validation failed: node "${ref}" name "${name2}" lacks valid metadata provenance.`);
589
+ }
590
+ }
414
591
  for (const proposal of Array.isArray(options.result.nodes) ? options.result.nodes : []) {
415
592
  const ref = text(proposal.ref, 120);
416
593
  const name2 = text(proposal.name, 160);
@@ -418,7 +595,15 @@ function applyGraphProjection(options) {
418
595
  if (!ref || !name2 || !NODE_TYPES.has(proposal.type) || sources2.length === 0) continue;
419
596
  const aliases = strings(proposal.aliases, 20).filter((alias) => normalizeSearchText(alias) !== normalizeSearchText(name2));
420
597
  const tags = strings(proposal.tags, 12);
421
- let node = options.nodes.find((candidate) => sameEntity(candidate, { ...proposal, name: name2, aliases }));
598
+ const projectedNameSources = validSources(proposal.metadataProvenance?.name);
599
+ if (projectedNameSources.length === 0) continue;
600
+ const projectedMetadata = {
601
+ name: projectedNameSources,
602
+ aliases: metadataEntriesFromProjection(proposal.metadataProvenance?.aliases, aliases),
603
+ tags: metadataEntriesFromProjection(proposal.metadataProvenance?.tags, tags)
604
+ };
605
+ const provenAliases = projectedMetadata.aliases.map(({ value }) => value);
606
+ let node = options.nodes.find((candidate) => entityMatchKind(candidate, { name: name2, aliases: provenAliases, type: proposal.type }, eventById));
422
607
  if (!node) {
423
608
  node = {
424
609
  id: options.idFactory("node"),
@@ -435,8 +620,37 @@ function applyGraphProjection(options) {
435
620
  };
436
621
  options.nodes.push(node);
437
622
  }
438
- node.aliases = [.../* @__PURE__ */ new Set([...node.aliases, ...aliases])];
439
- if (tags.length > 0) node.tags = [.../* @__PURE__ */ new Set([...node.tags ?? [], ...tags])].slice(0, 12);
623
+ const canonicalKey = normalizeSearchText(node.name);
624
+ const canonicalNameSources = [
625
+ ...normalizeSearchText(name2) === canonicalKey ? projectedMetadata.name : [],
626
+ ...projectedMetadata.aliases.filter(({ value }) => normalizeSearchText(value) === canonicalKey).flatMap(({ sourceEventIds }) => sourceEventIds),
627
+ ...node.metadataProvenance?.name ?? []
628
+ ];
629
+ const projectedAliasEntries = [
630
+ ...projectedMetadata.aliases.filter(({ value }) => normalizeSearchText(value) !== canonicalKey),
631
+ ...normalizeSearchText(name2) !== canonicalKey ? [{ value: name2, sourceEventIds: projectedMetadata.name }] : [],
632
+ ...aliases.filter((alias) => normalizeSearchText(alias) !== canonicalKey && !projectedMetadata.aliases.some((entry) => normalizeSearchText(entry.value) === normalizeSearchText(alias))).map((value) => ({ value, sourceEventIds: projectedMetadata.aliases.find((entry) => normalizeSearchText(entry.value) === normalizeSearchText(value))?.sourceEventIds ?? [] })).filter(({ sourceEventIds }) => sourceEventIds.length > 0)
633
+ ];
634
+ node.aliases = [.../* @__PURE__ */ new Set([...node.aliases, ...projectedAliasEntries.map(({ value }) => value)])];
635
+ if (tags.length > 0) node.tags = [.../* @__PURE__ */ new Set([...node.tags ?? [], ...projectedMetadata.tags.map(({ value }) => value)])].slice(0, 12);
636
+ const previousMetadata = node.metadataProvenance;
637
+ const aliasEntries = [...previousMetadata?.aliases ?? []];
638
+ for (const entry of projectedAliasEntries) {
639
+ const existing = aliasEntries.find((candidate) => normalizeSearchText(candidate.value) === normalizeSearchText(entry.value));
640
+ if (existing) existing.sourceEventIds = [.../* @__PURE__ */ new Set([...existing.sourceEventIds, ...entry.sourceEventIds])];
641
+ else aliasEntries.push(entry);
642
+ }
643
+ const tagEntries = [...previousMetadata?.tags ?? []];
644
+ for (const entry of projectedMetadata.tags) {
645
+ const existing = tagEntries.find((candidate) => normalizeSearchText(candidate.value) === normalizeSearchText(entry.value));
646
+ if (existing) existing.sourceEventIds = [.../* @__PURE__ */ new Set([...existing.sourceEventIds, ...entry.sourceEventIds])];
647
+ else tagEntries.push(entry);
648
+ }
649
+ node.metadataProvenance = {
650
+ ...canonicalNameSources.length > 0 ? { name: [...new Set(canonicalNameSources)] } : previousMetadata?.name ? { name: [...previousMetadata.name] } : {},
651
+ ...aliasEntries.length > 0 ? { aliases: aliasEntries } : {},
652
+ ...tagEntries.length > 0 ? { tags: tagEntries } : {}
653
+ };
440
654
  node.status = STATUSES.has(proposal.status ?? "active") ? proposal.status ?? "active" : "active";
441
655
  node.confidence = confidence(proposal.confidence);
442
656
  node.sourceEventIds = [.../* @__PURE__ */ new Set([...node.sourceEventIds, ...sources2])];
@@ -451,7 +665,14 @@ function applyGraphProjection(options) {
451
665
  const value = Array.isArray(rawFact.value) ? strings(rawFact.value, 40) : text(rawFact.value, 1200);
452
666
  if (!key || (Array.isArray(value) ? value.length === 0 : !value)) continue;
453
667
  const factSources = "sourceEventIds" in rawFact ? validSources(rawFact.sourceEventIds) : sources2;
454
- if (factSources.length === 0) continue;
668
+ if (factSources.length === 0) {
669
+ warnings.push(`Dropped fact "${key}" because it lacks valid exposable Event provenance.`);
670
+ continue;
671
+ }
672
+ if ((node.status === "active" || node.status === "disputed") && activeSources(factSources).length === 0) {
673
+ warnings.push(`Dropped current fact "${key}" because it has no active Event provenance.`);
674
+ continue;
675
+ }
455
676
  for (const old of node.facts.filter((fact2) => fact2.status === "active" && fact2.key === key)) {
456
677
  old.status = "superseded";
457
678
  if (!old.validTo) old.validTo = validFrom;
@@ -485,6 +706,10 @@ function applyGraphProjection(options) {
485
706
  const status = STATUSES.has(proposal.status ?? "active") ? proposal.status ?? "active" : "active";
486
707
  const validFrom = text(proposal.validFrom, 80) || chronology(options.events, sources2, options.now);
487
708
  const validTo = text(proposal.validTo, 80) || void 0;
709
+ if ((status === "active" || status === "disputed") && activeSources(sources2).length === 0) {
710
+ warnings.push(`Dropped current edge "${relation}" because it has no active Event provenance.`);
711
+ continue;
712
+ }
488
713
  for (const old of options.edges.filter((edge2) => edge2.status === "active" && edge2.fromNodeId === from.id && edge2.relation === relation && edge2.toNodeId !== to.id)) {
489
714
  old.status = "superseded";
490
715
  if (!old.validTo) old.validTo = validFrom;
@@ -517,7 +742,7 @@ function applyGraphProjection(options) {
517
742
  const participantNodeIds = options.nodes.filter((node) => node.sourceEventIds.includes(event.id)).map(({ id }) => id);
518
743
  event.temporal.participantNodeIds = [.../* @__PURE__ */ new Set([...event.temporal.participantNodeIds ?? [], ...participantNodeIds])];
519
744
  }
520
- return { nodeIds: [...touchedNodes], edgeIds: [...touchedEdges] };
745
+ return { nodeIds: [...touchedNodes], edgeIds: [...touchedEdges], warnings: unique(warnings) };
521
746
  }
522
747
 
523
748
  // packages/core/src/events.ts
@@ -3930,15 +4155,21 @@ var StrataGate = class _StrataGate {
3930
4155
  event.tags.join(" "),
3931
4156
  (event.temporal.participants ?? []).join(" ")
3932
4157
  ].join(" ")).join(" "));
3933
- const relevantNodes = this.graphNodes.filter((node) => [node.name, ...node.aliases].some((name2) => eventText.includes(normalizeSearchText(name2)))).concat([...this.graphNodes].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, 24));
4158
+ const effectiveViews = this.graphNodes.flatMap((node) => effectiveGraphNodeView(node, this.graphEdges, this.events) ?? []);
4159
+ const effectiveNodes = effectiveViews.map(({ node }) => node);
4160
+ const relevantNodes = effectiveNodes.filter((node) => [node.name, ...node.aliases].some((name2) => name2 && eventText.includes(normalizeSearchText(name2)))).concat([...effectiveNodes].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, 24));
3934
4161
  const existingNodes = [...new Map(relevantNodes.map((node) => [node.id, node])).values()].slice(0, 32);
3935
4162
  const nodeIds = new Set(existingNodes.map(({ id }) => id));
4163
+ const visibleEdgeIds = new Set(effectiveViews.flatMap(({ currentEdges, historicalEdges }) => [
4164
+ ...currentEdges,
4165
+ ...historicalEdges
4166
+ ]).map(({ id }) => id));
3936
4167
  return {
3937
4168
  jobId: job.id,
3938
4169
  projectorVersion: job.projectorVersion,
3939
4170
  events: structuredClone(events),
3940
4171
  existingNodes: structuredClone(existingNodes),
3941
- existingEdges: structuredClone(this.graphEdges.filter(({ fromNodeId, toNodeId }) => nodeIds.has(fromNodeId) && nodeIds.has(toNodeId)).slice(-60))
4172
+ existingEdges: structuredClone(this.graphEdges.filter(({ id, fromNodeId, toNodeId }) => visibleEdgeIds.has(id) && nodeIds.has(fromNodeId) && nodeIds.has(toNodeId)).slice(-60))
3942
4173
  };
3943
4174
  });
3944
4175
  }
@@ -3962,11 +4193,13 @@ var StrataGate = class _StrataGate {
3962
4193
  job.status = "completed";
3963
4194
  job.nodeIds = touched.nodeIds;
3964
4195
  job.edgeIds = touched.edgeIds;
3965
- job.reason = typeof result.reason === "string" ? result.reason.trim().replace(/\s+/g, " ").slice(0, 500) || null : null;
4196
+ const normalizedReason = typeof result.reason === "string" ? result.reason.trim().replace(/\s+/g, " ") : "";
4197
+ const warning = touched.warnings.length > 0 ? `Validation: ${touched.warnings.join(" ")}` : "";
4198
+ job.reason = [warning, normalizedReason].filter(Boolean).join(" ").slice(0, 500) || null;
3966
4199
  job.lastError = null;
3967
4200
  job.nextRetryAt = null;
3968
4201
  job.updatedAt = toUtc8Iso(this.now());
3969
- return touched;
4202
+ return { nodeIds: touched.nodeIds, edgeIds: touched.edgeIds };
3970
4203
  });
3971
4204
  }
3972
4205
  async failGraphProjection(jobId, error) {
@@ -4313,45 +4546,179 @@ var StrataGate = class _StrataGate {
4313
4546
  return job;
4314
4547
  }
4315
4548
  async searchGraphNodes(query, limit = 8) {
4316
- const candidates = this.graphNodes.filter((node) => node.status === "active" || node.status === "disputed");
4317
4549
  const queryTokens = [...new Set(searchTokens(query))];
4318
- const fieldValues = (node) => [
4319
- ["name", node.name],
4320
- ["aliases", node.aliases.join(" ")],
4321
- ["tags", (node.tags ?? []).join(" ")],
4322
- ["type", node.type],
4323
- ["currentState", node.currentState],
4324
- ["facts", node.facts.map((fact) => `${fact.key} ${Array.isArray(fact.value) ? fact.value.join(" ") : fact.value}`).join(" ")],
4325
- ["relations", this.graphEdges.filter((edge) => edge.fromNodeId === node.id || edge.toNodeId === node.id).map(({ relation }) => relation).join(" ")]
4326
- ];
4327
- const ranked = bm25Rank(candidates, query, (node) => weightedSearchTokens([
4328
- [node.name, 6],
4329
- [node.aliases.join(" "), 5],
4330
- [(node.tags ?? []).join(" "), 5],
4331
- [node.type, 2],
4332
- [node.currentState, 4],
4333
- [node.facts.map((fact) => `${fact.key} ${Array.isArray(fact.value) ? fact.value.join(" ") : fact.value}`).join(" "), 4],
4334
- [this.graphEdges.filter((edge) => edge.fromNodeId === node.id || edge.toNodeId === node.id).map(({ relation }) => relation).join(" "), 3]
4335
- ])).filter(({ item }) => {
4336
- const fields = fieldValues(item);
4337
- const matches = fields.filter(([, value]) => {
4338
- const haystack = new Set(searchTokens(value));
4339
- return queryTokens.some((token) => haystack.has(token));
4340
- }).map(([field]) => field);
4341
- if (!matches.some((field) => field !== "relations")) return false;
4342
- return matches.some((field) => field !== "relations");
4550
+ if (queryTokens.length === 0) return [];
4551
+ const views = this.graphNodes.flatMap((node) => effectiveGraphNodeView(node, this.graphEdges, this.events) ?? []);
4552
+ const viewById = new Map(views.map((view) => [view.node.id, view]));
4553
+ const dedupe = (values) => [...new Set(values)];
4554
+ const factValue = (fact) => Array.isArray(fact.value) ? fact.value.join(" ") : fact.value;
4555
+ const endpoint = (nodeId, edge) => {
4556
+ const otherId = edge.fromNodeId === nodeId ? edge.toNodeId : edge.fromNodeId;
4557
+ const other = viewById.get(otherId)?.node;
4558
+ return other ? `${other.name} ${other.aliases.join(" ")}` : "";
4559
+ };
4560
+ const tokenSet = (value) => new Set(searchTokens(value));
4561
+ const hasAny = (value, tokens) => {
4562
+ const available = tokenSet(value);
4563
+ return tokens.some((token) => available.has(token));
4564
+ };
4565
+ const matchDetails = (node) => {
4566
+ const view = viewById.get(node.id);
4567
+ const queryParts = normalizeSearchText(query).split(/[^\p{Letter}\p{Number}_]+/gu).filter(Boolean);
4568
+ const explicitFactKeys = dedupe([...view.currentFacts, ...view.historicalFacts].map(({ key }) => key)).filter((key) => queryParts.includes(normalizeSearchText(key)));
4569
+ const knownFactKeyTokens = new Set(explicitFactKeys.flatMap((key) => [...tokenSet(key)]));
4570
+ const queryKeyTokens = queryTokens.filter((token) => knownFactKeyTokens.has(token));
4571
+ const valueQueryTokens = queryTokens.filter((token) => !queryKeyTokens.includes(token));
4572
+ const meaningfulTokens = queryTokens;
4573
+ const factMatchesFor = (facts) => {
4574
+ if (queryKeyTokens.length === 0) return facts.filter((fact) => hasAny(`${fact.key} ${factValue(fact)}`, meaningfulTokens));
4575
+ const keyFacts = facts.filter((fact) => queryKeyTokens.some((token) => tokenSet(fact.key).has(token)));
4576
+ const directMatches = keyFacts.filter((fact) => valueQueryTokens.length === 0 || valueQueryTokens.some((token) => tokenSet(factValue(fact)).has(token)));
4577
+ return directMatches;
4578
+ };
4579
+ let currentFacts = factMatchesFor(view.currentFacts);
4580
+ let historicalFacts = factMatchesFor(view.historicalFacts);
4581
+ const edgeMatches = (edge, supportingFacts) => {
4582
+ if (queryKeyTokens.length === 0) return hasAny(endpoint(node.id, edge), meaningfulTokens);
4583
+ return supportingFacts.length > 0 && hasAny(endpoint(node.id, edge), valueQueryTokens.length > 0 ? valueQueryTokens : queryTokens);
4584
+ };
4585
+ let currentEdges = view.currentEdges.filter((edge) => edgeMatches(edge, currentFacts));
4586
+ let historicalEdges = view.historicalEdges.filter((edge) => edgeMatches(edge, historicalFacts));
4587
+ if (queryKeyTokens.length === 0) {
4588
+ const overlap = (value) => {
4589
+ const available = tokenSet(value);
4590
+ return meaningfulTokens.filter((token) => available.has(token)).length;
4591
+ };
4592
+ const scoredFacts = [...currentFacts, ...historicalFacts].map((fact) => [fact.id, overlap(`${fact.key} ${factValue(fact)}`)]);
4593
+ const scoredEdges = [...currentEdges, ...historicalEdges].map((edge) => [edge.id, overlap(endpoint(node.id, edge))]);
4594
+ const best = Math.max(0, ...scoredFacts.map(([, score]) => score), ...scoredEdges.map(([, score]) => score));
4595
+ const bestFacts = new Set(scoredFacts.filter(([, score]) => score === best).map(([id]) => id));
4596
+ const bestEdges = new Set(scoredEdges.filter(([, score]) => score === best).map(([id]) => id));
4597
+ currentFacts = currentFacts.filter(({ id }) => bestFacts.has(id));
4598
+ historicalFacts = historicalFacts.filter(({ id }) => bestFacts.has(id));
4599
+ currentEdges = currentEdges.filter(({ id }) => bestEdges.has(id));
4600
+ historicalEdges = historicalEdges.filter(({ id }) => bestEdges.has(id));
4601
+ }
4602
+ const metadataFields = [
4603
+ ["name", node.name],
4604
+ ["aliases", node.aliases.join(" ")],
4605
+ ["tags", (node.tags ?? []).join(" ")],
4606
+ ["type", node.type]
4607
+ ];
4608
+ const metadataMatches = metadataFields.filter(([, value]) => hasAny(value, queryTokens)).map(([field]) => field);
4609
+ const matchedMetadataEventIds = node.metadataProvenance ? dedupe([
4610
+ ...metadataMatches.includes("name") ? node.metadataProvenance.name ?? [] : [],
4611
+ ...(node.metadataProvenance.aliases ?? []).filter(({ value }) => hasAny(value, queryTokens)).flatMap(({ sourceEventIds }) => sourceEventIds),
4612
+ ...(node.metadataProvenance.tags ?? []).filter(({ value }) => hasAny(value, queryTokens)).flatMap(({ sourceEventIds }) => sourceEventIds),
4613
+ ...metadataMatches.includes("type") ? [...view.currentNodeEventIds, ...view.historicalNodeEventIds] : []
4614
+ ]) : metadataMatches.length > 0 ? dedupe([...view.currentNodeEventIds, ...view.historicalNodeEventIds]) : [];
4615
+ const eventStatus = new Map(this.events.map((event) => [event.id, event.status]));
4616
+ const metadataCurrentHit = matchedMetadataEventIds.some((id) => eventStatus.get(id) === "active");
4617
+ const metadataHistoricalHit = matchedMetadataEventIds.some((id) => eventStatus.get(id) === "superseded");
4618
+ const currentRecordHit = currentFacts.length > 0 || currentEdges.length > 0;
4619
+ const historicalRecordHit = historicalFacts.length > 0 || historicalEdges.length > 0;
4620
+ const hasRecordHit = currentRecordHit || historicalRecordHit;
4621
+ const currentHit = currentRecordHit || !hasRecordHit && metadataMatches.length > 0 && metadataCurrentHit;
4622
+ const historicalHit = historicalRecordHit || !hasRecordHit && metadataMatches.length > 0 && metadataHistoricalHit;
4623
+ const relationMatches = [...view.currentEdges, ...view.historicalEdges].some((edge) => hasAny(edge.relation, queryTokens));
4624
+ const matchedFields = [
4625
+ ...metadataMatches,
4626
+ ...currentFacts.length > 0 ? ["currentFacts"] : [],
4627
+ ...historicalFacts.length > 0 ? ["historicalFacts"] : [],
4628
+ ...currentEdges.length > 0 ? ["currentEdgeEndpoints"] : [],
4629
+ ...historicalEdges.length > 0 ? ["historicalEdgeEndpoints"] : [],
4630
+ ...relationMatches ? ["relations"] : []
4631
+ ];
4632
+ return {
4633
+ view,
4634
+ currentFacts,
4635
+ historicalFacts,
4636
+ currentEdges,
4637
+ historicalEdges,
4638
+ metadataMatches,
4639
+ matchedMetadataEventIds,
4640
+ currentHit,
4641
+ historicalHit,
4642
+ matchedFields
4643
+ };
4644
+ };
4645
+ const candidates = views.map(({ node }) => node);
4646
+ const ranked = bm25Rank(candidates, query, (node) => {
4647
+ const view = viewById.get(node.id);
4648
+ return weightedSearchTokens([
4649
+ [node.name, 6],
4650
+ [node.aliases.join(" "), 5],
4651
+ [(node.tags ?? []).join(" "), 5],
4652
+ [node.type, 2],
4653
+ [view.currentFacts.map((fact) => `${fact.key} ${factValue(fact)}`).join(" "), 4],
4654
+ [view.historicalFacts.map((fact) => `${fact.key} ${factValue(fact)}`).join(" "), 2],
4655
+ [view.currentEdges.map((edge) => endpoint(node.id, edge)).join(" "), 4],
4656
+ [view.historicalEdges.map((edge) => endpoint(node.id, edge)).join(" "), 2],
4657
+ [[...view.currentEdges, ...view.historicalEdges].map(({ relation }) => relation).join(" "), 1]
4658
+ ]);
4659
+ }).filter(({ item }) => {
4660
+ const details = matchDetails(item);
4661
+ return details.currentHit || details.historicalHit;
4343
4662
  }).slice(0, Math.max(1, Math.min(20, limit)));
4344
- if (searchTokens(query).length > 0 && ranked.length === 0) return [];
4345
4663
  return ranked.map(({ item: node, score }) => {
4346
- const matchedFields = fieldValues(node).filter(([, value]) => {
4347
- const haystack = new Set(searchTokens(value));
4348
- return queryTokens.some((token) => haystack.has(token));
4349
- }).map(([field]) => field);
4664
+ const details = matchDetails(node);
4665
+ const matchedEventIds = dedupe([
4666
+ ...details.currentFacts.flatMap(({ sourceEventIds }) => sourceEventIds),
4667
+ ...details.historicalFacts.flatMap(({ sourceEventIds }) => sourceEventIds),
4668
+ ...details.currentEdges.flatMap(({ sourceEventIds }) => sourceEventIds),
4669
+ ...details.historicalEdges.flatMap(({ sourceEventIds }) => sourceEventIds)
4670
+ ]);
4671
+ const historicalKeys = new Set(details.historicalFacts.map(({ key }) => key));
4672
+ const historicalRelations = new Set(details.historicalEdges.map(({ relation }) => relation));
4673
+ const currentContextEventIds = dedupe([
4674
+ ...details.view.currentFacts.filter(({ key }) => historicalKeys.has(key)).flatMap(({ sourceEventIds }) => sourceEventIds),
4675
+ ...details.view.currentEdges.filter(({ relation }) => historicalRelations.has(relation)).flatMap(({ sourceEventIds }) => sourceEventIds)
4676
+ ]);
4677
+ const currentContextFacts = details.view.currentFacts.filter(({ key }) => historicalKeys.has(key));
4678
+ const currentContextEdges = details.view.currentEdges.filter(({ relation }) => historicalRelations.has(relation));
4679
+ const nameEventIds = node.metadataProvenance?.name ?? [];
4680
+ const metadataCandidates = dedupe([...details.matchedMetadataEventIds, ...nameEventIds]);
4681
+ const metadataOnly = matchedEventIds.length === 0 && details.metadataMatches.length > 0;
4682
+ const hasVisibleLegacyMetadata = !node.metadataProvenance && Boolean(node.name || node.aliases.length > 0 || (node.tags?.length ?? 0) > 0);
4683
+ const legacyMetadataEventIds = hasVisibleLegacyMetadata ? dedupe([...details.view.currentNodeEventIds, ...details.view.historicalNodeEventIds]) : [];
4684
+ const recordPriority = dedupe([
4685
+ ...matchedEventIds,
4686
+ ...currentContextEventIds
4687
+ ]);
4688
+ const legacyMetadataFits = node.metadataProvenance !== void 0 || !hasVisibleLegacyMetadata || dedupe([...recordPriority, ...legacyMetadataEventIds]).length <= GRAPH_PROVENANCE_LIMIT;
4689
+ const legacyMetadataNotExpanded = hasVisibleLegacyMetadata && !legacyMetadataFits;
4690
+ const prioritized = node.metadataProvenance ? dedupe([
4691
+ ...matchedEventIds,
4692
+ ...metadataOnly ? metadataCandidates : [],
4693
+ ...currentContextEventIds,
4694
+ ...!metadataOnly ? metadataCandidates : []
4695
+ ]) : recordPriority;
4696
+ const provenanceEventIds = dedupe([
4697
+ ...prioritized,
4698
+ ...legacyMetadataFits ? legacyMetadataEventIds : []
4699
+ ]).slice(0, GRAPH_PROVENANCE_LIMIT);
4700
+ const evidenceIds = new Set(provenanceEventIds);
4701
+ const boundedView = boundEffectiveGraphNodeView(details.view, evidenceIds, {
4702
+ preserveLegacyMetadata: legacyMetadataNotExpanded
4703
+ });
4704
+ const bounded = (record2) => {
4705
+ const sourceEventIds = record2.sourceEventIds.filter((id) => evidenceIds.has(id));
4706
+ return sourceEventIds.length > 0 ? { ...record2, sourceEventIds } : null;
4707
+ };
4708
+ const matchType = details.currentHit && details.historicalHit ? "both" : details.historicalHit ? "historical" : "current";
4350
4709
  return {
4351
- node,
4710
+ node: boundedView.node,
4352
4711
  score,
4353
- matchedFields,
4354
- matchReason: `Lexical match in ${matchedFields.join(", ") || "indexed fields"}; score is ranking-only.`
4712
+ matchedFields: details.matchedFields,
4713
+ matchReason: `Meaningful ${matchType} match in ${details.matchedFields.join(", ")}; relation text is ranking-only.`,
4714
+ matchType,
4715
+ currentFacts: dedupe([...details.currentFacts, ...currentContextFacts]).flatMap((record2) => bounded(record2) ?? []),
4716
+ historicalFacts: details.historicalFacts.flatMap((record2) => bounded(record2) ?? []),
4717
+ currentEdges: dedupe([...details.currentEdges, ...currentContextEdges]).flatMap((record2) => bounded(record2) ?? []),
4718
+ historicalEdges: details.historicalEdges.flatMap((record2) => bounded(record2) ?? []),
4719
+ provenanceEventIds,
4720
+ ...legacyMetadataNotExpanded ? { metadataEvidenceStatus: "not_expanded" } : {},
4721
+ timeline: graphTimeline(provenanceEventIds, this.events)
4355
4722
  };
4356
4723
  });
4357
4724
  }
@@ -5148,6 +5515,20 @@ var GRAPH_FACT = {
5148
5515
  additionalProperties: false,
5149
5516
  properties: { key: { type: "string", required: true }, value: { ...VALUE, required: true }, sourceEventIds: { ...STRING_ARRAY, required: true } }
5150
5517
  };
5518
+ var GRAPH_METADATA_ENTRY = {
5519
+ type: "object",
5520
+ additionalProperties: false,
5521
+ properties: { value: { type: "string", required: true }, sourceEventIds: { ...STRING_ARRAY, required: true } }
5522
+ };
5523
+ var GRAPH_METADATA_PROVENANCE = {
5524
+ type: "object",
5525
+ additionalProperties: false,
5526
+ properties: {
5527
+ name: { ...STRING_ARRAY, required: true },
5528
+ aliases: { type: "array", items: GRAPH_METADATA_ENTRY },
5529
+ tags: { type: "array", items: GRAPH_METADATA_ENTRY }
5530
+ }
5531
+ };
5151
5532
  var GRAPH_NODE = {
5152
5533
  type: "object",
5153
5534
  additionalProperties: false,
@@ -5157,6 +5538,7 @@ var GRAPH_NODE = {
5157
5538
  type: { type: "string", enum: ["person", "project", "organization", "tool", "place"], required: true },
5158
5539
  aliases: STRING_ARRAY,
5159
5540
  tags: { ...STRING_ARRAY, required: true },
5541
+ metadataProvenance: { ...GRAPH_METADATA_PROVENANCE, required: true },
5160
5542
  state: { type: "string" },
5161
5543
  facts: { type: "array", items: GRAPH_FACT },
5162
5544
  status: { type: "string", enum: ["active", "superseded", "disputed", "archived"] },
@@ -5263,6 +5645,7 @@ function compactGraphProjectionContext(context) {
5263
5645
  type: node.type,
5264
5646
  aliases: node.aliases.slice(0, 12),
5265
5647
  tags: node.tags?.slice(0, 12),
5648
+ metadataProvenance: node.metadataProvenance,
5266
5649
  currentState: node.currentState.slice(0, 600),
5267
5650
  facts: node.facts.filter(({ status }) => status === "active" || status === "disputed").slice(-12).map((fact) => ({
5268
5651
  key: fact.key,
@@ -5395,7 +5778,7 @@ var DshModelBridge = class {
5395
5778
  const eventIds = new Set(context.events.map((event) => event.id));
5396
5779
  const raw = object(await this.callStructured(
5397
5780
  "graphProjector",
5398
- `Project the supplied Events into the current Knowledge Graph, then call ${STRUCTURED_TOOLS.graphProjector.name} exactly once. Events are the sole source of truth; never use legacy Element data. Return only nodes and edges touched by the supplied Events; never echo unchanged historical graph records. Return at most 24 nodes and 32 edges. Use stable entity nodes for people, projects, organizations, tools, and places. Use aliases to merge spelling/case/separator variants. Give every returned node 1-6 concise semantic role tags such as benchmark, evaluation, memory-plugin, parser, or development-tool; tags describe the node's specific role and never replace its person/project/organization/tool/place type. Reuse stable tag wording when possible. Put attributes in node facts and every relationship in a directed edge using fromRef/toRef\u2014never encode a relationship as a fact string. Prefer concise canonical Chinese relation labels such as \u4F7F\u7528\u3001\u5C5E\u4E8E\u3001\u521B\u5EFA\u3001\u53C2\u4E0E\u3001\u8D21\u732E\u3001\u4F9D\u8D56\u3001\u4F4D\u4E8E\u3001\u76F8\u5173. Every node, fact, and edge must cite only supplied Event ids. Do not return text.`,
5781
+ `Project the supplied Events into the current Knowledge Graph, then call ${STRUCTURED_TOOLS.graphProjector.name} exactly once. Events are the sole source of truth; never use legacy Element data. Return only nodes and edges touched by the supplied Events; never echo unchanged historical graph records. Return at most 24 nodes and 32 edges. Use stable entity nodes for people, projects, organizations, tools, and places. Use aliases to merge spelling/case/separator variants. Give every returned node 1-6 concise semantic role tags such as benchmark, evaluation, memory-plugin, parser, or development-tool; tags describe the node's specific role and never replace its person/project/organization/tool/place type. Reuse stable tag wording when possible. For every node name, alias, and tag, include metadataProvenance with the exact supplied Event ids that support that individual value; never use an unrelated active Event as a substitute. Put attributes in node facts and every relationship in a directed edge using fromRef/toRef\u2014never encode a relationship as a fact string. Prefer concise canonical Chinese relation labels such as \u4F7F\u7528\u3001\u5C5E\u4E8E\u3001\u521B\u5EFA\u3001\u53C2\u4E0E\u3001\u8D21\u732E\u3001\u4F9D\u8D56\u3001\u4F4D\u4E8E\u3001\u76F8\u5173. Every node, fact, edge, and metadata provenance id must cite only supplied Event ids. Do not return text.`,
5399
5782
  compactGraphProjectionContext(context)
5400
5783
  ));
5401
5784
  const nodes = (Array.isArray(raw.nodes) ? raw.nodes : []).flatMap((candidate) => {
@@ -5411,12 +5794,31 @@ var DshModelBridge = class {
5411
5794
  if (sourceEventIds2.length === 0) return [];
5412
5795
  return [{ key: text2(fact.key), value, sourceEventIds: sourceEventIds2 }];
5413
5796
  });
5797
+ const rawMetadata = object(item.metadataProvenance);
5798
+ const metadataEntries2 = (value) => (Array.isArray(value) ? value : []).flatMap((candidate2) => {
5799
+ const entry = object(candidate2);
5800
+ const value2 = text2(entry.value);
5801
+ const sourceEventIds2 = strings2(entry.sourceEventIds).filter((id) => eventIds.has(id));
5802
+ return value2 && sourceEventIds2.length > 0 ? [{ value: value2, sourceEventIds: sourceEventIds2 }] : [];
5803
+ });
5804
+ const metadataName = strings2(rawMetadata.name).filter((id) => eventIds.has(id));
5805
+ if (metadataName.length === 0) {
5806
+ throw new Error(`Graph projection validation failed: node "${text2(item.ref)}" name "${text2(item.name)}" lacks valid metadata provenance.`);
5807
+ }
5808
+ const metadataAliases = metadataEntries2(rawMetadata.aliases);
5809
+ const metadataTags = metadataEntries2(rawMetadata.tags);
5810
+ const metadataProvenance = {
5811
+ ...metadataName.length > 0 ? { name: metadataName } : {},
5812
+ ...metadataAliases.length > 0 ? { aliases: metadataAliases } : {},
5813
+ ...metadataTags.length > 0 ? { tags: metadataTags } : {}
5814
+ };
5414
5815
  return [{
5415
5816
  ref: text2(item.ref),
5416
5817
  name: text2(item.name),
5417
5818
  type,
5418
5819
  aliases: strings2(item.aliases),
5419
5820
  tags: strings2(item.tags).slice(0, 12),
5821
+ ...Object.keys(metadataProvenance).length > 0 ? { metadataProvenance } : {},
5420
5822
  ...text2(item.state) ? { state: text2(item.state) } : {},
5421
5823
  facts,
5422
5824
  ...typeof item.status === "string" ? { status: item.status } : {},
@@ -5605,8 +6007,9 @@ ${JSON_RETRY_INSTRUCTION}`,
5605
6007
  }
5606
6008
  }
5607
6009
  }
6010
+ const validationDetail = lastError?.message ? `: ${lastError.message}` : "";
5608
6011
  throw new ModelJsonResponseError(
5609
- `StrataGate model did not produce a valid ${STRUCTURED_TOOLS[kind].name} call after ${attemptsUsed} attempt${attemptsUsed === 1 ? "" : "s"}`,
6012
+ `StrataGate model did not produce a valid ${STRUCTURED_TOOLS[kind].name} call after ${attemptsUsed} attempt${attemptsUsed === 1 ? "" : "s"}${validationDetail}`,
5610
6013
  { cause: lastError, response: lastResponse }
5611
6014
  );
5612
6015
  }
@@ -6618,31 +7021,36 @@ var StrataGateRuntime = class {
6618
7021
  const openTail = memory.listOpenTail(threadId);
6619
7022
  const activationQuery = [currentUserMessage(session), renderMessages(recentTurns(openTail, 2))].filter(Boolean).join("\n\n");
6620
7023
  const eventHits = activationQuery ? await memory.searchEvents(activationQuery, { limit: 20 }) : [];
6621
- let graphNodes = [];
7024
+ let graphResults = [];
6622
7025
  if (activationQuery && typeof memory.searchGraphNodes === "function") {
6623
- graphNodes = (await memory.searchGraphNodes(activationQuery, 12)).map(({ node }) => node);
7026
+ graphResults = await memory.searchGraphNodes(activationQuery, 12);
6624
7027
  } else if (activationQuery) {
6625
7028
  const legacy = activatedElements(memory, await memory.searchElements(activationQuery, { limit: 12 }));
6626
- graphNodes = legacy.map((item) => ({
6627
- id: item.elementId,
6628
- name: item.name,
6629
- type: item.type,
6630
- aliases: [],
6631
- currentState: "",
6632
- status: "active",
6633
- confidence: item.fact.confidence ?? 0.8,
6634
- sourceEventIds: item.fact.sourceEventIds,
6635
- facts: [{ ...item.fact, confidence: item.fact.confidence ?? 0.8, status: item.fact.status === "disputed" ? "disputed" : item.fact.status === "superseded" ? "superseded" : "active" }],
6636
- createdAt: item.fact.createdAt,
6637
- updatedAt: item.fact.updatedAt
7029
+ graphResults = legacy.map((item) => ({
7030
+ node: {
7031
+ id: item.elementId,
7032
+ name: item.name,
7033
+ type: item.type,
7034
+ aliases: [],
7035
+ currentState: "",
7036
+ status: "active",
7037
+ confidence: item.fact.confidence ?? 0.8,
7038
+ sourceEventIds: item.fact.sourceEventIds,
7039
+ facts: [{ ...item.fact, confidence: item.fact.confidence ?? 0.8, status: item.fact.status === "disputed" ? "disputed" : item.fact.status === "superseded" ? "superseded" : "active" }],
7040
+ createdAt: item.fact.createdAt,
7041
+ updatedAt: item.fact.updatedAt
7042
+ },
7043
+ score: item.score,
7044
+ matchType: item.fact.status === "superseded" ? "historical" : "current",
7045
+ provenanceEventIds: item.fact.sourceEventIds
6638
7046
  }));
6639
7047
  }
6640
7048
  const events = activatedEvents(memory, eventHits);
6641
7049
  const currentBlockIds = new Set(memory.listBlocks().filter((block) => block.threadId === threadId).map(({ id }) => id));
6642
7050
  const currentEventIds = new Set(memory.listEvents().filter((event) => currentBlockIds.has(event.sourceBlockId)).map(({ id }) => id));
6643
7051
  const longTermEvents = events.filter((event) => !currentEventIds.has(event.id)).slice(0, AUTO_EVENT_LIMIT);
6644
- graphNodes = graphNodes.filter((node) => !node.sourceEventIds.some((id) => currentEventIds.has(id))).slice(0, AUTO_ELEMENT_LIMIT);
6645
- return renderActivatedMemory(longTermEvents, graphNodes);
7052
+ graphResults = graphResults.filter((result) => !(result.provenanceEventIds ?? result.node.sourceEventIds).some((id) => currentEventIds.has(id))).slice(0, AUTO_ELEMENT_LIMIT);
7053
+ return renderActivatedMemory(longTermEvents, graphResults);
6646
7054
  }
6647
7055
  /**
6648
7056
  * Replace the just-sealed DSH turns with one native surface message. The raw
@@ -7428,29 +7836,56 @@ var StrataGateRuntime = class {
7428
7836
  async searchGraph(session, query, limit = 8) {
7429
7837
  await this.flush();
7430
7838
  const results = await (await this.space(session)).searchGraphNodes(query, limit);
7431
- return this.batch(session, results.map(({ node }) => ({
7839
+ return this.batch(session, results.map(({ node, provenanceEventIds }) => ({
7432
7840
  ref: `graph-node:${node.id}`,
7433
7841
  target: {
7434
- eventIds: node.sourceEventIds,
7842
+ eventIds: (provenanceEventIds ?? []).slice(0, GRAPH_PROVENANCE_LIMIT),
7435
7843
  elementIds: [],
7436
7844
  citation: citation("graph", node.id, node.name, `graph-node:${node.id}`, "nodeId")
7437
7845
  }
7438
- })), results.map(({ node, score, matchedFields, matchReason }) => compactGraphNode(node, score, matchedFields, matchReason)));
7846
+ })), results.map(compactGraphNode));
7439
7847
  }
7440
7848
  async expandGraphNode(session, id) {
7441
7849
  await this.flush();
7442
7850
  const memory = await this.space(session);
7443
7851
  const node = memory.listGraphNodes().find((candidate) => candidate.id === id);
7444
7852
  if (!node) throw new Error(`Unknown graph node: ${id}`);
7445
- const edges = memory.listGraphEdges().filter(({ fromNodeId, toNodeId }) => fromNodeId === id || toNodeId === id);
7853
+ const view = effectiveGraphNodeView(node, memory.listGraphEdges(), memory.listEvents());
7854
+ if (!view) throw new Error(`Graph node ${id} has no retrievable Event evidence`);
7855
+ const provenanceEventIds = [.../* @__PURE__ */ new Set([
7856
+ ...view.currentFacts.flatMap(({ sourceEventIds }) => sourceEventIds),
7857
+ ...view.currentEdges.flatMap(({ sourceEventIds }) => sourceEventIds),
7858
+ ...view.historicalFacts.flatMap(({ sourceEventIds }) => sourceEventIds),
7859
+ ...view.historicalEdges.flatMap(({ sourceEventIds }) => sourceEventIds),
7860
+ ...view.currentNodeEventIds,
7861
+ ...view.historicalNodeEventIds
7862
+ ])].slice(0, GRAPH_PROVENANCE_LIMIT);
7863
+ const legacyMetadataOverflow = !view.node.metadataProvenance && view.node.sourceEventIds.length > GRAPH_PROVENANCE_LIMIT;
7864
+ const boundedView = boundEffectiveGraphNodeView(view, new Set(provenanceEventIds), {
7865
+ preserveLegacyMetadata: legacyMetadataOverflow
7866
+ });
7867
+ const currentFacts = boundedView.currentFacts;
7868
+ const historicalFacts = boundedView.historicalFacts;
7869
+ const currentEdges = boundedView.currentEdges;
7870
+ const historicalEdges = boundedView.historicalEdges;
7446
7871
  return this.batch(session, [{
7447
7872
  ref: `graph-node:${node.id}:expanded`,
7448
7873
  target: {
7449
- eventIds: [.../* @__PURE__ */ new Set([...node.sourceEventIds, ...edges.flatMap(({ sourceEventIds }) => sourceEventIds)])],
7874
+ eventIds: provenanceEventIds,
7450
7875
  elementIds: [],
7451
- citation: citation("graph", node.id, node.name, `graph-node:${node.id}:expanded`, "nodeId", { expanded: true })
7876
+ citation: citation("graph", node.id, boundedView.node.name, `graph-node:${node.id}:expanded`, "nodeId", { expanded: true })
7452
7877
  }
7453
- }], { node, edges });
7878
+ }], {
7879
+ node: boundedView.node,
7880
+ edges: [...currentEdges, ...historicalEdges],
7881
+ currentFacts,
7882
+ historicalFacts,
7883
+ currentEdges,
7884
+ historicalEdges,
7885
+ provenanceEventIds,
7886
+ ...legacyMetadataOverflow ? { metadataEvidenceStatus: "not_expanded" } : {},
7887
+ timeline: graphTimeline(provenanceEventIds, memory.listEvents())
7888
+ });
7454
7889
  }
7455
7890
  scheduleBlockDerivation(session, memory) {
7456
7891
  const threadId = String(session.id);
@@ -7762,18 +8197,27 @@ function compactEvent(event, score) {
7762
8197
  scoreMeaning: "Ranking-only BM25/RRF score; not confidence, probability, or factual accuracy."
7763
8198
  };
7764
8199
  }
7765
- function compactGraphNode(node, score, matchedFields, matchReason) {
8200
+ function compactGraphNode(result) {
8201
+ const { node, score, matchedFields, matchReason } = result;
7766
8202
  return {
7767
8203
  id: node.id,
7768
8204
  name: node.name,
7769
8205
  type: node.type,
7770
8206
  aliases: node.aliases.map((alias) => compactText2(alias, 160)),
8207
+ ...node.tags ? { tags: node.tags.map((tag) => compactText2(tag, 120)) } : {},
7771
8208
  currentState: compactText2(node.currentState, 500),
7772
8209
  status: node.status,
7773
8210
  rankScore: score,
7774
8211
  ...matchedFields ? { matchedFields } : {},
7775
8212
  ...matchReason ? { matchReason } : {},
7776
- scoreMeaning: "Ranking-only BM25/RRF score; not confidence, probability, or factual accuracy."
8213
+ ...result.metadataEvidenceStatus ? { metadataEvidenceStatus: result.metadataEvidenceStatus } : {},
8214
+ scoreMeaning: "Ranking-only BM25/RRF score; not confidence, probability, or factual accuracy.",
8215
+ ...result.matchType ? { matchType: result.matchType } : {},
8216
+ ...result.currentFacts ? { currentMatches: result.currentFacts } : {},
8217
+ ...result.historicalFacts ? { historicalMatches: result.historicalFacts } : {},
8218
+ ...result.currentEdges ? { currentRelations: result.currentEdges } : {},
8219
+ ...result.historicalEdges ? { historicalRelations: result.historicalEdges } : {},
8220
+ ...result.timeline ? { timeline: result.timeline } : {}
7777
8221
  };
7778
8222
  }
7779
8223
  function compactRawHit(result) {
@@ -7848,7 +8292,7 @@ function activatedElements(memory, relevance) {
7848
8292
  weight
7849
8293
  ]).map(({ item }) => item);
7850
8294
  }
7851
- function renderActivatedMemory(events, graphNodes) {
8295
+ function renderActivatedMemory(events, graphResults) {
7852
8296
  const heading = [
7853
8297
  "[Activated long-term memory]",
7854
8298
  "Historical memory context.",
@@ -7877,13 +8321,23 @@ Events:
7877
8321
  tokens += cost;
7878
8322
  eventCount += 1;
7879
8323
  }
7880
- for (const node of graphNodes) {
8324
+ for (const result of graphResults) {
8325
+ const node = result.node;
7881
8326
  const rendered = JSON.stringify({
7882
8327
  nodeId: node.id,
7883
8328
  name: node.name,
7884
8329
  type: node.type,
8330
+ aliases: node.aliases,
8331
+ tags: node.tags,
8332
+ status: node.status,
8333
+ metadataEvidenceStatus: result.metadataEvidenceStatus,
8334
+ matchType: result.matchType,
7885
8335
  currentState: node.currentState,
7886
- facts: node.facts.filter(({ status }) => status === "active").map(({ key, value, validFrom, validTo }) => ({ key, value, validFrom, validTo }))
8336
+ facts: node.facts.map(({ key, value, status, validFrom, validTo }) => ({ key, value, status, validFrom, validTo })),
8337
+ currentRelations: result.currentEdges?.map(({ relation, status }) => ({ relation, status })),
8338
+ historicalMatches: result.historicalFacts?.map(({ key, value, status, validFrom, validTo }) => ({ key, value, status, validFrom, validTo })),
8339
+ historicalRelations: result.historicalEdges?.map(({ relation, status, validFrom, validTo }) => ({ relation, status, validFrom, validTo })),
8340
+ timeline: result.timeline
7887
8341
  });
7888
8342
  const cost = estimateTokens(`
7889
8343
  KnowledgeGraph:
@@ -7961,7 +8415,7 @@ function registerMemoryTools(ctx, runtime) {
7961
8415
  }));
7962
8416
  ctx.tools.register(defineTool({
7963
8417
  name: "memory_search_graph",
7964
- description: "Search the current Event-backed Knowledge Graph for people, projects, organizations, tools, places, facts, and relations. Returns compact node cards with matchedFields/matchReason; call memory_expand_graph_node for complete facts and edges. rankScore is BM25/RRF ordering only, never confidence or factual accuracy. Results are independently assessable.",
8418
+ description: "Search the Event-backed Knowledge Graph for current state and query-relevant history. Results explicitly label current, historical, or both; historical matches never imply current truth, disputed records remain marked, and Event evidence is bounded to the match. Endpoint names and aliases can match relations, while relation text alone is ranking context. rankScore is ranking-only, never confidence or factual accuracy.",
7965
8419
  parameters: {
7966
8420
  query: { type: "string", required: true },
7967
8421
  limit: { type: "integer", description: "Maximum results, 1-20." }
@@ -7971,7 +8425,7 @@ function registerMemoryTools(ctx, runtime) {
7971
8425
  }));
7972
8426
  ctx.tools.register(defineTool({
7973
8427
  name: "memory_expand_graph_node",
7974
- description: "Expand one Knowledge Graph node with its current facts, directed edges, and supporting Event evidence.",
8428
+ description: "Expand one Knowledge Graph node through the same Event-authoritative view as search: dynamic current state, marked disputed records, retrievable history, and bounded supporting Event evidence. Forgotten or archived Event provenance is never exposed.",
7975
8429
  parameters: { id: { type: "string", required: true } },
7976
8430
  output: jsonOutput,
7977
8431
  execute: async (args, exec) => runtime.expandGraphNode(sessionOf(exec), args.id)
@@ -9095,17 +9549,36 @@ async function sources(runtime, url) {
9095
9549
  events = [event];
9096
9550
  ids = new Set(event.sourceMessageIds);
9097
9551
  } else if (nodeId) {
9098
- const node = snapshot.graphNodes.find(({ id }) => id === nodeId);
9099
- if (!node) throw new AdminHttpError(404, `Unknown graph node: ${nodeId}`);
9100
- events = snapshot.events.filter(({ id }) => node.sourceEventIds.includes(id));
9552
+ const rawNode = snapshot.graphNodes.find(({ id }) => id === nodeId);
9553
+ if (!rawNode) throw new AdminHttpError(404, `Unknown graph node: ${nodeId}`);
9554
+ const view = effectiveGraphNodeView(rawNode, snapshot.graphEdges, snapshot.events);
9555
+ if (!view) throw new AdminHttpError(404, `Graph node has no retrievable Event evidence: ${nodeId}`);
9556
+ const edges = [...view.currentEdges, ...view.historicalEdges];
9557
+ const relatedNodeIds = /* @__PURE__ */ new Set([rawNode.id, ...edges.flatMap(({ fromNodeId, toNodeId }) => [fromNodeId, toNodeId])]);
9558
+ const nodes = snapshot.graphNodes.filter(({ id }) => relatedNodeIds.has(id)).flatMap((candidate) => effectiveGraphNodeView(candidate, snapshot.graphEdges, snapshot.events)?.node ?? []);
9559
+ const metadataEventIds = [
9560
+ ...view.node.metadataProvenance?.name ?? [],
9561
+ ...(view.node.metadataProvenance?.aliases ?? []).flatMap(({ sourceEventIds }) => sourceEventIds),
9562
+ ...(view.node.metadataProvenance?.tags ?? []).flatMap(({ sourceEventIds }) => sourceEventIds)
9563
+ ];
9564
+ const eventIds = /* @__PURE__ */ new Set([
9565
+ ...view.node.sourceEventIds,
9566
+ ...metadataEventIds,
9567
+ ...view.currentFacts.flatMap(({ sourceEventIds }) => sourceEventIds),
9568
+ ...view.historicalFacts.flatMap(({ sourceEventIds }) => sourceEventIds),
9569
+ ...edges.flatMap(({ sourceEventIds }) => sourceEventIds)
9570
+ ]);
9571
+ events = snapshot.events.filter(({ id }) => eventIds.has(id));
9101
9572
  ids = new Set(events.flatMap(({ sourceMessageIds }) => sourceMessageIds));
9102
- const edges = snapshot.graphEdges.filter(({ fromNodeId, toNodeId }) => fromNodeId === node.id || toNodeId === node.id);
9103
- const relatedNodeIds = /* @__PURE__ */ new Set([node.id, ...edges.flatMap(({ fromNodeId, toNodeId }) => [fromNodeId, toNodeId])]);
9104
9573
  return {
9105
9574
  namespace,
9106
- node,
9107
- nodes: snapshot.graphNodes.filter(({ id }) => relatedNodeIds.has(id)),
9575
+ node: view.node,
9576
+ nodes,
9108
9577
  edges,
9578
+ currentFacts: view.currentFacts,
9579
+ historicalFacts: view.historicalFacts,
9580
+ currentEdges: view.currentEdges,
9581
+ historicalEdges: view.historicalEdges,
9109
9582
  events: events.map((event) => eventSummary(event)),
9110
9583
  messages: sourceMessages(snapshot, ids)
9111
9584
  };
@@ -9139,7 +9612,7 @@ async function sources(runtime, url) {
9139
9612
  namespace,
9140
9613
  events: events.map((event) => eventSummary(event, eventId ? snapshot : void 0)),
9141
9614
  ...eventId ? {
9142
- relatedNodes: snapshot.graphNodes.filter(({ id }) => events[0]?.temporal.participantNodeIds?.includes(id)).map(({ id, name: name2, type, aliases }) => ({ id, name: name2, type, aliases }))
9615
+ relatedNodes: snapshot.graphNodes.filter(({ id }) => events[0]?.temporal.participantNodeIds?.includes(id)).flatMap((candidate) => effectiveGraphNodeView(candidate, snapshot.graphEdges, snapshot.events)?.node ?? []).map(({ id, name: name2, type, aliases }) => ({ id, name: name2, type, aliases }))
9143
9616
  } : {},
9144
9617
  elements: elements.map(elementSummary),
9145
9618
  blocks: events.map((event) => snapshot.blocks.find(({ id }) => id === event.sourceBlockId)).filter((block) => Boolean(block)).map((block) => ({ id: block.id, title: block.l0Title, createdAt: block.createdAt })),