@remnic/core 9.69.54 → 9.69.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/access-admin-ops-surface.d.ts +1 -1
  2. package/dist/access-admin-ops-surface.js +2 -2
  3. package/dist/access-authorization-probe.d.ts +1 -1
  4. package/dist/access-authorization-probe.js +3 -3
  5. package/dist/access-boundary.d.ts +1 -1
  6. package/dist/access-boundary.js +2 -2
  7. package/dist/access-cli.js +24 -24
  8. package/dist/access-extraction-force-flush.d.ts +1 -1
  9. package/dist/access-extraction-force-flush.js +2 -2
  10. package/dist/access-http-briefing.d.ts +1 -1
  11. package/dist/access-http-briefing.js +3 -3
  12. package/dist/access-http-lcm-compaction.d.ts +1 -1
  13. package/dist/access-http-lifecycle-flush.d.ts +1 -1
  14. package/dist/access-http-offline-stream.d.ts +1 -1
  15. package/dist/access-http-query.js +2 -2
  16. package/dist/access-http.d.ts +2 -2
  17. package/dist/access-http.js +14 -14
  18. package/dist/access-identity-continuity-surface.js +2 -2
  19. package/dist/access-lcm-surface.d.ts +1 -1
  20. package/dist/access-lcm-surface.js +2 -2
  21. package/dist/access-mcp-shared-context-tools.d.ts +1 -1
  22. package/dist/access-mcp.d.ts +1 -1
  23. package/dist/access-mcp.js +9 -9
  24. package/dist/access-namespace-preflight.js +2 -2
  25. package/dist/access-observe-write-surface.d.ts +1 -1
  26. package/dist/access-observe-write-surface.js +2 -2
  27. package/dist/access-operations-batch.js +3 -3
  28. package/dist/access-operations.d.ts +1 -1
  29. package/dist/access-operations.js +8 -8
  30. package/dist/access-recall-concurrency.d.ts +1 -1
  31. package/dist/access-recall-concurrency.js +2 -2
  32. package/dist/access-recall-response.d.ts +1 -1
  33. package/dist/access-recall-response.js +2 -2
  34. package/dist/access-recall-surface.d.ts +1 -1
  35. package/dist/access-recall-surface.js +2 -2
  36. package/dist/access-schema.d.ts +113 -113
  37. package/dist/{access-service-H-qEdnSs.d.ts → access-service-CUNM5RfA.d.ts} +64 -64
  38. package/dist/access-service-helpers.d.ts +1 -1
  39. package/dist/access-service.d.ts +1 -1
  40. package/dist/access-service.js +2 -2
  41. package/dist/access-surface-catalog.d.ts +1 -1
  42. package/dist/access-surface-catalog.js +4 -4
  43. package/dist/active-recall.js +2 -2
  44. package/dist/{chunk-ZADYEZY4.js → chunk-2SIKTXWX.js} +2 -2
  45. package/dist/{chunk-ZHV6FGVF.js → chunk-3R23DDD3.js} +8 -8
  46. package/dist/{chunk-EQNZC3BH.js → chunk-3S6URXEH.js} +2 -2
  47. package/dist/{chunk-BCXZ4BDS.js → chunk-75YZ5VAI.js} +2 -2
  48. package/dist/{chunk-MXTCV3B5.js → chunk-AHVJ6GLL.js} +2 -2
  49. package/dist/{chunk-6UEM5EOB.js → chunk-AQ55YKEV.js} +2 -2
  50. package/dist/{chunk-VMKAA5SU.js → chunk-BSCNBIF4.js} +2 -2
  51. package/dist/{chunk-M7HFDWOS.js → chunk-FROKVQQY.js} +2 -2
  52. package/dist/{chunk-A4TQFNRX.js → chunk-GRDRVYXJ.js} +3 -3
  53. package/dist/{chunk-EYK3UHAP.js → chunk-H6WOFMOV.js} +2 -2
  54. package/dist/{chunk-SOOWBRMH.js → chunk-IYNCJKBM.js} +8 -8
  55. package/dist/{chunk-3KJTSCSX.js → chunk-J54B5DGD.js} +5 -4
  56. package/dist/chunk-J54B5DGD.js.map +1 -0
  57. package/dist/{chunk-KGY6F4VJ.js → chunk-KIGC55FQ.js} +2 -2
  58. package/dist/{chunk-KQQEZTH6.js → chunk-KQVBR7BC.js} +2 -2
  59. package/dist/{chunk-MTJUIIM4.js → chunk-MTWD4KMM.js} +2 -2
  60. package/dist/{chunk-WJ5KWJLK.js → chunk-NNEWBRUL.js} +2 -2
  61. package/dist/{chunk-5IKW2VP6.js → chunk-NPX6WMFJ.js} +2 -2
  62. package/dist/{chunk-Z22M4JCY.js → chunk-NZLFVLLJ.js} +165 -5
  63. package/dist/chunk-NZLFVLLJ.js.map +1 -0
  64. package/dist/{chunk-QKCXRS5A.js → chunk-OGXXGHW6.js} +11 -1
  65. package/dist/{chunk-QKCXRS5A.js.map → chunk-OGXXGHW6.js.map} +1 -1
  66. package/dist/{chunk-CO7KBRPF.js → chunk-TJNDFNCU.js} +2 -2
  67. package/dist/{chunk-OKOG5MXF.js → chunk-XZRZJHBW.js} +10 -10
  68. package/dist/{chunk-HUQMVI3A.js → chunk-ZBYVR5W4.js} +2 -2
  69. package/dist/{chunk-ZANEMAEV.js → chunk-ZP73EM2X.js} +2 -2
  70. package/dist/{chunk-IEOBAOA6.js → chunk-ZXQPAYCS.js} +2 -2
  71. package/dist/{cli-VGDSn2zY.d.ts → cli-CCc34xV7.d.ts} +1 -1
  72. package/dist/cli.d.ts +2 -2
  73. package/dist/cli.js +19 -19
  74. package/dist/compounding/engine.js +3 -3
  75. package/dist/config.js +2 -2
  76. package/dist/deep-recall-http-glue.d.ts +1 -1
  77. package/dist/deep-recall-mcp-tools.d.ts +1 -1
  78. package/dist/external-wiki-access.d.ts +1 -1
  79. package/dist/external-wiki-access.js +3 -3
  80. package/dist/external-wiki-mcp-tools.d.ts +1 -1
  81. package/dist/index.d.ts +680 -680
  82. package/dist/index.js +24 -24
  83. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  84. package/dist/memory-browse-http-glue.d.ts +1 -1
  85. package/dist/memory-browse-mcp-tools.d.ts +1 -1
  86. package/dist/memory-browse-operations.js +3 -3
  87. package/dist/memory-subject-cli.d.ts +1 -1
  88. package/dist/memory-subject.d.ts +1 -1
  89. package/dist/operator-toolkit.js +3 -3
  90. package/dist/orchestration/maintenance.js +3 -2
  91. package/dist/orchestrator.js +24 -24
  92. package/dist/{public-http-P0cnHzYB.d.ts → public-http-UWVcqMcb.d.ts} +1 -1
  93. package/dist/recall-navigation-http-glue.d.ts +1 -1
  94. package/dist/recall-navigation-mcp-tools.d.ts +1 -1
  95. package/dist/recall-navigation-operations.js +3 -3
  96. package/dist/recall-navigation.js +2 -2
  97. package/dist/resume-bundles.js +3 -3
  98. package/dist/schemas.d.ts +160 -160
  99. package/dist/shared-context/manager.d.ts +24 -8
  100. package/dist/shared-context/manager.js +2 -2
  101. package/dist/support-passport/index.d.ts +4 -4
  102. package/dist/support-passport/index.js +5 -5
  103. package/dist/transfer/types.d.ts +78 -78
  104. package/dist/who-knows-cli.d.ts +1 -1
  105. package/dist/who-knows.d.ts +1 -1
  106. package/package.json +2 -2
  107. package/src/activity/config.test.ts +19 -0
  108. package/src/activity/config.ts +3 -4
  109. package/src/shared-context/governance-wiring.test.ts +153 -0
  110. package/src/shared-context/manager.ts +104 -3
  111. package/src/shared-context/staleness.ts +1 -1
  112. package/dist/chunk-3KJTSCSX.js.map +0 -1
  113. package/dist/chunk-Z22M4JCY.js.map +0 -1
  114. /package/dist/{chunk-ZADYEZY4.js.map → chunk-2SIKTXWX.js.map} +0 -0
  115. /package/dist/{chunk-ZHV6FGVF.js.map → chunk-3R23DDD3.js.map} +0 -0
  116. /package/dist/{chunk-EQNZC3BH.js.map → chunk-3S6URXEH.js.map} +0 -0
  117. /package/dist/{chunk-BCXZ4BDS.js.map → chunk-75YZ5VAI.js.map} +0 -0
  118. /package/dist/{chunk-MXTCV3B5.js.map → chunk-AHVJ6GLL.js.map} +0 -0
  119. /package/dist/{chunk-6UEM5EOB.js.map → chunk-AQ55YKEV.js.map} +0 -0
  120. /package/dist/{chunk-VMKAA5SU.js.map → chunk-BSCNBIF4.js.map} +0 -0
  121. /package/dist/{chunk-M7HFDWOS.js.map → chunk-FROKVQQY.js.map} +0 -0
  122. /package/dist/{chunk-A4TQFNRX.js.map → chunk-GRDRVYXJ.js.map} +0 -0
  123. /package/dist/{chunk-EYK3UHAP.js.map → chunk-H6WOFMOV.js.map} +0 -0
  124. /package/dist/{chunk-SOOWBRMH.js.map → chunk-IYNCJKBM.js.map} +0 -0
  125. /package/dist/{chunk-KGY6F4VJ.js.map → chunk-KIGC55FQ.js.map} +0 -0
  126. /package/dist/{chunk-KQQEZTH6.js.map → chunk-KQVBR7BC.js.map} +0 -0
  127. /package/dist/{chunk-MTJUIIM4.js.map → chunk-MTWD4KMM.js.map} +0 -0
  128. /package/dist/{chunk-WJ5KWJLK.js.map → chunk-NNEWBRUL.js.map} +0 -0
  129. /package/dist/{chunk-5IKW2VP6.js.map → chunk-NPX6WMFJ.js.map} +0 -0
  130. /package/dist/{chunk-CO7KBRPF.js.map → chunk-TJNDFNCU.js.map} +0 -0
  131. /package/dist/{chunk-OKOG5MXF.js.map → chunk-XZRZJHBW.js.map} +0 -0
  132. /package/dist/{chunk-HUQMVI3A.js.map → chunk-ZBYVR5W4.js.map} +0 -0
  133. /package/dist/{chunk-ZANEMAEV.js.map → chunk-ZP73EM2X.js.map} +0 -0
  134. /package/dist/{chunk-IEOBAOA6.js.map → chunk-ZXQPAYCS.js.map} +0 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Regression: #1957 governance controls 2-4 wired into the REAL manager
3
+ * synthesis and curation paths (issue #3012).
4
+ *
5
+ * - Control 2: an expired item never appears in cross-signals sources or the
6
+ * roundtable; a superseded item that still circulates is marked, not dropped.
7
+ * - Control 3: two agents ruling differently on the same feedback ref surface
8
+ * as a disputed section citing both — neither entry is silently dropped.
9
+ * - Control 4: the persisted report carries curator provenance and every
10
+ * synthesized overlap claim cites source item ids (lint tripwire holds).
11
+ */
12
+
13
+ import test from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import os from "node:os";
16
+ import path from "node:path";
17
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
18
+ import { SharedContextManager } from "./manager.js";
19
+ import { parseConfig } from "../config.js";
20
+
21
+ async function makeManager() {
22
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-gov-wiring-"));
23
+ const config = parseConfig({
24
+ sharedContextEnabled: true,
25
+ sharedContextDir: dir,
26
+ });
27
+ const manager = new SharedContextManager(config);
28
+ await manager.ensureStructure();
29
+ return { manager, dir };
30
+ }
31
+
32
+ function today(): string {
33
+ return new Date().toISOString().slice(0, 10);
34
+ }
35
+
36
+ /** Stored-item shape the write path cannot produce (past expiry), but the
37
+ * read path must still classify: legacy or externally written items. */
38
+ async function writeStoredItem(
39
+ dir: string,
40
+ opts: { agent: string; title: string; expiresAt?: string; supersedes?: string },
41
+ ): Promise<string> {
42
+ const dayDir = path.join(dir, "agent-outputs", opts.agent, today());
43
+ await mkdir(dayDir, { recursive: true });
44
+ const fp = path.join(dayDir, `000000-${opts.title.replace(/\s+/g, "-").toLowerCase()}.md`);
45
+ const frontmatter = [
46
+ "kind: agent_output",
47
+ `agent: ${JSON.stringify(opts.agent)}`,
48
+ `createdAt: ${new Date().toISOString()}`,
49
+ `title: ${JSON.stringify(opts.title)}`,
50
+ ...(opts.expiresAt ? [`expiresAt: ${JSON.stringify(opts.expiresAt)}`] : []),
51
+ ...(opts.supersedes ? [`supersedes: ${JSON.stringify(opts.supersedes)}`] : []),
52
+ ];
53
+ await writeFile(fp, `---\n${frontmatter.join("\n")}\n---\n\nbody text about deploy runbook\n`, "utf-8");
54
+ return fp;
55
+ }
56
+
57
+ test("expired items never appear in cross-signals or curation; superseded circulation is marked", async (t) => {
58
+ const { manager, dir } = await makeManager();
59
+ t.after(() => rm(dir, { recursive: true, force: true }));
60
+
61
+ const livePath = await writeStoredItem(dir, { agent: "agent-live", title: "live item" });
62
+ const expiredPath = await writeStoredItem(dir, {
63
+ agent: "agent-old",
64
+ title: "expired item",
65
+ expiresAt: "2020-01-01T00:00:00.000Z",
66
+ });
67
+
68
+ const signals = await manager.synthesizeCrossSignals({ date: today() });
69
+ const paths = signals.report.sources.map((source) => source.path);
70
+ assert.ok(paths.includes(livePath), "live item must appear in sources");
71
+ assert.ok(!paths.includes(expiredPath), "expired item must never appear in sources");
72
+ assert.equal(signals.report.staleness.expiredDropped, 1);
73
+ assert.deepEqual(signals.report.staleness.supersededStillCirculating, []);
74
+
75
+ const curation = await manager.curateDaily({ date: today() });
76
+ const roundtable = await readFile(curation.roundtablePath, "utf-8");
77
+ assert.ok(!roundtable.includes(expiredPath), "expired item must never appear in the roundtable");
78
+ assert.match(roundtable, /## Staleness/);
79
+ assert.match(roundtable, /Expired items dropped: 1/);
80
+ });
81
+
82
+ test("a superseded item that still circulates is marked, not dropped", async (t) => {
83
+ const { manager, dir } = await makeManager();
84
+ t.after(() => rm(dir, { recursive: true, force: true }));
85
+
86
+ const olderPath = await writeStoredItem(dir, { agent: "agent-a", title: "older item" });
87
+ await writeStoredItem(dir, { agent: "agent-b", title: "newer item", supersedes: olderPath });
88
+
89
+ const signals = await manager.synthesizeCrossSignals({ date: today() });
90
+ assert.deepEqual(signals.report.staleness.supersededStillCirculating, [olderPath]);
91
+ const paths = signals.report.sources.map((source) => source.path);
92
+ assert.ok(paths.includes(olderPath), "superseded item still circulates (marked, not dropped)");
93
+
94
+ const curation = await manager.curateDaily({ date: today() });
95
+ const roundtable = await readFile(curation.roundtablePath, "utf-8");
96
+ assert.match(roundtable, /Superseded items still circulating: 1/);
97
+ });
98
+
99
+ test("contradictory agent rulings on the same ref surface as disputed, both preserved", async (t) => {
100
+ const { manager, dir } = await makeManager();
101
+ t.after(() => rm(dir, { recursive: true, force: true }));
102
+
103
+ await manager.appendFeedback({
104
+ agent: "agent-a",
105
+ decision: "approved",
106
+ reason: "matches the runbook",
107
+ date: `${today()}T10:00:00.000Z`,
108
+ refs: ["out-0001"],
109
+ });
110
+ await manager.appendFeedback({
111
+ agent: "agent-b",
112
+ decision: "rejected",
113
+ reason: "misses the rollback step",
114
+ date: `${today()}T11:00:00.000Z`,
115
+ refs: ["out-0001"],
116
+ });
117
+
118
+ const signals = await manager.synthesizeCrossSignals({ date: today() });
119
+ assert.deepEqual(signals.report.disputed, [
120
+ { ref: "out-0001", agents: ["agent-a", "agent-b"], decisions: ["approved", "rejected"] },
121
+ ]);
122
+ assert.equal(signals.report.feedbackEntries.length, 2, "neither disputed entry is dropped");
123
+
124
+ const curation = await manager.curateDaily({ date: today() });
125
+ const roundtable = await readFile(curation.roundtablePath, "utf-8");
126
+ assert.match(roundtable, /## Disputed/);
127
+ assert.match(roundtable, /`out-0001` disputed by agent-a, agent-b/);
128
+ assert.match(roundtable, /agent-a\] approved/);
129
+ assert.match(roundtable, /agent-b\] rejected/);
130
+ });
131
+
132
+ test("curation report carries provenance and cited synthesized claims", async (t) => {
133
+ const { manager, dir } = await makeManager();
134
+ t.after(() => rm(dir, { recursive: true, force: true }));
135
+
136
+ // Two agents on one topic produce a synthesized overlap claim.
137
+ await manager.writeAgentOutput({ agentId: "agent-a", title: "deploy runbook notes", content: "deploy runbook steps" });
138
+ await manager.writeAgentOutput({ agentId: "agent-b", title: "deploy runbook review", content: "deploy runbook feedback" });
139
+
140
+ const signals = await manager.synthesizeCrossSignals({ date: today() });
141
+ assert.equal(signals.report.provenance.actor, "shared-context:curation");
142
+ assert.ok(!Number.isNaN(Date.parse(signals.report.provenance.at)), "provenance.at is a valid instant");
143
+ assert.ok(signals.report.overlaps.length > 0, "shared topic produces an overlap claim");
144
+ for (const overlap of signals.report.overlaps) {
145
+ assert.ok(overlap.sourcePaths.length > 0, `overlap ${overlap.token} cites source item ids`);
146
+ }
147
+
148
+ // The curation self-check gates the roundtable: it throws on an
149
+ // uncited synthesized claim and passes when every claim cites.
150
+ const curation = await manager.curateDaily({ date: today() });
151
+ const roundtable = await readFile(curation.roundtablePath, "utf-8");
152
+ assert.match(roundtable, /\[sources: [^\]]+\]/, "synthesized overlap claims cite their sources");
153
+ });
@@ -15,6 +15,10 @@ import {
15
15
  resolveWriteOrigin,
16
16
  } from "./envelope-io.js";
17
17
  import type { SharedAuthority, SharedEnvelope } from "./governance.js";
18
+ import { detectContradictionPair } from "./contradictions.js";
19
+ import { lintCuratedClaims } from "./curation-lint.js";
20
+ import { stampProvenance } from "./provenance.js";
21
+ import { filterLiveEnvelopes, markSupersededCirculation } from "./staleness.js";
18
22
 
19
23
  export const SharedFeedbackEntrySchema = z.object({
20
24
  agent: z.string().min(1),
@@ -341,6 +345,15 @@ interface SharedCrossSignalReport {
341
345
  feedbackEntries: SharedFeedbackEntry[];
342
346
  sources: SharedCrossSignalSource[];
343
347
  overlaps: SharedCrossSignalOverlap[];
348
+ /** Control 2 (issue #1957): expiry drops and supersession circulation. */
349
+ staleness: {
350
+ expiredDropped: number;
351
+ supersededStillCirculating: string[];
352
+ };
353
+ /** Control 3 (issue #1957): refs two agents ruled on differently. */
354
+ disputed: Array<{ ref: string; agents: string[]; decisions: string[] }>;
355
+ /** Control 4 (issue #1957): who synthesized this report and when. */
356
+ provenance: { actor: string; at: string };
344
357
  semantic: {
345
358
  enabled: boolean;
346
359
  applied: boolean;
@@ -418,6 +431,23 @@ function formatOverlapLine(entry: SharedCrossSignalOverlap): string {
418
431
  return `- \`${entry.token}\` (${entry.agentCount} agents: ${markdownLineList(entry.agents)}) [sources: ${markdownLineList(entry.sourcePaths)}]`;
419
432
  }
420
433
 
434
+ function formatStalenessLines(report: SharedCrossSignalReport): string[] {
435
+ const circulating = report.staleness.supersededStillCirculating;
436
+ return [
437
+ `- Expired items dropped: ${report.staleness.expiredDropped}`,
438
+ circulating.length === 0
439
+ ? "- Superseded items still circulating: 0"
440
+ : `- Superseded items still circulating: ${circulating.length} (${markdownLineList(circulating)})`,
441
+ ];
442
+ }
443
+
444
+ function formatDisputedLines(report: SharedCrossSignalReport): string[] {
445
+ if (report.disputed.length === 0) return ["- No contradictory agent judgments recorded."];
446
+ return report.disputed.map((entry) =>
447
+ `- \`${entry.ref}\` disputed by ${markdownLineList(entry.agents)} (decisions: ${markdownLineList(entry.decisions)})`
448
+ );
449
+ }
450
+
421
451
  async function writeFileAtomic(filePath: string, content: string): Promise<void> {
422
452
  await mkdir(path.dirname(filePath), { recursive: true });
423
453
  const tempPath = path.join(
@@ -730,6 +760,20 @@ export class SharedContextManager {
730
760
  || a.title.localeCompare(b.title),
731
761
  );
732
762
 
763
+ // Control 2 (issue #1957): expired items never enter the working set;
764
+ // items another live item supersedes are marked while they circulate.
765
+ const liveOutputs = filterLiveEnvelopes(
766
+ outputs.map((output) => ({ output, expiresAt: output.envelope.expiresAt })),
767
+ Date.now(),
768
+ ).map((entry) => entry.output);
769
+ const expiredDropped = outputs.length - liveOutputs.length;
770
+ const supersededStillCirculating = markSupersededCirculation(
771
+ liveOutputs.map((output) => ({ id: output.path, supersedes: output.envelope.supersedes })),
772
+ )
773
+ .filter((item) => item.circulating === true)
774
+ .map((item) => item.id)
775
+ .sort();
776
+
733
777
  // Collect feedback entries for the day.
734
778
  const feedback: SharedFeedbackEntry[] = [];
735
779
  try {
@@ -749,7 +793,7 @@ export class SharedContextManager {
749
793
  // ignore
750
794
  }
751
795
 
752
- const sources: SharedCrossSignalSource[] = outputs.map((output) => {
796
+ const sources: SharedCrossSignalSource[] = liveOutputs.map((output) => {
753
797
  const body = stripYamlFrontmatter(output.raw);
754
798
  return {
755
799
  agent: output.agent,
@@ -828,15 +872,44 @@ export class SharedContextManager {
828
872
  feedbackByDecision[entry.decision] += 1;
829
873
  }
830
874
 
831
- const report: SharedCrossSignalReport = {
875
+ // Control 3 (issue #1957): feedback refs are claim keys; two agents
876
+ // ruling differently on the same ref surface as disputed, never silently
877
+ // averaged. Pairwise scan is bounded by one day's feedback entries.
878
+ const claimItems = feedback.map((entry, index) => ({
879
+ id: `${entry.agent}#${index}`,
880
+ claims: Object.fromEntries((entry.refs ?? []).map((ref) => [ref, entry.decision])),
881
+ }));
882
+ const disputedByRef = new Map<string, { agents: Set<string>; decisions: Set<string> }>();
883
+ for (let i = 0; i < claimItems.length; i++) {
884
+ for (let j = i + 1; j < claimItems.length; j++) {
885
+ const pair = detectContradictionPair(claimItems[i]!, claimItems[j]!);
886
+ if (pair.kind !== "conflict") continue;
887
+ for (const ref of pair.keys) {
888
+ const slot = disputedByRef.get(ref) ?? { agents: new Set<string>(), decisions: new Set<string>() };
889
+ slot.agents.add(feedback[i]!.agent);
890
+ slot.agents.add(feedback[j]!.agent);
891
+ slot.decisions.add(feedback[i]!.decision);
892
+ slot.decisions.add(feedback[j]!.decision);
893
+ disputedByRef.set(ref, slot);
894
+ }
895
+ }
896
+ }
897
+ const disputed = [...disputedByRef.entries()]
898
+ .map(([ref, slot]) => ({ ref, agents: [...slot.agents].sort(), decisions: [...slot.decisions].sort() }))
899
+ .sort((a, b) => a.ref.localeCompare(b.ref));
900
+
901
+ const generatedAt = new Date().toISOString();
902
+ const baseReport = {
832
903
  date,
833
- generatedAt: new Date().toISOString(),
904
+ generatedAt,
834
905
  sourceCount: sources.length,
835
906
  feedbackCount: feedback.length,
836
907
  feedbackByDecision,
837
908
  feedbackEntries: [...feedback].sort(compareFeedbackPriority),
838
909
  sources,
839
910
  overlaps: mergedOverlaps,
911
+ staleness: { expiredDropped, supersededStillCirculating },
912
+ disputed,
840
913
  semantic: {
841
914
  enabled: semanticEnabled,
842
915
  applied: semanticApplied,
@@ -846,6 +919,8 @@ export class SharedContextManager {
846
919
  addedOverlapCount: semanticAddedOverlapCount,
847
920
  },
848
921
  };
922
+ // Control 4 (issue #1957): stamp who synthesized the report and when.
923
+ const report = stampProvenance(baseReport, { actor: "shared-context:curation", at: generatedAt });
849
924
 
850
925
  const crossSignalsPath = path.join(this.crossSignalsDir, `${date}.json`);
851
926
  await writeFileAtomic(crossSignalsPath, `${JSON.stringify(report, null, 2)}\n`);
@@ -879,6 +954,12 @@ export class SharedContextManager {
879
954
  `- Semantic enhancer: ${semanticEnabled ? (semanticTimedOut ? "enabled (timed out, fail-open)" : semanticApplied ? "enabled (applied)" : "enabled (no additional overlaps)") : "disabled"}`,
880
955
  `- JSON report: ${crossSignalsPath}`,
881
956
  "",
957
+ "## Staleness",
958
+ ...formatStalenessLines(report),
959
+ "",
960
+ "## Disputed",
961
+ ...formatDisputedLines(report),
962
+ "",
882
963
  "## Recurring Themes",
883
964
  ...recurringThemeLines,
884
965
  "",
@@ -916,6 +997,19 @@ export class SharedContextManager {
916
997
  const date = opts.date;
917
998
  const maxChars = Math.max(2_000, opts.maxChars ?? 20_000);
918
999
  const crossSignals = await this.synthesizeCrossSignalsUnlocked({ date });
1000
+ // Control 4 (issue #1957): every synthesized claim must cite at least one
1001
+ // item this run actually had. Overlaps are built from sources, so this is
1002
+ // a tripwire — it fires only if a synthesized section stops citing.
1003
+ const lintFindings = lintCuratedClaims({
1004
+ claims: crossSignals.report.overlaps.map((overlap) => ({
1005
+ claimId: `overlap:${overlap.token}`,
1006
+ citedItemIds: overlap.sourcePaths,
1007
+ })),
1008
+ availableItemIds: crossSignals.report.sources.map((source) => source.path),
1009
+ });
1010
+ if (lintFindings.length > 0) {
1011
+ throw new Error(`shared-context curation self-check failed: ${JSON.stringify(lintFindings)}`);
1012
+ }
919
1013
  const feedbackLines = crossSignals.report.feedbackEntries.length === 0
920
1014
  ? ["- (none)"]
921
1015
  : crossSignals.report.feedbackEntries.map((entry) => formatFeedbackLine(entry));
@@ -944,8 +1038,15 @@ export class SharedContextManager {
944
1038
  `- Cross-signals markdown: ${crossSignals.crossSignalsMarkdownPath}`,
945
1039
  ...overlapBullets,
946
1040
  "",
1041
+ "## Staleness",
1042
+ ...formatStalenessLines(crossSignals.report),
1043
+ "",
1044
+ "## Disputed",
1045
+ ...formatDisputedLines(crossSignals.report),
1046
+ "",
947
1047
  ];
948
1048
 
1049
+
949
1050
  const out = md.join("\n");
950
1051
  const trimmed = out.length > maxChars ? out.slice(0, maxChars) + "\n\n...(trimmed)\n" : out;
951
1052
 
@@ -27,7 +27,7 @@ export function filterLiveEnvelopes<T extends { expiresAt?: string }>(
27
27
  */
28
28
  export function markSupersededCirculation<T extends SharedStalenessItem>(
29
29
  items: readonly T[],
30
- ): T[] {
30
+ ): (T & { circulating?: boolean })[] {
31
31
  const supersededIds = new Set<string>();
32
32
  for (const item of items) {
33
33
  if (item.supersedes) supersededIds.add(item.supersedes);