@remnic/core 9.69.53 → 9.69.55

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 (102) hide show
  1. package/dist/access-admin-ops-surface.js +2 -2
  2. package/dist/access-authorization-probe.js +3 -3
  3. package/dist/access-boundary.js +2 -2
  4. package/dist/access-cli.js +24 -24
  5. package/dist/access-extraction-force-flush.js +2 -2
  6. package/dist/access-http-briefing.js +3 -3
  7. package/dist/access-http-query.js +2 -2
  8. package/dist/access-http.js +14 -14
  9. package/dist/access-identity-continuity-surface.js +2 -2
  10. package/dist/access-lcm-surface.js +2 -2
  11. package/dist/access-mcp.js +9 -9
  12. package/dist/access-namespace-preflight.js +2 -2
  13. package/dist/access-observe-write-surface.js +2 -2
  14. package/dist/access-operations-batch.js +3 -3
  15. package/dist/access-operations.js +8 -8
  16. package/dist/access-recall-concurrency.js +2 -2
  17. package/dist/access-recall-response.js +2 -2
  18. package/dist/access-recall-surface.js +2 -2
  19. package/dist/access-service.js +2 -2
  20. package/dist/access-surface-catalog.js +4 -4
  21. package/dist/active-recall.js +2 -2
  22. package/dist/{chunk-ZADYEZY4.js → chunk-2SIKTXWX.js} +2 -2
  23. package/dist/{chunk-TS66MUUJ.js → chunk-3R23DDD3.js} +8 -8
  24. package/dist/{chunk-EQNZC3BH.js → chunk-3S6URXEH.js} +2 -2
  25. package/dist/{chunk-BCXZ4BDS.js → chunk-75YZ5VAI.js} +2 -2
  26. package/dist/{chunk-MXTCV3B5.js → chunk-AHVJ6GLL.js} +2 -2
  27. package/dist/{chunk-6UEM5EOB.js → chunk-AQ55YKEV.js} +2 -2
  28. package/dist/{chunk-VMKAA5SU.js → chunk-BSCNBIF4.js} +2 -2
  29. package/dist/{chunk-M7HFDWOS.js → chunk-FROKVQQY.js} +2 -2
  30. package/dist/{chunk-A4TQFNRX.js → chunk-GRDRVYXJ.js} +3 -3
  31. package/dist/{chunk-EYK3UHAP.js → chunk-H6WOFMOV.js} +2 -2
  32. package/dist/{chunk-SOOWBRMH.js → chunk-IYNCJKBM.js} +8 -8
  33. package/dist/{chunk-3KJTSCSX.js → chunk-J54B5DGD.js} +5 -4
  34. package/dist/chunk-J54B5DGD.js.map +1 -0
  35. package/dist/{chunk-UVZBDIJX.js → chunk-KIGC55FQ.js} +489 -49
  36. package/dist/chunk-KIGC55FQ.js.map +1 -0
  37. package/dist/{chunk-KQQEZTH6.js → chunk-KQVBR7BC.js} +2 -2
  38. package/dist/{chunk-MTJUIIM4.js → chunk-MTWD4KMM.js} +2 -2
  39. package/dist/{chunk-JE4H2QBO.js → chunk-NNEWBRUL.js} +2 -2
  40. package/dist/{chunk-SEQ6DWXD.js → chunk-NPX6WMFJ.js} +2 -2
  41. package/dist/{chunk-Z22M4JCY.js → chunk-NZLFVLLJ.js} +165 -5
  42. package/dist/chunk-NZLFVLLJ.js.map +1 -0
  43. package/dist/{chunk-QKCXRS5A.js → chunk-OGXXGHW6.js} +11 -1
  44. package/dist/{chunk-QKCXRS5A.js.map → chunk-OGXXGHW6.js.map} +1 -1
  45. package/dist/{chunk-CO7KBRPF.js → chunk-TJNDFNCU.js} +2 -2
  46. package/dist/{chunk-K7FXR75S.js → chunk-XZRZJHBW.js} +361 -579
  47. package/dist/chunk-XZRZJHBW.js.map +1 -0
  48. package/dist/{chunk-HUQMVI3A.js → chunk-ZBYVR5W4.js} +2 -2
  49. package/dist/{chunk-ZANEMAEV.js → chunk-ZP73EM2X.js} +2 -2
  50. package/dist/{chunk-IEOBAOA6.js → chunk-ZXQPAYCS.js} +2 -2
  51. package/dist/cli.js +19 -19
  52. package/dist/compounding/engine.js +3 -3
  53. package/dist/config.js +2 -2
  54. package/dist/external-wiki-access.js +3 -3
  55. package/dist/index.d.ts +139 -41
  56. package/dist/index.js +36 -26
  57. package/dist/memory-browse-operations.js +3 -3
  58. package/dist/operator-toolkit.js +3 -3
  59. package/dist/orchestration/maintenance.js +3 -2
  60. package/dist/orchestrator.js +24 -24
  61. package/dist/recall-navigation-operations.js +3 -3
  62. package/dist/recall-navigation.js +2 -2
  63. package/dist/resume-bundles.js +3 -3
  64. package/dist/shared-context/manager.d.ts +16 -0
  65. package/dist/shared-context/manager.js +2 -2
  66. package/dist/support-passport/index.js +5 -5
  67. package/package.json +2 -2
  68. package/src/activity/config.test.ts +19 -0
  69. package/src/activity/config.ts +3 -4
  70. package/src/activity/index.ts +2 -0
  71. package/src/activity/timeline/index.ts +1 -0
  72. package/src/activity/timeline/week-dashboard.test.ts +362 -0
  73. package/src/activity/timeline/week-dashboard.ts +273 -0
  74. package/src/activity/timeline/weekly.ts +2 -1
  75. package/src/lifecycle/seed-graduation.test.ts +209 -0
  76. package/src/lifecycle/seed-graduation.ts +94 -11
  77. package/src/shared-context/governance-wiring.test.ts +153 -0
  78. package/src/shared-context/manager.ts +104 -3
  79. package/src/shared-context/staleness.ts +1 -1
  80. package/dist/chunk-3KJTSCSX.js.map +0 -1
  81. package/dist/chunk-K7FXR75S.js.map +0 -1
  82. package/dist/chunk-UVZBDIJX.js.map +0 -1
  83. package/dist/chunk-Z22M4JCY.js.map +0 -1
  84. /package/dist/{chunk-ZADYEZY4.js.map → chunk-2SIKTXWX.js.map} +0 -0
  85. /package/dist/{chunk-TS66MUUJ.js.map → chunk-3R23DDD3.js.map} +0 -0
  86. /package/dist/{chunk-EQNZC3BH.js.map → chunk-3S6URXEH.js.map} +0 -0
  87. /package/dist/{chunk-BCXZ4BDS.js.map → chunk-75YZ5VAI.js.map} +0 -0
  88. /package/dist/{chunk-MXTCV3B5.js.map → chunk-AHVJ6GLL.js.map} +0 -0
  89. /package/dist/{chunk-6UEM5EOB.js.map → chunk-AQ55YKEV.js.map} +0 -0
  90. /package/dist/{chunk-VMKAA5SU.js.map → chunk-BSCNBIF4.js.map} +0 -0
  91. /package/dist/{chunk-M7HFDWOS.js.map → chunk-FROKVQQY.js.map} +0 -0
  92. /package/dist/{chunk-A4TQFNRX.js.map → chunk-GRDRVYXJ.js.map} +0 -0
  93. /package/dist/{chunk-EYK3UHAP.js.map → chunk-H6WOFMOV.js.map} +0 -0
  94. /package/dist/{chunk-SOOWBRMH.js.map → chunk-IYNCJKBM.js.map} +0 -0
  95. /package/dist/{chunk-KQQEZTH6.js.map → chunk-KQVBR7BC.js.map} +0 -0
  96. /package/dist/{chunk-MTJUIIM4.js.map → chunk-MTWD4KMM.js.map} +0 -0
  97. /package/dist/{chunk-JE4H2QBO.js.map → chunk-NNEWBRUL.js.map} +0 -0
  98. /package/dist/{chunk-SEQ6DWXD.js.map → chunk-NPX6WMFJ.js.map} +0 -0
  99. /package/dist/{chunk-CO7KBRPF.js.map → chunk-TJNDFNCU.js.map} +0 -0
  100. /package/dist/{chunk-HUQMVI3A.js.map → chunk-ZBYVR5W4.js.map} +0 -0
  101. /package/dist/{chunk-ZANEMAEV.js.map → chunk-ZP73EM2X.js.map} +0 -0
  102. /package/dist/{chunk-IEOBAOA6.js.map → chunk-ZXQPAYCS.js.map} +0 -0
@@ -28,6 +28,10 @@ const SEED_TEXT = "The launch moved to September twelfth after the vendor call."
28
28
  // Restates the seed in similar wording (the wearable support-corpus pair).
29
29
  const RESTATE_TEXT = "Launch moved to September twelfth after the vendor call.";
30
30
  const UNRELATED_TEXT = "Quarterly budget planning covered the new office lease.";
31
+ // Restates the seed's core content with flipped negation polarity.
32
+ const CONTRADICT_TEXT = "The launch was not moved to September twelfth after the vendor call.";
33
+ const NEGATED_UNRELATED_TEXT =
34
+ "The office lease was not approved during quarterly budget planning.";
31
35
 
32
36
  function fakeMemory(overrides: {
33
37
  id: string;
@@ -329,6 +333,144 @@ test("disabled config holds with the disabled reason (zero-value semantics)", ()
329
333
  assert.deepEqual(decision.reasons, ["seed-graduation-disabled"]);
330
334
  });
331
335
 
336
+ // ---------------------------------------------------------------------------
337
+ // Gate — contradiction during the corroboration window (issue #2974)
338
+ // ---------------------------------------------------------------------------
339
+
340
+ function seedMemory(overrides: Partial<Parameters<typeof fakeMemory>[0]> & { id: string }): MemoryFile {
341
+ return fakeMemory({
342
+ content: SEED_TEXT,
343
+ created: "2026-08-01T10:00:00.000Z",
344
+ status: "pending_review",
345
+ source: "wearable:bee",
346
+ ...overrides,
347
+ });
348
+ }
349
+
350
+ test("an independent negated restatement holds the seed even with enough corroborations", () => {
351
+ const seed = seedMemory({ id: "seed-1" });
352
+ const corroborator = fakeMemory({
353
+ id: "ev-1",
354
+ content: RESTATE_TEXT,
355
+ created: "2026-08-02T10:00:00.000Z",
356
+ source: "wearable:limitless",
357
+ });
358
+ const contradictor = fakeMemory({
359
+ id: "ev-2",
360
+ content: CONTRADICT_TEXT,
361
+ created: "2026-08-03T10:00:00.000Z",
362
+ source: "extraction",
363
+ });
364
+ const decision = evaluateSeedGraduation(seed, [corroborator, contradictor], {
365
+ config: ENABLED,
366
+ });
367
+ assert.equal(decision.decision, "hold", "contradiction vetoes graduation");
368
+ assert.deepEqual(decision.contradicting, [{ memoryId: "ev-2", source: "extraction" }]);
369
+ assert.ok(decision.reasons.includes("contradiction-observed:1"));
370
+ });
371
+
372
+ test("a negated seed restated positively is held (polarity symmetry)", () => {
373
+ const seed = seedMemory({ id: "seed-1", content: CONTRADICT_TEXT });
374
+ const positive = fakeMemory({
375
+ id: "ev-1",
376
+ content: RESTATE_TEXT,
377
+ created: "2026-08-02T10:00:00.000Z",
378
+ source: "wearable:limitless",
379
+ });
380
+ const decision = evaluateSeedGraduation(seed, [positive], { config: ENABLED });
381
+ assert.equal(decision.decision, "hold");
382
+ assert.equal(decision.contradicting.length, 1);
383
+ });
384
+
385
+ test("a same-session negation is a draft correction, not a contradiction", () => {
386
+ const seed = seedMemory({
387
+ id: "seed-1",
388
+ structuredAttributes: { sessionKey: "session-a" },
389
+ });
390
+ const retraction = fakeMemory({
391
+ id: "ev-2",
392
+ content: CONTRADICT_TEXT,
393
+ created: "2026-08-03T10:00:00.000Z",
394
+ source: "extraction",
395
+ structuredAttributes: { sessionKey: "session-a" },
396
+ });
397
+ const corroborator = fakeMemory({
398
+ id: "ev-1",
399
+ content: RESTATE_TEXT,
400
+ created: "2026-08-02T10:00:00.000Z",
401
+ source: "wearable:limitless",
402
+ });
403
+ const decision = evaluateSeedGraduation(seed, [retraction, corroborator], {
404
+ config: ENABLED,
405
+ });
406
+ assert.equal(decision.decision, "promote");
407
+ assert.equal(decision.contradicting.length, 0);
408
+ });
409
+
410
+ test("a polarity flip without core-content restatement neither contradicts nor corroborates", () => {
411
+ const seed = seedMemory({ id: "seed-1" });
412
+ const negatedUnrelated = fakeMemory({
413
+ id: "ev-2",
414
+ content: NEGATED_UNRELATED_TEXT,
415
+ created: "2026-08-03T10:00:00.000Z",
416
+ source: "extraction",
417
+ });
418
+ const corroborator = fakeMemory({
419
+ id: "ev-1",
420
+ content: RESTATE_TEXT,
421
+ created: "2026-08-02T10:00:00.000Z",
422
+ source: "wearable:limitless",
423
+ });
424
+ const decision = evaluateSeedGraduation(seed, [negatedUnrelated, corroborator], {
425
+ config: ENABLED,
426
+ });
427
+ assert.equal(decision.decision, "promote");
428
+ assert.equal(decision.contradicting.length, 0);
429
+ });
430
+
431
+ test("recall echo does not suppress a contradiction: denying a recalled seed is a correction", () => {
432
+ const seed = seedMemory({
433
+ id: "seed-1",
434
+ structuredAttributes: { sessionKey: "session-a" },
435
+ });
436
+ const contradictor = fakeMemory({
437
+ id: "ev-2",
438
+ content: CONTRADICT_TEXT,
439
+ created: "2026-08-03T10:00:00.000Z",
440
+ source: "extraction",
441
+ structuredAttributes: { sessionKey: "session-b" },
442
+ });
443
+ const decision = evaluateSeedGraduation(seed, [contradictor], {
444
+ config: ENABLED,
445
+ recalledBySession: (sessionKey) =>
446
+ sessionKey === "session-b" ? [["seed-1"]] : [],
447
+ });
448
+ assert.equal(decision.decision, "hold");
449
+ assert.equal(decision.contradicting.length, 1);
450
+ });
451
+
452
+ test("a superseded contradictor no longer holds the seed (review resolution unblocks)", () => {
453
+ const seed = seedMemory({ id: "seed-1" });
454
+ const resolved = fakeMemory({
455
+ id: "ev-2",
456
+ content: CONTRADICT_TEXT,
457
+ created: "2026-08-03T10:00:00.000Z",
458
+ source: "extraction",
459
+ status: "superseded",
460
+ });
461
+ const corroborator = fakeMemory({
462
+ id: "ev-1",
463
+ content: RESTATE_TEXT,
464
+ created: "2026-08-02T10:00:00.000Z",
465
+ source: "wearable:limitless",
466
+ });
467
+ const decision = evaluateSeedGraduation(seed, [resolved, corroborator], {
468
+ config: ENABLED,
469
+ });
470
+ assert.equal(decision.decision, "promote");
471
+ assert.equal(decision.contradicting.length, 0);
472
+ });
473
+
332
474
  // ---------------------------------------------------------------------------
333
475
  // Pass — against a real StorageManager (acceptance cases)
334
476
  // ---------------------------------------------------------------------------
@@ -401,6 +543,40 @@ test("pass: independent corroboration promotes the seed in place with audit attr
401
543
  }
402
544
  });
403
545
 
546
+ test("pass: contradiction during the window holds the seed in place", async () => {
547
+ const { storage, dir } = makeStorage();
548
+ try {
549
+ await storage.writeMemory("fact", SEED_TEXT, {
550
+ source: "wearable:bee",
551
+ status: "pending_review",
552
+ });
553
+ await storage.writeMemory("fact", RESTATE_TEXT, {
554
+ source: "wearable:limitless",
555
+ });
556
+ await storage.writeMemory("fact", CONTRADICT_TEXT, {
557
+ source: "extraction",
558
+ });
559
+ const memories = await storage.readAllMemories();
560
+ const summary = await runSeedGraduationPass({
561
+ memories,
562
+ storage,
563
+ config: ENABLED,
564
+ });
565
+ assert.equal(summary.evaluated, 1);
566
+ assert.equal(summary.promoted, 0, "contradicted seed must not graduate");
567
+ assert.equal(summary.held, 1);
568
+ assert.equal(summary.contradictionHeld, 1);
569
+ const seed = (await storage.readAllMemories()).find(
570
+ (memory) => memory.frontmatter.source === "wearable:bee",
571
+ );
572
+ assert.ok(seed);
573
+ assert.equal(seed.frontmatter.status, "pending_review", "held for review resolution");
574
+ assert.equal(seed.frontmatter.structuredAttributes?.graduatedBy, undefined);
575
+ } finally {
576
+ rmSync(dir, { recursive: true, force: true });
577
+ }
578
+ });
579
+
404
580
  test("pass: a non-corroborated seed stays in its prior state", async () => {
405
581
  const { storage, dir } = makeStorage();
406
582
  try {
@@ -450,6 +626,7 @@ test("pass: disabled config touches nothing", async () => {
450
626
  evaluated: 0,
451
627
  promoted: 0,
452
628
  held: 0,
629
+ contradictionHeld: 0,
453
630
  echoSuppressed: 0,
454
631
  disabled: true,
455
632
  });
@@ -581,3 +758,35 @@ test("lifecycle pass: recall-handle history suppresses echo corroboration", asyn
581
758
  rmSync(dir, { recursive: true, force: true });
582
759
  }
583
760
  });
761
+
762
+ test("lifecycle pass: contradiction during the window holds the seed through the policy seam", async () => {
763
+ const { storage, dir } = makeStorage();
764
+ try {
765
+ await storage.writeMemory("fact", SEED_TEXT, {
766
+ source: "wearable:bee",
767
+ status: "pending_review",
768
+ });
769
+ await storage.writeMemory("fact", RESTATE_TEXT, {
770
+ source: "wearable:limitless",
771
+ });
772
+ await storage.writeMemory("fact", CONTRADICT_TEXT, {
773
+ source: "extraction",
774
+ });
775
+ const config = parseConfig({
776
+ memoryDir: dir,
777
+ seedGraduation: { enabled: true, minCorroborations: 1 },
778
+ });
779
+ const memories = await storage.readAllMemories();
780
+ await new LifecyclePolicyCoordinator(coordinatorDeps(storage, config)).runLifecyclePolicyPass(
781
+ memories,
782
+ storage,
783
+ );
784
+ const seed = (await storage.readAllMemories()).find(
785
+ (memory) => memory.frontmatter.source === "wearable:bee",
786
+ );
787
+ assert.equal(seed?.frontmatter.status, "pending_review", "contradiction holds the seed");
788
+ assert.equal(seed?.frontmatter.structuredAttributes?.graduatedBy, undefined);
789
+ } finally {
790
+ rmSync(dir, { recursive: true, force: true });
791
+ }
792
+ });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Corroboration-graduated seed memories (issue #2974) — foundation layer.
2
+ * Corroboration-graduated seed memories (issue #2974).
3
3
  *
4
4
  * A `pending_review` memory is a SEED: it must not enter active recall on its
5
5
  * own say-so. This module decides, deterministically, when later evidence has
@@ -12,20 +12,21 @@
12
12
  * anchors are absent, and never a lineage descendant of the seed,
13
13
  * - echo is suppressed: when the caller can show the evidence's session
14
14
  * RECALLED the seed (the store's own content quoted back), that evidence
15
- * never counts — otherwise recall would manufacture its own confirmation.
15
+ * never counts — otherwise recall would manufacture its own confirmation,
16
+ * - a CONTRADICTION observed during the corroboration window — a later
17
+ * independent memory restating the seed's core content with flipped
18
+ * negation polarity (the dedup detector's vocabulary) — holds the seed:
19
+ * it never auto-graduates while the contradiction stands. Demoting or
20
+ * superseding stays a reviewer verb, exactly like preference drift.
16
21
  *
17
22
  * Promotion itself reuses the existing in-place `pending_review -> active`
18
23
  * machinery (`StorageManager.promoteWearableMemory`, the same status flip the
19
24
  * wearable cross-device corroboration path uses) — no parallel write path.
20
25
  * Every graduation stamps the corroborating evidence ids on the promoted row
21
26
  * (audit surface; the lifecycle ledger already records the status transition).
22
- *
23
- * Foundation scope: the gate, the pass, and the config parser. Wiring the key
24
- * into `parseConfig` (config.ts sits at its size ratchet), the conservative
25
- * preset pin, scheduling inside the lifecycle sweep, contradiction holds, and
26
- * the docs/manifest surface are follow-up layers on this issue.
27
27
  */
28
28
 
29
+ import { NEGATION_WORDS } from "../dedup/index.js";
29
30
  import { normalizeRecallTokens } from "../recall-tokenization.js";
30
31
  import { stripAttributesSuffix } from "../storage.js";
31
32
  import { STRUCTURED_ATTRIBUTE_LIMITS } from "../write-envelope.js";
@@ -161,8 +162,17 @@ function isIndependentProvenance(seed: MemoryFile, evidence: MemoryFile): boolea
161
162
 
162
163
  /** Reads the token coverage of `seed`'s tokens inside `evidence`'s body. */
163
164
  function coverageOf(seedTokens: ReadonlySet<string>, evidence: MemoryFile): number {
165
+ return tokenCoverageOf(
166
+ seedTokens,
167
+ new Set(normalizeRecallTokens(stripAttributesSuffix(evidence.content))),
168
+ );
169
+ }
170
+
171
+ function tokenCoverageOf(
172
+ seedTokens: ReadonlySet<string>,
173
+ evidenceTokens: ReadonlySet<string>,
174
+ ): number {
164
175
  if (seedTokens.size === 0) return 0;
165
- const evidenceTokens = new Set(normalizeRecallTokens(stripAttributesSuffix(evidence.content)));
166
176
  let matches = 0;
167
177
  for (const token of seedTokens) {
168
178
  if (evidenceTokens.has(token)) matches += 1;
@@ -170,6 +180,28 @@ function coverageOf(seedTokens: ReadonlySet<string>, evidence: MemoryFile): numb
170
180
  return matches / seedTokens.size;
171
181
  }
172
182
 
183
+ // Contradiction detection shares the dedup detector's negation vocabulary
184
+ // (NEGATION_WORDS) — one vocabulary source, never a per-consumer copy.
185
+
186
+ /** Word shapes that keep contractions ("don't") as single negation words. */
187
+ const NEGATION_WORD_SPLIT = /[^\p{L}\p{M}\p{N}']+/u;
188
+
189
+ function hasNegationWord(text: string): boolean {
190
+ for (const word of text.toLowerCase().split(NEGATION_WORD_SPLIT)) {
191
+ if (NEGATION_WORDS.has(word)) return true;
192
+ }
193
+ return false;
194
+ }
195
+
196
+ /** Drops negation words so polarity-flipped texts compare on core content. */
197
+ function stripNegationWords(text: string): string {
198
+ return text
199
+ .toLowerCase()
200
+ .split(NEGATION_WORD_SPLIT)
201
+ .filter((word) => !NEGATION_WORDS.has(word))
202
+ .join(" ");
203
+ }
204
+
173
205
  /**
174
206
  * Per-session recall history seam, satisfied by
175
207
  * `RecallHandleHistoryStore.recent(sessionKey)`: the memory-id sets a session
@@ -187,6 +219,12 @@ export interface SeedGraduationEvaluation {
187
219
  decision: "promote" | "hold";
188
220
  corroborationCount: number;
189
221
  corroborating: SeedGraduationEvidenceRef[];
222
+ /**
223
+ * Later independent evidence that restated the seed's core content with
224
+ * flipped negation polarity. Non-empty vetoes promotion: the seed is held
225
+ * for review resolution instead (issue #2974).
226
+ */
227
+ contradicting: SeedGraduationEvidenceRef[];
190
228
  /** Evidence that restated the seed but was excluded as echo/derivation. */
191
229
  echoSuppressedCount: number;
192
230
  reasons: string[];
@@ -208,23 +246,29 @@ export function evaluateSeedGraduation(
208
246
  decision: "hold",
209
247
  corroborationCount: 0,
210
248
  corroborating: [],
249
+ contradicting: [],
211
250
  echoSuppressedCount: 0,
212
251
  reasons: ["seed-graduation-disabled"],
213
252
  };
214
253
  }
215
- const seedTokens = new Set(normalizeRecallTokens(stripAttributesSuffix(seed.content)));
254
+ const seedText = stripAttributesSuffix(seed.content);
255
+ const seedTokens = new Set(normalizeRecallTokens(seedText));
216
256
  if (seedTokens.size < MIN_SEED_TOKENS) {
217
257
  return {
218
258
  decision: "hold",
219
259
  corroborationCount: 0,
220
260
  corroborating: [],
261
+ contradicting: [],
221
262
  echoSuppressedCount: 0,
222
263
  reasons: ["seed-too-short-for-deterministic-corroboration"],
223
264
  };
224
265
  }
266
+ const seedNegated = hasNegationWord(seedText);
267
+ const seedCoreTokens = new Set(normalizeRecallTokens(stripNegationWords(seedText)));
225
268
 
226
269
  const seedCreated = createdMs(seed);
227
270
  const corroborating: SeedGraduationEvidenceRef[] = [];
271
+ const contradicting: SeedGraduationEvidenceRef[] = [];
228
272
  const reasons: string[] = [];
229
273
  let echoSuppressedCount = 0;
230
274
 
@@ -236,6 +280,24 @@ export function evaluateSeedGraduation(
236
280
  if (!Number.isFinite(seedCreated) || !Number.isFinite(candidateCreated)) continue;
237
281
  if (candidateCreated <= seedCreated) continue;
238
282
  if (!isEvidenceStatus(candidate.frontmatter.status)) continue;
283
+ const candidateText = stripAttributesSuffix(candidate.content);
284
+ if (hasNegationWord(candidateText) !== seedNegated) {
285
+ // Polarity-flipped restatement of the seed's core content is a
286
+ // contradiction, never a corroboration. The same independence rules
287
+ // apply, but recall echo does NOT suppress a contradiction: an
288
+ // independent consumer denying a recalled seed is a correction, not
289
+ // manufactured confirmation.
290
+ if (seedCoreTokens.size >= MIN_SEED_TOKENS && isIndependentProvenance(seed, candidate)) {
291
+ const coreTokens = new Set(normalizeRecallTokens(stripNegationWords(candidateText)));
292
+ if (tokenCoverageOf(seedCoreTokens, coreTokens) >= SEED_CORROBORATION_COVERAGE) {
293
+ contradicting.push({
294
+ memoryId: candidate.frontmatter.id,
295
+ source: candidate.frontmatter.source,
296
+ });
297
+ }
298
+ }
299
+ continue;
300
+ }
239
301
  if (coverageOf(seedTokens, candidate) < SEED_CORROBORATION_COVERAGE) continue;
240
302
 
241
303
  if (!isIndependentProvenance(seed, candidate)) {
@@ -258,11 +320,27 @@ export function evaluateSeedGraduation(
258
320
  }
259
321
 
260
322
  if (echoSuppressedCount > 0) reasons.push(`echo-suppressed:${echoSuppressedCount}`);
323
+ if (contradicting.length > 0) {
324
+ // A contradiction observed during the corroboration window vetoes
325
+ // graduation no matter how many corroborations also landed: promoting a
326
+ // disputed fact is never automatic. Demotion/supersession stays with the
327
+ // reviewer via the existing contradiction-resolution verbs.
328
+ reasons.push(`contradiction-observed:${contradicting.length}`);
329
+ return {
330
+ decision: "hold",
331
+ corroborationCount: corroborating.length,
332
+ corroborating,
333
+ contradicting,
334
+ echoSuppressedCount,
335
+ reasons,
336
+ };
337
+ }
261
338
  if (corroborating.length >= config.minCorroborations) {
262
339
  return {
263
340
  decision: "promote",
264
341
  corroborationCount: corroborating.length,
265
342
  corroborating,
343
+ contradicting,
266
344
  echoSuppressedCount,
267
345
  reasons,
268
346
  };
@@ -272,6 +350,7 @@ export function evaluateSeedGraduation(
272
350
  decision: "hold",
273
351
  corroborationCount: corroborating.length,
274
352
  corroborating,
353
+ contradicting,
275
354
  echoSuppressedCount,
276
355
  reasons,
277
356
  };
@@ -306,6 +385,8 @@ export interface SeedGraduationPassSummary {
306
385
  evaluated: number;
307
386
  promoted: number;
308
387
  held: number;
388
+ /** Held specifically because a contradiction was observed in the window. */
389
+ contradictionHeld: number;
309
390
  echoSuppressed: number;
310
391
  disabled: boolean;
311
392
  }
@@ -333,13 +414,14 @@ export async function runSeedGraduationPass(
333
414
  ): Promise<SeedGraduationPassSummary> {
334
415
  const config = input.config ?? SEED_GRADUATION_DEFAULTS;
335
416
  if (!config.enabled) {
336
- return { evaluated: 0, promoted: 0, held: 0, echoSuppressed: 0, disabled: true };
417
+ return { evaluated: 0, promoted: 0, held: 0, contradictionHeld: 0, echoSuppressed: 0, disabled: true };
337
418
  }
338
419
  const corpus = excludeSupportPassportPrivateMemories(input.memories);
339
420
  const evidencePool = corpus.filter((memory) => isEvidenceStatus(memory.frontmatter.status));
340
421
 
341
422
  let promoted = 0;
342
423
  let held = 0;
424
+ let contradictionHeld = 0;
343
425
  let echoSuppressed = 0;
344
426
  let evaluated = 0;
345
427
 
@@ -358,6 +440,7 @@ export async function runSeedGraduationPass(
358
440
 
359
441
  if (evaluation.decision === "hold") {
360
442
  held += 1;
443
+ if (evaluation.contradicting.length > 0) contradictionHeld += 1;
361
444
  continue;
362
445
  }
363
446
  // A concurrent review resolution wins: promoteWearableMemory returns
@@ -370,5 +453,5 @@ export async function runSeedGraduationPass(
370
453
  else held += 1;
371
454
  }
372
455
 
373
- return { evaluated, promoted, held, echoSuppressed, disabled: false };
456
+ return { evaluated, promoted, held, contradictionHeld, echoSuppressed, disabled: false };
374
457
  }
@@ -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
+ });