@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/{chunk-K442KOID.js → chunk-HZFHQ4AQ.js} +1 -1
- package/dist/chunk-HZFHQ4AQ.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/reconcile/cursor.d.ts +11 -1
- package/dist/reconcile/cursor.js +54 -0
- package/dist/reconcile/cursor.js.map +1 -1
- package/dist/reconcile/manifest.d.ts +2 -2
- package/dist/reconcile/manifest.js +41 -4
- package/dist/reconcile/manifest.js.map +1 -1
- package/dist/reconcile/plan.d.ts +18 -1
- package/dist/reconcile/plan.js +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/transfer/types.d.ts +12 -12
- package/package.json +2 -2
- package/src/converge-conflict-policy-type.test.ts +15 -0
- package/src/reconcile/cursor.test.ts +30 -0
- package/src/reconcile/cursor.ts +63 -0
- package/src/reconcile/manifest.test.ts +93 -2
- package/src/reconcile/manifest.ts +56 -3
- package/src/reconcile/plan.ts +20 -0
- package/dist/chunk-K442KOID.js.map +0 -1
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;
|
package/dist/transfer/types.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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 = {
|
package/src/reconcile/cursor.ts
CHANGED
|
@@ -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
|
-
|
|
136
|
-
|
|
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) };
|