@remnic/core 9.69.53 → 9.69.54
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/access-cli.js +5 -5
- package/dist/active-recall.js +1 -1
- package/dist/{chunk-SEQ6DWXD.js → chunk-5IKW2VP6.js} +2 -2
- package/dist/{chunk-UVZBDIJX.js → chunk-KGY6F4VJ.js} +488 -48
- package/dist/chunk-KGY6F4VJ.js.map +1 -0
- package/dist/{chunk-K7FXR75S.js → chunk-OKOG5MXF.js} +355 -573
- package/dist/chunk-OKOG5MXF.js.map +1 -0
- package/dist/{chunk-JE4H2QBO.js → chunk-WJ5KWJLK.js} +2 -2
- package/dist/{chunk-TS66MUUJ.js → chunk-ZHV6FGVF.js} +4 -4
- package/dist/cli.js +4 -4
- package/dist/config.js +1 -1
- package/dist/index.d.ts +139 -41
- package/dist/index.js +17 -7
- package/dist/operator-toolkit.js +2 -2
- package/dist/orchestrator.js +5 -5
- package/dist/resume-bundles.js +2 -2
- package/package.json +2 -2
- package/src/activity/index.ts +2 -0
- package/src/activity/timeline/index.ts +1 -0
- package/src/activity/timeline/week-dashboard.test.ts +362 -0
- package/src/activity/timeline/week-dashboard.ts +273 -0
- package/src/activity/timeline/weekly.ts +2 -1
- package/src/lifecycle/seed-graduation.test.ts +209 -0
- package/src/lifecycle/seed-graduation.ts +94 -11
- package/dist/chunk-K7FXR75S.js.map +0 -1
- package/dist/chunk-UVZBDIJX.js.map +0 -1
- /package/dist/{chunk-SEQ6DWXD.js.map → chunk-5IKW2VP6.js.map} +0 -0
- /package/dist/{chunk-JE4H2QBO.js.map → chunk-WJ5KWJLK.js.map} +0 -0
- /package/dist/{chunk-TS66MUUJ.js.map → chunk-ZHV6FGVF.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)
|
|
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
|
|
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
|
}
|