stratagate-dsh 0.2.74 → 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
  }
@@ -6042,11 +6445,68 @@ function workspaceDisplayName(cwd) {
6042
6445
  function feedbackText(value, maximum) {
6043
6446
  return typeof value === "string" ? value.trim().slice(0, maximum) : "";
6044
6447
  }
6448
+ var FEEDBACK_MARKDOWN_HEADINGS = {
6449
+ description: "\u95EE\u9898\u63CF\u8FF0",
6450
+ reproduction: "\u590D\u73B0\u6B65\u9AA4",
6451
+ expected: "\u9884\u671F\u884C\u4E3A",
6452
+ actual: "\u5B9E\u9645\u884C\u4E3A",
6453
+ errorContext: "\u76F8\u5173\u9519\u8BEF\u4FE1\u606F"
6454
+ };
6455
+ function patchFeedbackMarkdown(markdown, input) {
6456
+ let result = markdown.trim();
6457
+ for (const [key, heading] of Object.entries(FEEDBACK_MARKDOWN_HEADINGS)) {
6458
+ if (!Object.prototype.hasOwnProperty.call(input, key)) continue;
6459
+ const raw = input[key];
6460
+ const text3 = Array.isArray(raw) ? raw.map((item, index) => {
6461
+ const value = feedbackText(item, 2e3);
6462
+ return value ? `${index + 1}. ${value}` : "";
6463
+ }).filter(Boolean).join("\n") : feedbackText(raw, key === "description" ? 2e4 : key === "expected" || key === "actual" ? 1e4 : 2e4);
6464
+ const marker = `## ${heading}`;
6465
+ const lines = result.split("\n");
6466
+ let fence = null;
6467
+ let sectionStart = -1;
6468
+ let sectionEnd = lines.length;
6469
+ for (let index = 0; index < lines.length; index += 1) {
6470
+ const line = (lines[index] ?? "").replace(/\r$/, "");
6471
+ if (fence) {
6472
+ const closingFence = line.match(/^ {0,3}(`+|~+)[ \t]*$/);
6473
+ if (closingFence?.[1]?.[0] === fence.character && closingFence[1].length >= fence.length) fence = null;
6474
+ continue;
6475
+ }
6476
+ const openingFence = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
6477
+ if (openingFence?.[1] && (openingFence[1][0] === "~" || !openingFence[2]?.includes("`"))) {
6478
+ fence = { character: openingFence[1][0], length: openingFence[1].length };
6479
+ continue;
6480
+ }
6481
+ if (line === marker && sectionStart < 0) {
6482
+ sectionStart = index;
6483
+ continue;
6484
+ }
6485
+ if (sectionStart >= 0 && /^##\s+\S/.test(line)) {
6486
+ sectionEnd = index;
6487
+ break;
6488
+ }
6489
+ }
6490
+ if (sectionStart < 0) {
6491
+ if (text3) result = `${result ? `${result}
6492
+
6493
+ ` : ""}${marker}
6494
+
6495
+ ${text3}`;
6496
+ continue;
6497
+ }
6498
+ const replacement = text3 ? [marker, "", ...text3.split("\n")] : [];
6499
+ lines.splice(sectionStart, sectionEnd - sectionStart, ...replacement);
6500
+ result = lines.join("\n").trim();
6501
+ }
6502
+ return result.trim();
6503
+ }
6045
6504
  function normalizeFeedbackDraft(input, previous) {
6046
6505
  const has = (key) => Object.prototype.hasOwnProperty.call(input, key);
6047
6506
  const replacesStructuredBody = has("bodyMarkdown");
6507
+ const updatesStructuredBody = ["description", "reproduction", "expected", "actual", "errorContext"].some((key) => has(key));
6048
6508
  const reproduction = replacesStructuredBody ? [] : has("reproduction") ? (Array.isArray(input.reproduction) ? input.reproduction : []).map((value) => feedbackText(value, 2e3)).filter(Boolean).slice(0, 20) : previous?.reproduction ?? [];
6049
- const bodyMarkdown = has("bodyMarkdown") ? feedbackText(input.bodyMarkdown, 5e4) : previous?.bodyMarkdown;
6509
+ const bodyMarkdown = has("bodyMarkdown") ? feedbackText(input.bodyMarkdown, 5e4) : updatesStructuredBody && previous?.bodyMarkdown ? patchFeedbackMarkdown(previous.bodyMarkdown, input) : previous?.bodyMarkdown;
6050
6510
  return {
6051
6511
  title: has("title") ? feedbackText(input.title, 240) : previous?.title ?? "",
6052
6512
  description: replacesStructuredBody ? "" : has("description") ? feedbackText(input.description, 2e4) : previous?.description ?? "",
@@ -6561,31 +7021,36 @@ var StrataGateRuntime = class {
6561
7021
  const openTail = memory.listOpenTail(threadId);
6562
7022
  const activationQuery = [currentUserMessage(session), renderMessages(recentTurns(openTail, 2))].filter(Boolean).join("\n\n");
6563
7023
  const eventHits = activationQuery ? await memory.searchEvents(activationQuery, { limit: 20 }) : [];
6564
- let graphNodes = [];
7024
+ let graphResults = [];
6565
7025
  if (activationQuery && typeof memory.searchGraphNodes === "function") {
6566
- graphNodes = (await memory.searchGraphNodes(activationQuery, 12)).map(({ node }) => node);
7026
+ graphResults = await memory.searchGraphNodes(activationQuery, 12);
6567
7027
  } else if (activationQuery) {
6568
7028
  const legacy = activatedElements(memory, await memory.searchElements(activationQuery, { limit: 12 }));
6569
- graphNodes = legacy.map((item) => ({
6570
- id: item.elementId,
6571
- name: item.name,
6572
- type: item.type,
6573
- aliases: [],
6574
- currentState: "",
6575
- status: "active",
6576
- confidence: item.fact.confidence ?? 0.8,
6577
- sourceEventIds: item.fact.sourceEventIds,
6578
- facts: [{ ...item.fact, confidence: item.fact.confidence ?? 0.8, status: item.fact.status === "disputed" ? "disputed" : item.fact.status === "superseded" ? "superseded" : "active" }],
6579
- createdAt: item.fact.createdAt,
6580
- 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
6581
7046
  }));
6582
7047
  }
6583
7048
  const events = activatedEvents(memory, eventHits);
6584
7049
  const currentBlockIds = new Set(memory.listBlocks().filter((block) => block.threadId === threadId).map(({ id }) => id));
6585
7050
  const currentEventIds = new Set(memory.listEvents().filter((event) => currentBlockIds.has(event.sourceBlockId)).map(({ id }) => id));
6586
7051
  const longTermEvents = events.filter((event) => !currentEventIds.has(event.id)).slice(0, AUTO_EVENT_LIMIT);
6587
- graphNodes = graphNodes.filter((node) => !node.sourceEventIds.some((id) => currentEventIds.has(id))).slice(0, AUTO_ELEMENT_LIMIT);
6588
- 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);
6589
7054
  }
6590
7055
  /**
6591
7056
  * Replace the just-sealed DSH turns with one native surface message. The raw
@@ -6704,7 +7169,8 @@ var StrataGateRuntime = class {
6704
7169
  }
6705
7170
  async prepareFeedback(session, input) {
6706
7171
  const namespace = this.namespaceFor(session);
6707
- const draft = normalizeFeedbackDraft(input);
7172
+ const previous = this.loadFeedbackDraft(namespace);
7173
+ const draft = normalizeFeedbackDraft(input, previous);
6708
7174
  this.saveFeedbackDraft(namespace, draft);
6709
7175
  const feedbackUrl = feedbackDraftUrl(namespace, this.feedbackOrigin());
6710
7176
  return {
@@ -7370,29 +7836,56 @@ var StrataGateRuntime = class {
7370
7836
  async searchGraph(session, query, limit = 8) {
7371
7837
  await this.flush();
7372
7838
  const results = await (await this.space(session)).searchGraphNodes(query, limit);
7373
- return this.batch(session, results.map(({ node }) => ({
7839
+ return this.batch(session, results.map(({ node, provenanceEventIds }) => ({
7374
7840
  ref: `graph-node:${node.id}`,
7375
7841
  target: {
7376
- eventIds: node.sourceEventIds,
7842
+ eventIds: (provenanceEventIds ?? []).slice(0, GRAPH_PROVENANCE_LIMIT),
7377
7843
  elementIds: [],
7378
7844
  citation: citation("graph", node.id, node.name, `graph-node:${node.id}`, "nodeId")
7379
7845
  }
7380
- })), results.map(({ node, score, matchedFields, matchReason }) => compactGraphNode(node, score, matchedFields, matchReason)));
7846
+ })), results.map(compactGraphNode));
7381
7847
  }
7382
7848
  async expandGraphNode(session, id) {
7383
7849
  await this.flush();
7384
7850
  const memory = await this.space(session);
7385
7851
  const node = memory.listGraphNodes().find((candidate) => candidate.id === id);
7386
7852
  if (!node) throw new Error(`Unknown graph node: ${id}`);
7387
- 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;
7388
7871
  return this.batch(session, [{
7389
7872
  ref: `graph-node:${node.id}:expanded`,
7390
7873
  target: {
7391
- eventIds: [.../* @__PURE__ */ new Set([...node.sourceEventIds, ...edges.flatMap(({ sourceEventIds }) => sourceEventIds)])],
7874
+ eventIds: provenanceEventIds,
7392
7875
  elementIds: [],
7393
- 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 })
7394
7877
  }
7395
- }], { 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
+ });
7396
7889
  }
7397
7890
  scheduleBlockDerivation(session, memory) {
7398
7891
  const threadId = String(session.id);
@@ -7704,18 +8197,27 @@ function compactEvent(event, score) {
7704
8197
  scoreMeaning: "Ranking-only BM25/RRF score; not confidence, probability, or factual accuracy."
7705
8198
  };
7706
8199
  }
7707
- function compactGraphNode(node, score, matchedFields, matchReason) {
8200
+ function compactGraphNode(result) {
8201
+ const { node, score, matchedFields, matchReason } = result;
7708
8202
  return {
7709
8203
  id: node.id,
7710
8204
  name: node.name,
7711
8205
  type: node.type,
7712
8206
  aliases: node.aliases.map((alias) => compactText2(alias, 160)),
8207
+ ...node.tags ? { tags: node.tags.map((tag) => compactText2(tag, 120)) } : {},
7713
8208
  currentState: compactText2(node.currentState, 500),
7714
8209
  status: node.status,
7715
8210
  rankScore: score,
7716
8211
  ...matchedFields ? { matchedFields } : {},
7717
8212
  ...matchReason ? { matchReason } : {},
7718
- 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 } : {}
7719
8221
  };
7720
8222
  }
7721
8223
  function compactRawHit(result) {
@@ -7790,7 +8292,7 @@ function activatedElements(memory, relevance) {
7790
8292
  weight
7791
8293
  ]).map(({ item }) => item);
7792
8294
  }
7793
- function renderActivatedMemory(events, graphNodes) {
8295
+ function renderActivatedMemory(events, graphResults) {
7794
8296
  const heading = [
7795
8297
  "[Activated long-term memory]",
7796
8298
  "Historical memory context.",
@@ -7819,13 +8321,23 @@ Events:
7819
8321
  tokens += cost;
7820
8322
  eventCount += 1;
7821
8323
  }
7822
- for (const node of graphNodes) {
8324
+ for (const result of graphResults) {
8325
+ const node = result.node;
7823
8326
  const rendered = JSON.stringify({
7824
8327
  nodeId: node.id,
7825
8328
  name: node.name,
7826
8329
  type: node.type,
8330
+ aliases: node.aliases,
8331
+ tags: node.tags,
8332
+ status: node.status,
8333
+ metadataEvidenceStatus: result.metadataEvidenceStatus,
8334
+ matchType: result.matchType,
7827
8335
  currentState: node.currentState,
7828
- 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
7829
8341
  });
7830
8342
  const cost = estimateTokens(`
7831
8343
  KnowledgeGraph:
@@ -7903,7 +8415,7 @@ function registerMemoryTools(ctx, runtime) {
7903
8415
  }));
7904
8416
  ctx.tools.register(defineTool({
7905
8417
  name: "memory_search_graph",
7906
- 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.",
7907
8419
  parameters: {
7908
8420
  query: { type: "string", required: true },
7909
8421
  limit: { type: "integer", description: "Maximum results, 1-20." }
@@ -7913,7 +8425,7 @@ function registerMemoryTools(ctx, runtime) {
7913
8425
  }));
7914
8426
  ctx.tools.register(defineTool({
7915
8427
  name: "memory_expand_graph_node",
7916
- 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.",
7917
8429
  parameters: { id: { type: "string", required: true } },
7918
8430
  output: jsonOutput,
7919
8431
  execute: async (args, exec) => runtime.expandGraphNode(sessionOf(exec), args.id)
@@ -9037,17 +9549,36 @@ async function sources(runtime, url) {
9037
9549
  events = [event];
9038
9550
  ids = new Set(event.sourceMessageIds);
9039
9551
  } else if (nodeId) {
9040
- const node = snapshot.graphNodes.find(({ id }) => id === nodeId);
9041
- if (!node) throw new AdminHttpError(404, `Unknown graph node: ${nodeId}`);
9042
- 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));
9043
9572
  ids = new Set(events.flatMap(({ sourceMessageIds }) => sourceMessageIds));
9044
- const edges = snapshot.graphEdges.filter(({ fromNodeId, toNodeId }) => fromNodeId === node.id || toNodeId === node.id);
9045
- const relatedNodeIds = /* @__PURE__ */ new Set([node.id, ...edges.flatMap(({ fromNodeId, toNodeId }) => [fromNodeId, toNodeId])]);
9046
9573
  return {
9047
9574
  namespace,
9048
- node,
9049
- nodes: snapshot.graphNodes.filter(({ id }) => relatedNodeIds.has(id)),
9575
+ node: view.node,
9576
+ nodes,
9050
9577
  edges,
9578
+ currentFacts: view.currentFacts,
9579
+ historicalFacts: view.historicalFacts,
9580
+ currentEdges: view.currentEdges,
9581
+ historicalEdges: view.historicalEdges,
9051
9582
  events: events.map((event) => eventSummary(event)),
9052
9583
  messages: sourceMessages(snapshot, ids)
9053
9584
  };
@@ -9081,7 +9612,7 @@ async function sources(runtime, url) {
9081
9612
  namespace,
9082
9613
  events: events.map((event) => eventSummary(event, eventId ? snapshot : void 0)),
9083
9614
  ...eventId ? {
9084
- 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 }))
9085
9616
  } : {},
9086
9617
  elements: elements.map(elementSummary),
9087
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 })),