@remnic/core 9.45.1 → 9.45.3

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/schemas.d.ts CHANGED
@@ -290,12 +290,12 @@ declare const EntityMentionSchema: z.ZodObject<{
290
290
  title: z.ZodString;
291
291
  facts: z.ZodArray<z.ZodString, "many">;
292
292
  }, "strip", z.ZodTypeAny, {
293
- title: string;
294
293
  key: string;
294
+ title: string;
295
295
  facts: string[];
296
296
  }, {
297
- title: string;
298
297
  key: string;
298
+ title: string;
299
299
  facts: string[];
300
300
  }>, "many">>>;
301
301
  }, "strip", z.ZodTypeAny, {
@@ -303,8 +303,8 @@ declare const EntityMentionSchema: z.ZodObject<{
303
303
  name: string;
304
304
  facts: string[];
305
305
  structuredSections?: {
306
- title: string;
307
306
  key: string;
307
+ title: string;
308
308
  facts: string[];
309
309
  }[] | null | undefined;
310
310
  promptedByQuestion?: string | null | undefined;
@@ -313,8 +313,8 @@ declare const EntityMentionSchema: z.ZodObject<{
313
313
  name: string;
314
314
  facts: string[];
315
315
  structuredSections?: {
316
- title: string;
317
316
  key: string;
317
+ title: string;
318
318
  facts: string[];
319
319
  }[] | null | undefined;
320
320
  promptedByQuestion?: string | null | undefined;
@@ -614,12 +614,12 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
614
614
  title: z.ZodString;
615
615
  facts: z.ZodArray<z.ZodString, "many">;
616
616
  }, "strip", z.ZodTypeAny, {
617
- title: string;
618
617
  key: string;
618
+ title: string;
619
619
  facts: string[];
620
620
  }, {
621
- title: string;
622
621
  key: string;
622
+ title: string;
623
623
  facts: string[];
624
624
  }>, "many">>>;
625
625
  }, "strip", z.ZodTypeAny, {
@@ -627,8 +627,8 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
627
627
  name: string;
628
628
  facts: string[];
629
629
  structuredSections?: {
630
- title: string;
631
630
  key: string;
631
+ title: string;
632
632
  facts: string[];
633
633
  }[] | null | undefined;
634
634
  promptedByQuestion?: string | null | undefined;
@@ -637,8 +637,8 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
637
637
  name: string;
638
638
  facts: string[];
639
639
  structuredSections?: {
640
- title: string;
641
640
  key: string;
641
+ title: string;
642
642
  facts: string[];
643
643
  }[] | null | undefined;
644
644
  promptedByQuestion?: string | null | undefined;
@@ -698,8 +698,8 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
698
698
  name: string;
699
699
  facts: string[];
700
700
  structuredSections?: {
701
- title: string;
702
701
  key: string;
702
+ title: string;
703
703
  facts: string[];
704
704
  }[] | null | undefined;
705
705
  promptedByQuestion?: string | null | undefined;
@@ -750,8 +750,8 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
750
750
  name: string;
751
751
  facts: string[];
752
752
  structuredSections?: {
753
- title: string;
754
753
  key: string;
754
+ title: string;
755
755
  facts: string[];
756
756
  }[] | null | undefined;
757
757
  promptedByQuestion?: string | null | undefined;
@@ -1003,12 +1003,12 @@ declare const ExtractionResultSchema: z.ZodObject<{
1003
1003
  title: z.ZodString;
1004
1004
  facts: z.ZodArray<z.ZodString, "many">;
1005
1005
  }, "strip", z.ZodTypeAny, {
1006
- title: string;
1007
1006
  key: string;
1007
+ title: string;
1008
1008
  facts: string[];
1009
1009
  }, {
1010
- title: string;
1011
1010
  key: string;
1011
+ title: string;
1012
1012
  facts: string[];
1013
1013
  }>, "many">>>;
1014
1014
  }, "strip", z.ZodTypeAny, {
@@ -1016,8 +1016,8 @@ declare const ExtractionResultSchema: z.ZodObject<{
1016
1016
  name: string;
1017
1017
  facts: string[];
1018
1018
  structuredSections?: {
1019
- title: string;
1020
1019
  key: string;
1020
+ title: string;
1021
1021
  facts: string[];
1022
1022
  }[] | null | undefined;
1023
1023
  promptedByQuestion?: string | null | undefined;
@@ -1026,8 +1026,8 @@ declare const ExtractionResultSchema: z.ZodObject<{
1026
1026
  name: string;
1027
1027
  facts: string[];
1028
1028
  structuredSections?: {
1029
- title: string;
1030
1029
  key: string;
1030
+ title: string;
1031
1031
  facts: string[];
1032
1032
  }[] | null | undefined;
1033
1033
  promptedByQuestion?: string | null | undefined;
@@ -1106,8 +1106,8 @@ declare const ExtractionResultSchema: z.ZodObject<{
1106
1106
  name: string;
1107
1107
  facts: string[];
1108
1108
  structuredSections?: {
1109
- title: string;
1110
1109
  key: string;
1110
+ title: string;
1111
1111
  facts: string[];
1112
1112
  }[] | null | undefined;
1113
1113
  promptedByQuestion?: string | null | undefined;
@@ -1164,8 +1164,8 @@ declare const ExtractionResultSchema: z.ZodObject<{
1164
1164
  name: string;
1165
1165
  facts: string[];
1166
1166
  structuredSections?: {
1167
- title: string;
1168
1167
  key: string;
1168
+ title: string;
1169
1169
  facts: string[];
1170
1170
  }[] | null | undefined;
1171
1171
  promptedByQuestion?: string | null | undefined;
@@ -1229,12 +1229,12 @@ declare const ConsolidationResultSchema: z.ZodObject<{
1229
1229
  title: z.ZodString;
1230
1230
  facts: z.ZodArray<z.ZodString, "many">;
1231
1231
  }, "strip", z.ZodTypeAny, {
1232
- title: string;
1233
1232
  key: string;
1233
+ title: string;
1234
1234
  facts: string[];
1235
1235
  }, {
1236
- title: string;
1237
1236
  key: string;
1237
+ title: string;
1238
1238
  facts: string[];
1239
1239
  }>, "many">>>;
1240
1240
  }, "strip", z.ZodTypeAny, {
@@ -1242,8 +1242,8 @@ declare const ConsolidationResultSchema: z.ZodObject<{
1242
1242
  name: string;
1243
1243
  facts: string[];
1244
1244
  structuredSections?: {
1245
- title: string;
1246
1245
  key: string;
1246
+ title: string;
1247
1247
  facts: string[];
1248
1248
  }[] | null | undefined;
1249
1249
  promptedByQuestion?: string | null | undefined;
@@ -1252,8 +1252,8 @@ declare const ConsolidationResultSchema: z.ZodObject<{
1252
1252
  name: string;
1253
1253
  facts: string[];
1254
1254
  structuredSections?: {
1255
- title: string;
1256
1255
  key: string;
1256
+ title: string;
1257
1257
  facts: string[];
1258
1258
  }[] | null | undefined;
1259
1259
  promptedByQuestion?: string | null | undefined;
@@ -1272,8 +1272,8 @@ declare const ConsolidationResultSchema: z.ZodObject<{
1272
1272
  name: string;
1273
1273
  facts: string[];
1274
1274
  structuredSections?: {
1275
- title: string;
1276
1275
  key: string;
1276
+ title: string;
1277
1277
  facts: string[];
1278
1278
  }[] | null | undefined;
1279
1279
  promptedByQuestion?: string | null | undefined;
@@ -1292,8 +1292,8 @@ declare const ConsolidationResultSchema: z.ZodObject<{
1292
1292
  name: string;
1293
1293
  facts: string[];
1294
1294
  structuredSections?: {
1295
- title: string;
1296
1295
  key: string;
1296
+ title: string;
1297
1297
  facts: string[];
1298
1298
  }[] | null | undefined;
1299
1299
  promptedByQuestion?: string | null | undefined;
@@ -313,13 +313,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
313
313
  peerProfiles: boolean;
314
314
  }>;
315
315
  }, "strip", z.ZodTypeAny, {
316
- schemaVersion: string;
317
316
  includes: {
318
317
  procedural: boolean;
319
318
  taxonomy: boolean;
320
319
  identityAnchors: boolean;
321
320
  peerProfiles: boolean;
322
321
  };
322
+ schemaVersion: string;
323
323
  id: string;
324
324
  version: string;
325
325
  description: string;
@@ -334,13 +334,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
334
334
  directAnswerEnabled: boolean;
335
335
  };
336
336
  }, {
337
- schemaVersion: string;
338
337
  includes: {
339
338
  procedural: boolean;
340
339
  taxonomy: boolean;
341
340
  identityAnchors: boolean;
342
341
  peerProfiles: boolean;
343
342
  };
343
+ schemaVersion: string;
344
344
  id: string;
345
345
  version: string;
346
346
  description: string;
@@ -464,13 +464,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
464
464
  peerProfiles: boolean;
465
465
  }>;
466
466
  }, "strip", z.ZodTypeAny, {
467
- schemaVersion: string;
468
467
  includes: {
469
468
  procedural: boolean;
470
469
  taxonomy: boolean;
471
470
  identityAnchors: boolean;
472
471
  peerProfiles: boolean;
473
472
  };
473
+ schemaVersion: string;
474
474
  id: string;
475
475
  version: string;
476
476
  description: string;
@@ -485,13 +485,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
485
485
  directAnswerEnabled: boolean;
486
486
  };
487
487
  }, {
488
- schemaVersion: string;
489
488
  includes: {
490
489
  procedural: boolean;
491
490
  taxonomy: boolean;
492
491
  identityAnchors: boolean;
493
492
  peerProfiles: boolean;
494
493
  };
494
+ schemaVersion: string;
495
495
  id: string;
496
496
  version: string;
497
497
  description: string;
@@ -518,13 +518,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
518
518
  pluginVersion: string;
519
519
  includesTranscripts: boolean;
520
520
  capsule: {
521
- schemaVersion: string;
522
521
  includes: {
523
522
  procedural: boolean;
524
523
  taxonomy: boolean;
525
524
  identityAnchors: boolean;
526
525
  peerProfiles: boolean;
527
526
  };
527
+ schemaVersion: string;
528
528
  id: string;
529
529
  version: string;
530
530
  description: string;
@@ -551,13 +551,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
551
551
  pluginVersion: string;
552
552
  includesTranscripts: boolean;
553
553
  capsule: {
554
- schemaVersion: string;
555
554
  includes: {
556
555
  procedural: boolean;
557
556
  taxonomy: boolean;
558
557
  identityAnchors: boolean;
559
558
  peerProfiles: boolean;
560
559
  };
560
+ schemaVersion: string;
561
561
  id: string;
562
562
  version: string;
563
563
  description: string;
@@ -683,13 +683,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
683
683
  peerProfiles: boolean;
684
684
  }>;
685
685
  }, "strip", z.ZodTypeAny, {
686
- schemaVersion: string;
687
686
  includes: {
688
687
  procedural: boolean;
689
688
  taxonomy: boolean;
690
689
  identityAnchors: boolean;
691
690
  peerProfiles: boolean;
692
691
  };
692
+ schemaVersion: string;
693
693
  id: string;
694
694
  version: string;
695
695
  description: string;
@@ -704,13 +704,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
704
704
  directAnswerEnabled: boolean;
705
705
  };
706
706
  }, {
707
- schemaVersion: string;
708
707
  includes: {
709
708
  procedural: boolean;
710
709
  taxonomy: boolean;
711
710
  identityAnchors: boolean;
712
711
  peerProfiles: boolean;
713
712
  };
713
+ schemaVersion: string;
714
714
  id: string;
715
715
  version: string;
716
716
  description: string;
@@ -737,13 +737,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
737
737
  pluginVersion: string;
738
738
  includesTranscripts: boolean;
739
739
  capsule: {
740
- schemaVersion: string;
741
740
  includes: {
742
741
  procedural: boolean;
743
742
  taxonomy: boolean;
744
743
  identityAnchors: boolean;
745
744
  peerProfiles: boolean;
746
745
  };
746
+ schemaVersion: string;
747
747
  id: string;
748
748
  version: string;
749
749
  description: string;
@@ -770,13 +770,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
770
770
  pluginVersion: string;
771
771
  includesTranscripts: boolean;
772
772
  capsule: {
773
- schemaVersion: string;
774
773
  includes: {
775
774
  procedural: boolean;
776
775
  taxonomy: boolean;
777
776
  identityAnchors: boolean;
778
777
  peerProfiles: boolean;
779
778
  };
779
+ schemaVersion: string;
780
780
  id: string;
781
781
  version: string;
782
782
  description: string;
@@ -815,13 +815,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
815
815
  pluginVersion: string;
816
816
  includesTranscripts: boolean;
817
817
  capsule: {
818
- schemaVersion: string;
819
818
  includes: {
820
819
  procedural: boolean;
821
820
  taxonomy: boolean;
822
821
  identityAnchors: boolean;
823
822
  peerProfiles: boolean;
824
823
  };
824
+ schemaVersion: string;
825
825
  id: string;
826
826
  version: string;
827
827
  description: string;
@@ -854,13 +854,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
854
854
  pluginVersion: string;
855
855
  includesTranscripts: boolean;
856
856
  capsule: {
857
- schemaVersion: string;
858
857
  includes: {
859
858
  procedural: boolean;
860
859
  taxonomy: boolean;
861
860
  identityAnchors: boolean;
862
861
  peerProfiles: boolean;
863
862
  };
863
+ schemaVersion: string;
864
864
  id: string;
865
865
  version: string;
866
866
  description: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.45.1",
3
+ "version": "9.45.3",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3106,7 +3106,7 @@
3106
3106
  "core"
3107
3107
  ],
3108
3108
  "peerDependencies": {
3109
- "@remnic/coding-graph": "^9.45.1"
3109
+ "@remnic/coding-graph": "^9.45.3"
3110
3110
  },
3111
3111
  "peerDependenciesMeta": {
3112
3112
  "@remnic/coding-graph": {
@@ -0,0 +1,15 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import type { ReconcileConflictPolicy } from "./reconcile/plan.js";
5
+ import type { ConvergeConflictPolicy } from "./types.js";
6
+
7
+ type Equal<Left, Right> = (<Value>() => Value extends Left ? 1 : 2) extends <Value>() => Value extends Right ? 1 : 2
8
+ ? true
9
+ : false;
10
+
11
+ const conflictPolicyNamesAreEquivalent: Equal<ReconcileConflictPolicy, ConvergeConflictPolicy> = true;
12
+
13
+ test("ReconcileConflictPolicy remains a type alias for ConvergeConflictPolicy", () => {
14
+ assert.equal(conflictPolicyNamesAreEquivalent, true);
15
+ });
@@ -5,6 +5,7 @@ import * as path from "node:path";
5
5
  import { test } from "node:test";
6
6
  import {
7
7
  defaultConvergeCursorPath,
8
+ deriveConvergeCursorBase,
8
9
  hashPeerNamespace,
9
10
  normalizeConvergeCursor,
10
11
  readConvergeCursor,
@@ -13,6 +14,7 @@ import {
13
14
  } from "./cursor.js";
14
15
 
15
16
  const shaA = "a".repeat(64);
17
+ const shaB = "b".repeat(64);
16
18
 
17
19
  test("hashPeerNamespace: deterministic key normalization", () => {
18
20
  const k1 = hashPeerNamespace("http://peer.example.com/", "default");
@@ -46,6 +48,18 @@ test("defaultConvergeCursorPath: constructs path under memoryDir state", () => {
46
48
  assert.ok(p.endsWith(".json"));
47
49
  });
48
50
 
51
+ test("deriveConvergeCursorBase retains a deferred semantic agreement", () => {
52
+ const semanticAgreement = {
53
+ local: { path: "facts/local.md", sha256: shaA },
54
+ peer: { path: "facts/peer.md", sha256: shaB },
55
+ };
56
+
57
+ assert.deepEqual(
58
+ deriveConvergeCursorBase([], "default", [semanticAgreement]).semanticAgreements,
59
+ [semanticAgreement]
60
+ );
61
+ });
62
+
49
63
  test("normalizeConvergeCursor: validates envelope shape", () => {
50
64
  assert.throws(() => normalizeConvergeCursor(null), /must be an object/);
51
65
  assert.throws(() => normalizeConvergeCursor({ version: 2 }), /version must be 1/);
@@ -63,11 +77,19 @@ test("normalizeConvergeCursor: validates envelope shape", () => {
63
77
  peerUrl: "http://peer",
64
78
  namespace: "default",
65
79
  baseFiles: [{ path: "a.md", sha256: shaA }],
80
+ semanticAgreements: [{
81
+ local: { path: "facts/local.md", sha256: shaA },
82
+ peer: { path: "facts/peer.md", sha256: shaB },
83
+ }],
66
84
  });
67
85
  assert.equal(valid.peerUrl, "http://peer");
68
86
  assert.equal(valid.namespace, "default");
69
87
  assert.equal(valid.baseFiles.length, 1);
70
88
  assert.equal(valid.baseFiles[0]?.path, "a.md");
89
+ assert.deepEqual(valid.semanticAgreements, [{
90
+ local: { path: "facts/local.md", sha256: shaA },
91
+ peer: { path: "facts/peer.md", sha256: shaB },
92
+ }]);
71
93
  });
72
94
 
73
95
  test("readConvergeCursor: returns null when file is missing or invalid", async () => {
@@ -96,6 +118,10 @@ test("writeConvergeCursor & readConvergeCursor: atomic write roundtrip", async (
96
118
  lastConvergedAt: new Date().toISOString(),
97
119
  baseFiles: [{ path: "facts/a.md", sha256: shaA, bytes: 120, mtimeMs: 1000 }],
98
120
  completedPaths: ["facts/a.md"],
121
+ semanticAgreements: [{
122
+ local: { path: "facts/local.md", sha256: shaA },
123
+ peer: { path: "facts/peer.md", sha256: shaB },
124
+ }],
99
125
  };
100
126
 
101
127
  await writeConvergeCursor(cursorPath, cursor);
@@ -106,6 +132,10 @@ test("writeConvergeCursor & readConvergeCursor: atomic write roundtrip", async (
106
132
  assert.equal(read.baseFiles.length, 1);
107
133
  assert.equal(read.baseFiles[0]?.sha256, shaA);
108
134
  assert.deepEqual(read.completedPaths, ["facts/a.md"]);
135
+ assert.deepEqual(read.semanticAgreements, [{
136
+ local: { path: "facts/local.md", sha256: shaA },
137
+ peer: { path: "facts/peer.md", sha256: shaB },
138
+ }]);
109
139
 
110
140
  // Update existing cursor atomically
111
141
  const updated: ConvergeCursorState = {
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
+ import type { ReconcilePlanEntry, ReconcileSemanticAgreement } from "./plan.js";
4
5
 
5
6
  export interface ConvergeCursorFileState {
6
7
  path: string;
@@ -15,6 +16,7 @@ export interface ConvergeCursorState {
15
16
  namespace: string;
16
17
  lastConvergedAt?: string;
17
18
  baseFiles: ConvergeCursorFileState[];
19
+ semanticAgreements?: ReconcileSemanticAgreement[];
18
20
  completedPaths?: string[];
19
21
  }
20
22
 
@@ -48,6 +50,56 @@ export function defaultConvergeCursorPath(
48
50
  return path.join(path.resolve(memoryDir), ".remnic", "state", "converge-cursors", `${key}.json`);
49
51
  }
50
52
 
53
+ function normalizeSemanticFileState(input: unknown): { path: string; sha256: string } | undefined {
54
+ if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
55
+ const file = input as Record<string, unknown>;
56
+ if (typeof file.path !== "string" || typeof file.sha256 !== "string") return undefined;
57
+ return { path: file.path, sha256: file.sha256 };
58
+ }
59
+
60
+ function digestAfterReconcile(entry: ReconcilePlanEntry): string | undefined {
61
+ if (entry.action === "push") return entry.localSha256;
62
+ if (entry.action === "pull") return entry.peerSha256;
63
+ if (entry.action === "conflict") {
64
+ if (entry.resolution === "local-wins") return entry.localSha256;
65
+ if (entry.resolution === "peer-wins") return entry.peerSha256;
66
+ return undefined;
67
+ }
68
+ if (entry.action !== "identical") return undefined;
69
+ if (entry.localSha256 && entry.peerSha256 && entry.localSha256 !== entry.peerSha256) return undefined;
70
+ return entry.localSha256 ?? entry.peerSha256;
71
+ }
72
+
73
+ function semanticAgreementKey(agreement: ReconcileSemanticAgreement): string {
74
+ return `${agreement.local.path}\0${agreement.peer.path}`;
75
+ }
76
+
77
+ export function deriveConvergeCursorBase(
78
+ entries: readonly ReconcilePlanEntry[],
79
+ namespace: string,
80
+ priorSemanticAgreements: readonly ReconcileSemanticAgreement[] = [],
81
+ ): Pick<ConvergeCursorState, "baseFiles" | "semanticAgreements"> {
82
+ const baseFiles: ConvergeCursorFileState[] = [];
83
+ const semanticAgreementsByPathPair = new Map(
84
+ priorSemanticAgreements.map((agreement) => [semanticAgreementKey(agreement), agreement])
85
+ );
86
+ for (const entry of entries) {
87
+ if (entry.namespace !== namespace) continue;
88
+ if (entry.semanticAgreement) {
89
+ semanticAgreementsByPathPair.set(semanticAgreementKey(entry.semanticAgreement), entry.semanticAgreement);
90
+ continue;
91
+ }
92
+ const sha256 = digestAfterReconcile(entry);
93
+ if (sha256) baseFiles.push({ path: entry.path, sha256 });
94
+ }
95
+ baseFiles.sort((left, right) => left.path.localeCompare(right.path));
96
+ const semanticAgreements = [...semanticAgreementsByPathPair.values()];
97
+ semanticAgreements.sort((left, right) =>
98
+ left.local.path.localeCompare(right.local.path) || left.peer.path.localeCompare(right.peer.path)
99
+ );
100
+ return { baseFiles, semanticAgreements };
101
+ }
102
+
51
103
  export function normalizeConvergeCursor(input: unknown): ConvergeCursorState {
52
104
  if (!input || typeof input !== "object" || Array.isArray(input)) {
53
105
  throw new Error("converge cursor must be an object");
@@ -78,6 +130,16 @@ export function normalizeConvergeCursor(input: unknown): ConvergeCursorState {
78
130
  }
79
131
  }
80
132
  }
133
+ const semanticAgreements: ReconcileSemanticAgreement[] = [];
134
+ if (Array.isArray(obj.semanticAgreements)) {
135
+ for (const item of obj.semanticAgreements) {
136
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
137
+ const agreement = item as Record<string, unknown>;
138
+ const local = normalizeSemanticFileState(agreement.local);
139
+ const peer = normalizeSemanticFileState(agreement.peer);
140
+ if (local && peer) semanticAgreements.push({ local, peer });
141
+ }
142
+ }
81
143
  const completedPaths: string[] = [];
82
144
  if (Array.isArray(obj.completedPaths)) {
83
145
  for (const item of obj.completedPaths) {
@@ -92,6 +154,7 @@ export function normalizeConvergeCursor(input: unknown): ConvergeCursorState {
92
154
  namespace: obj.namespace.trim(),
93
155
  lastConvergedAt: typeof obj.lastConvergedAt === "string" ? obj.lastConvergedAt : undefined,
94
156
  baseFiles,
157
+ semanticAgreements,
95
158
  completedPaths,
96
159
  };
97
160
  }
@@ -132,8 +132,11 @@ test("semantic collapse prevents different active fact paths from transferring b
132
132
  namespace: "default",
133
133
  action: "identical",
134
134
  reason: "semantic_duplicate",
135
- localSha256: localFile.sha256,
136
- peerSha256: peerFile.sha256,
135
+ semanticAgreement: {
136
+ local: localFile,
137
+ peer: peerFile,
138
+ },
139
+ semanticChange: "unchanged",
137
140
  },
138
141
  ]);
139
142
  assert.equal(collapsed.byNamespace[0]?.identical, 1);
@@ -141,6 +144,51 @@ test("semantic collapse prevents different active fact paths from transferring b
141
144
  assert.equal(collapsed.byNamespace[0]?.push, 0);
142
145
  });
143
146
 
147
+ test("semantic collapse classifies a later one-sided metadata edit from its per-side base", () => {
148
+ const semanticHash = ContentHashIndex.computeHash("same active fact");
149
+ const priorLocal = { path: "facts/local-id.md", sha256: "a".repeat(64) };
150
+ const currentLocal = { ...priorLocal, sha256: "c".repeat(64) };
151
+ const peerFile = { path: "facts/peer-id.md", sha256: "b".repeat(64) };
152
+ const manifest = (file: typeof currentLocal): ReconcileManifest => ({
153
+ format: "remnic-reconcile-manifest",
154
+ schemaVersion: 1,
155
+ files: [{
156
+ ...file,
157
+ memory: {
158
+ id: file.path,
159
+ category: "fact",
160
+ contentHash: semanticHash,
161
+ status: "active",
162
+ },
163
+ }],
164
+ });
165
+ const plan = planReconciliation([{
166
+ namespace: "default",
167
+ local: [currentLocal],
168
+ peer: [peerFile],
169
+ }]);
170
+
171
+ const collapsed = collapseActiveFactDuplicates(
172
+ plan,
173
+ new Map([["default", manifest(currentLocal)]]),
174
+ new Map([["default", manifest(peerFile)]]),
175
+ new Map([["default", [{ local: priorLocal, peer: peerFile }]]])
176
+ );
177
+
178
+ assert.deepEqual(collapsed.entries, [{
179
+ path: currentLocal.path,
180
+ namespace: "default",
181
+ action: "identical",
182
+ reason: "semantic_duplicate",
183
+ semanticAgreement: {
184
+ local: currentLocal,
185
+ peer: peerFile,
186
+ },
187
+ semanticChange: "local_changed",
188
+ }]);
189
+ assert.equal(collapsed.converged, true);
190
+ });
191
+
144
192
  test("semantic collapse does not fold non-active facts or unrelated path conflicts", () => {
145
193
  const semanticHash = ContentHashIndex.computeHash("same inactive fact");
146
194
  const localFile = { path: "facts/local.md", sha256: "a".repeat(64) };
@@ -215,6 +263,49 @@ test("semantic collapse preserves same-path metadata updates", () => {
215
263
  assert.equal(collapsed.entries[0]?.action, "push");
216
264
  });
217
265
 
266
+ test("semantic collapse preserves a shared-path metadata update when peer canonical path differs", () => {
267
+ const semanticHash = ContentHashIndex.computeHash("same raw fact");
268
+ const sharedLocal = { path: "facts/b.md", sha256: "a".repeat(64) };
269
+ const sharedPeer = { path: "facts/b.md", sha256: "b".repeat(64) };
270
+ const peerDuplicate = { path: "facts/a.md", sha256: "c".repeat(64) };
271
+ const plan = planReconciliation([{
272
+ namespace: "default",
273
+ local: [sharedLocal],
274
+ peer: [peerDuplicate, sharedPeer],
275
+ base: [{ path: sharedPeer.path, sha256: sharedPeer.sha256 }],
276
+ }]);
277
+ const manifest = (files: Array<{ path: string; sha256: string }>): ReconcileManifest => ({
278
+ format: "remnic-reconcile-manifest",
279
+ schemaVersion: 1,
280
+ files: files.map((file) => ({
281
+ ...file,
282
+ memory: {
283
+ id: file.path,
284
+ category: "fact",
285
+ contentHash: semanticHash,
286
+ status: "active",
287
+ },
288
+ })),
289
+ });
290
+
291
+ const collapsed = collapseActiveFactDuplicates(
292
+ plan,
293
+ new Map([["default", manifest([sharedLocal])]]),
294
+ new Map([["default", manifest([peerDuplicate, sharedPeer])]])
295
+ );
296
+
297
+ assert.deepEqual(collapsed.entries, [{
298
+ path: sharedLocal.path,
299
+ namespace: "default",
300
+ action: "push",
301
+ reason: "local_changed",
302
+ localSha256: sharedLocal.sha256,
303
+ peerSha256: sharedPeer.sha256,
304
+ baseSha256: sharedPeer.sha256,
305
+ }]);
306
+ assert.equal(collapsed.converged, false);
307
+ });
308
+
218
309
  test("semantic collapse remains converged after transferring the canonical one-sided duplicate", () => {
219
310
  const semanticHash = ContentHashIndex.computeHash("same one-sided fact");
220
311
  const canonical = { path: "facts/a.md", sha256: "a".repeat(64) };