@bcts/provenance-mark 1.0.0-alpha.17 → 1.0.0-alpha.18

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/index.cjs CHANGED
@@ -797,6 +797,279 @@ function fromBase64(base64) {
797
797
  throw new Error("atob not available and require is not defined");
798
798
  }
799
799
 
800
+ //#endregion
801
+ //#region src/validate.ts
802
+ /**
803
+ * Format for validation report output.
804
+ */
805
+ let ValidationReportFormat = /* @__PURE__ */ function(ValidationReportFormat) {
806
+ /** Human-readable text format */
807
+ ValidationReportFormat["Text"] = "text";
808
+ /** Compact JSON format (no whitespace) */
809
+ ValidationReportFormat["JsonCompact"] = "json-compact";
810
+ /** Pretty-printed JSON format (with indentation) */
811
+ ValidationReportFormat["JsonPretty"] = "json-pretty";
812
+ return ValidationReportFormat;
813
+ }({});
814
+ /**
815
+ * Format a validation issue as a string.
816
+ */
817
+ function formatValidationIssue(issue) {
818
+ switch (issue.type) {
819
+ case "HashMismatch": return `hash mismatch: expected ${issue.expected}, got ${issue.actual}`;
820
+ case "KeyMismatch": return "key mismatch: current hash was not generated from next key";
821
+ case "SequenceGap": return `sequence number gap: expected ${issue.expected}, got ${issue.actual}`;
822
+ case "DateOrdering": return `date must be equal or later: previous is ${issue.previous}, next is ${issue.next}`;
823
+ case "NonGenesisAtZero": return "non-genesis mark at sequence 0";
824
+ case "InvalidGenesisKey": return "genesis mark must have key equal to chain_id";
825
+ }
826
+ }
827
+ /**
828
+ * Get the chain ID as a hex string for display.
829
+ */
830
+ function chainIdHex(report) {
831
+ return hexEncode(report.chainId);
832
+ }
833
+ /**
834
+ * Check if the validation report has any issues.
835
+ */
836
+ function hasIssues(report) {
837
+ for (const chain of report.chains) if (!chain.hasGenesis) return true;
838
+ for (const chain of report.chains) for (const seq of chain.sequences) for (const mark of seq.marks) if (mark.issues.length > 0) return true;
839
+ if (report.chains.length > 1) return true;
840
+ if (report.chains.length === 1 && report.chains[0].sequences.length > 1) return true;
841
+ return false;
842
+ }
843
+ /**
844
+ * Check if the validation report contains interesting information.
845
+ */
846
+ function isInteresting(report) {
847
+ if (report.chains.length === 0) return false;
848
+ for (const chain of report.chains) if (!chain.hasGenesis) return true;
849
+ if (report.chains.length === 1) {
850
+ const chain = report.chains[0];
851
+ if (chain.sequences.length === 1) {
852
+ if (chain.sequences[0].marks.every((m) => m.issues.length === 0)) return false;
853
+ }
854
+ }
855
+ return true;
856
+ }
857
+ /**
858
+ * Format the validation report as human-readable text.
859
+ */
860
+ function formatText(report) {
861
+ if (!isInteresting(report)) return "";
862
+ const lines = [];
863
+ lines.push(`Total marks: ${report.marks.length}`);
864
+ lines.push(`Chains: ${report.chains.length}`);
865
+ lines.push("");
866
+ for (let chainIdx = 0; chainIdx < report.chains.length; chainIdx++) {
867
+ const chain = report.chains[chainIdx];
868
+ const chainIdStr = chainIdHex(chain);
869
+ const shortChainId = chainIdStr.length > 8 ? chainIdStr.slice(0, 8) : chainIdStr;
870
+ lines.push(`Chain ${chainIdx + 1}: ${shortChainId}`);
871
+ if (!chain.hasGenesis) lines.push(" Warning: No genesis mark found");
872
+ for (const seq of chain.sequences) for (const flaggedMark of seq.marks) {
873
+ const mark = flaggedMark.mark;
874
+ const shortId = mark.identifier();
875
+ const seqNum = mark.seq();
876
+ const annotations = [];
877
+ if (mark.isGenesis()) annotations.push("genesis mark");
878
+ for (const issue of flaggedMark.issues) {
879
+ let issueStr;
880
+ switch (issue.type) {
881
+ case "SequenceGap":
882
+ issueStr = `gap: ${issue.expected} missing`;
883
+ break;
884
+ case "DateOrdering":
885
+ issueStr = `date ${issue.previous} < ${issue.next}`;
886
+ break;
887
+ case "HashMismatch":
888
+ issueStr = "hash mismatch";
889
+ break;
890
+ case "KeyMismatch":
891
+ issueStr = "key mismatch";
892
+ break;
893
+ case "NonGenesisAtZero":
894
+ issueStr = "non-genesis at seq 0";
895
+ break;
896
+ case "InvalidGenesisKey":
897
+ issueStr = "invalid genesis key";
898
+ break;
899
+ }
900
+ annotations.push(issueStr);
901
+ }
902
+ if (annotations.length === 0) lines.push(` ${seqNum}: ${shortId}`);
903
+ else lines.push(` ${seqNum}: ${shortId} (${annotations.join(", ")})`);
904
+ }
905
+ lines.push("");
906
+ }
907
+ return lines.join("\n").trimEnd();
908
+ }
909
+ /**
910
+ * Format the validation report.
911
+ */
912
+ function formatReport(report, format) {
913
+ switch (format) {
914
+ case ValidationReportFormat.Text: return formatText(report);
915
+ case ValidationReportFormat.JsonCompact: return JSON.stringify(reportToJSON(report));
916
+ case ValidationReportFormat.JsonPretty: return JSON.stringify(reportToJSON(report), null, 2);
917
+ }
918
+ }
919
+ /**
920
+ * Convert a report to a JSON-serializable object.
921
+ */
922
+ function reportToJSON(report) {
923
+ return {
924
+ marks: report.marks.map((m) => m.urString()),
925
+ chains: report.chains.map((chain) => ({
926
+ chain_id: hexEncode(chain.chainId),
927
+ has_genesis: chain.hasGenesis,
928
+ marks: chain.marks.map((m) => m.urString()),
929
+ sequences: chain.sequences.map((seq) => ({
930
+ start_seq: seq.startSeq,
931
+ end_seq: seq.endSeq,
932
+ marks: seq.marks.map((fm) => ({
933
+ mark: fm.mark.urString(),
934
+ issues: fm.issues.map(issueToJSON)
935
+ }))
936
+ }))
937
+ }))
938
+ };
939
+ }
940
+ /**
941
+ * Convert a ValidationIssue to JSON matching Rust's serde format.
942
+ *
943
+ * Rust uses `#[serde(tag = "type", content = "data")]` which wraps
944
+ * struct variant data in a `"data"` field. Unit variants have no
945
+ * `"data"` field.
946
+ */
947
+ function issueToJSON(issue) {
948
+ switch (issue.type) {
949
+ case "HashMismatch": return {
950
+ type: "HashMismatch",
951
+ data: {
952
+ expected: issue.expected,
953
+ actual: issue.actual
954
+ }
955
+ };
956
+ case "SequenceGap": return {
957
+ type: "SequenceGap",
958
+ data: {
959
+ expected: issue.expected,
960
+ actual: issue.actual
961
+ }
962
+ };
963
+ case "DateOrdering": return {
964
+ type: "DateOrdering",
965
+ data: {
966
+ previous: issue.previous,
967
+ next: issue.next
968
+ }
969
+ };
970
+ case "KeyMismatch": return { type: "KeyMismatch" };
971
+ case "NonGenesisAtZero": return { type: "NonGenesisAtZero" };
972
+ case "InvalidGenesisKey": return { type: "InvalidGenesisKey" };
973
+ }
974
+ }
975
+ /**
976
+ * Build sequence bins for a chain.
977
+ */
978
+ function buildSequenceBins(marks) {
979
+ const sequences = [];
980
+ let currentSequence = [];
981
+ for (let i = 0; i < marks.length; i++) {
982
+ const mark = marks[i];
983
+ if (i === 0) currentSequence.push({
984
+ mark,
985
+ issues: []
986
+ });
987
+ else {
988
+ const prev = marks[i - 1];
989
+ try {
990
+ prev.precedesOpt(mark);
991
+ currentSequence.push({
992
+ mark,
993
+ issues: []
994
+ });
995
+ } catch (e) {
996
+ if (currentSequence.length > 0) sequences.push(createSequenceReport(currentSequence));
997
+ let issue;
998
+ if (e instanceof ProvenanceMarkError && e.details?.["validationIssue"] !== void 0) issue = e.details["validationIssue"];
999
+ else issue = { type: "KeyMismatch" };
1000
+ currentSequence = [{
1001
+ mark,
1002
+ issues: [issue]
1003
+ }];
1004
+ }
1005
+ }
1006
+ }
1007
+ if (currentSequence.length > 0) sequences.push(createSequenceReport(currentSequence));
1008
+ return sequences;
1009
+ }
1010
+ /**
1011
+ * Create a sequence report from flagged marks.
1012
+ */
1013
+ function createSequenceReport(marks) {
1014
+ return {
1015
+ startSeq: marks.length > 0 ? marks[0].mark.seq() : 0,
1016
+ endSeq: marks.length > 0 ? marks[marks.length - 1].mark.seq() : 0,
1017
+ marks
1018
+ };
1019
+ }
1020
+ /**
1021
+ * Validate a collection of provenance marks.
1022
+ */
1023
+ function validate(marks) {
1024
+ const seen = /* @__PURE__ */ new Set();
1025
+ const deduplicatedMarks = [];
1026
+ for (const mark of marks) {
1027
+ const key = `${mark.res()}:${hexEncode(mark.message())}`;
1028
+ if (!seen.has(key)) {
1029
+ seen.add(key);
1030
+ deduplicatedMarks.push(mark);
1031
+ }
1032
+ }
1033
+ const chainBins = /* @__PURE__ */ new Map();
1034
+ for (const mark of deduplicatedMarks) {
1035
+ const chainIdKey = hexEncode(mark.chainId());
1036
+ const bin = chainBins.get(chainIdKey);
1037
+ if (bin !== void 0) bin.push(mark);
1038
+ else chainBins.set(chainIdKey, [mark]);
1039
+ }
1040
+ const chains = [];
1041
+ for (const [chainIdKey, chainMarks] of chainBins) {
1042
+ chainMarks.sort((a, b) => a.seq() - b.seq());
1043
+ const hasGenesis = chainMarks.length > 0 && chainMarks[0].seq() === 0 && chainMarks[0].isGenesis();
1044
+ const sequences = buildSequenceBins(chainMarks);
1045
+ chains.push({
1046
+ chainId: hexDecode(chainIdKey),
1047
+ hasGenesis,
1048
+ marks: chainMarks,
1049
+ sequences
1050
+ });
1051
+ }
1052
+ chains.sort((a, b) => hexEncode(a.chainId).localeCompare(hexEncode(b.chainId)));
1053
+ return {
1054
+ marks: deduplicatedMarks,
1055
+ chains
1056
+ };
1057
+ }
1058
+ /**
1059
+ * Helper function to encode bytes as hex.
1060
+ */
1061
+ function hexEncode(bytes) {
1062
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1063
+ }
1064
+ /**
1065
+ * Helper function to decode hex to bytes.
1066
+ */
1067
+ function hexDecode(hex) {
1068
+ const bytes = new Uint8Array(hex.length / 2);
1069
+ for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
1070
+ return bytes;
1071
+ }
1072
+
800
1073
  //#endregion
801
1074
  //#region src/mark.ts
802
1075
  /**
@@ -948,6 +1221,33 @@ var ProvenanceMark = class ProvenanceMark {
948
1221
  return prefix ? `\u{1F151} ${s}` : s;
949
1222
  }
950
1223
  /**
1224
+ * A compact 8-letter identifier derived from the upper-case ByteWords
1225
+ * identifier by taking the first and last letter of each ByteWords word
1226
+ * (4 words x 2 letters = 8 letters).
1227
+ *
1228
+ * Example: "ABLE ACID ALSO APEX" -> "AEADAOAX"
1229
+ * If prefix is true, prepends the provenance mark prefix character.
1230
+ */
1231
+ bytewordsMinimalIdentifier(prefix) {
1232
+ const full = (0, _bcts_uniform_resources.encodeBytewordsIdentifier)(this._hash.slice(0, 4));
1233
+ const words = full.split(/\s+/);
1234
+ let out = "";
1235
+ if (words.length === 4) for (const w of words) {
1236
+ if (w.length === 0) continue;
1237
+ out += w[0].toUpperCase();
1238
+ out += w[w.length - 1].toUpperCase();
1239
+ }
1240
+ if (out.length !== 8) {
1241
+ out = "";
1242
+ const compact = full.replace(/[^a-zA-Z]/g, "").toUpperCase();
1243
+ for (let i = 0; i + 3 < compact.length; i += 4) {
1244
+ out += compact[i];
1245
+ out += compact[i + 3];
1246
+ }
1247
+ }
1248
+ return prefix ? `\u{1F151} ${out}` : out;
1249
+ }
1250
+ /**
951
1251
  * Get the first four bytes of the hash as Bytemoji.
952
1252
  */
953
1253
  bytemojiIdentifier(prefix) {
@@ -967,18 +1267,39 @@ var ProvenanceMark = class ProvenanceMark {
967
1267
  }
968
1268
  /**
969
1269
  * Check if this mark precedes another mark, throwing on validation errors.
1270
+ * Errors carry a structured `validationIssue` in their details, matching Rust's
1271
+ * `Error::Validation(ValidationIssue)` pattern.
970
1272
  */
971
1273
  precedesOpt(next) {
972
- if (next._seq === 0) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, void 0, { message: "non-genesis mark at sequence 0" });
973
- if (arraysEqual(next._key, next._chainId)) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, void 0, { message: "genesis mark must have key equal to chain_id" });
974
- if (this._seq !== next._seq - 1) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, void 0, { message: `sequence gap: expected ${this._seq + 1}, got ${next._seq}` });
975
- if (this._date > next._date) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, void 0, { message: `date ordering: ${this._date.toISOString()} > ${next._date.toISOString()}` });
1274
+ if (next._seq === 0) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, "non-genesis mark at sequence 0", { validationIssue: { type: "NonGenesisAtZero" } });
1275
+ if (arraysEqual(next._key, next._chainId)) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, "genesis mark must have key equal to chain_id", { validationIssue: { type: "InvalidGenesisKey" } });
1276
+ if (this._seq !== next._seq - 1) {
1277
+ const issue = {
1278
+ type: "SequenceGap",
1279
+ expected: this._seq + 1,
1280
+ actual: next._seq
1281
+ };
1282
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, `sequence gap: expected ${this._seq + 1}, got ${next._seq}`, { validationIssue: issue });
1283
+ }
1284
+ if (this._date > next._date) {
1285
+ const dateStr = this._date.toISOString().replace(".000Z", "Z");
1286
+ const nextDateStr = next._date.toISOString().replace(".000Z", "Z");
1287
+ const issue = {
1288
+ type: "DateOrdering",
1289
+ previous: dateStr,
1290
+ next: nextDateStr
1291
+ };
1292
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, `date ordering: ${dateStr} > ${nextDateStr}`, { validationIssue: issue });
1293
+ }
976
1294
  const expectedHash = ProvenanceMark.makeHash(this._res, this._key, next._key, this._chainId, this._seqBytes, this._dateBytes, this._infoBytes);
977
- if (!arraysEqual(this._hash, expectedHash)) throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, void 0, {
978
- message: "hash mismatch",
979
- expected: Array.from(expectedHash).map((b) => b.toString(16).padStart(2, "0")).join(""),
980
- actual: Array.from(this._hash).map((b) => b.toString(16).padStart(2, "0")).join("")
981
- });
1295
+ if (!arraysEqual(this._hash, expectedHash)) {
1296
+ const issue = {
1297
+ type: "HashMismatch",
1298
+ expected: bytesToHex(expectedHash),
1299
+ actual: bytesToHex(this._hash)
1300
+ };
1301
+ throw new ProvenanceMarkError(ProvenanceMarkErrorType.ValidationError, `hash mismatch: expected ${bytesToHex(expectedHash)}, got ${bytesToHex(this._hash)}`, { validationIssue: issue });
1302
+ }
982
1303
  }
983
1304
  /**
984
1305
  * Check if a sequence of marks is valid.
@@ -1176,30 +1497,35 @@ var ProvenanceMark = class ProvenanceMark {
1176
1497
  return new ProvenanceMark(res, key, hash, chainId, seqBytes, dateBytes, infoBytes, seq, date);
1177
1498
  }
1178
1499
  /**
1179
- * Convert this provenance mark to a Gordian Envelope.
1500
+ * Validate a collection of provenance marks.
1180
1501
  *
1181
- * The envelope contains the tagged CBOR representation of the mark.
1502
+ * Matches Rust: `ProvenanceMark::validate()` which delegates to
1503
+ * `ValidationReport::validate()`.
1504
+ */
1505
+ static validate(marks) {
1506
+ return validate(marks);
1507
+ }
1508
+ /**
1509
+ * Convert this provenance mark to a Gordian Envelope.
1182
1510
  *
1183
- * Note: Use provenanceMarkToEnvelope() for a standalone function alternative.
1511
+ * Creates a leaf envelope containing the tagged CBOR representation.
1512
+ * Matches Rust: `Envelope::new(mark.to_cbor())` which creates a CBOR leaf.
1184
1513
  */
1185
1514
  intoEnvelope() {
1186
- return _bcts_envelope.Envelope.new(this.toCborData());
1515
+ return _bcts_envelope.Envelope.newLeaf(this.taggedCbor());
1187
1516
  }
1188
1517
  /**
1189
1518
  * Extract a ProvenanceMark from a Gordian Envelope.
1190
1519
  *
1520
+ * Matches Rust: `envelope.subject().try_leaf()?.try_into()`
1521
+ *
1191
1522
  * @param envelope - The envelope to extract from
1192
1523
  * @returns The extracted provenance mark
1193
1524
  * @throws ProvenanceMarkError if extraction fails
1194
1525
  */
1195
1526
  static fromEnvelope(envelope) {
1196
- const bytes = envelope.asByteString();
1197
- if (bytes !== void 0) return ProvenanceMark.fromCborData(bytes);
1198
- const envCase = envelope.case();
1199
- if (envCase.type === "node") {
1200
- const subjectBytes = envCase.subject.asByteString();
1201
- if (subjectBytes !== void 0) return ProvenanceMark.fromCborData(subjectBytes);
1202
- }
1527
+ const leaf = envelope.subject().asLeaf();
1528
+ if (leaf !== void 0) return ProvenanceMark.fromTaggedCbor(leaf);
1203
1529
  throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, void 0, { message: "Could not extract ProvenanceMark from envelope" });
1204
1530
  }
1205
1531
  };
@@ -1392,276 +1718,6 @@ var ProvenanceMarkGenerator = class ProvenanceMarkGenerator {
1392
1718
  }
1393
1719
  };
1394
1720
 
1395
- //#endregion
1396
- //#region src/validate.ts
1397
- /**
1398
- * Format for validation report output.
1399
- */
1400
- let ValidationReportFormat = /* @__PURE__ */ function(ValidationReportFormat) {
1401
- /** Human-readable text format */
1402
- ValidationReportFormat["Text"] = "text";
1403
- /** Compact JSON format (no whitespace) */
1404
- ValidationReportFormat["JsonCompact"] = "json-compact";
1405
- /** Pretty-printed JSON format (with indentation) */
1406
- ValidationReportFormat["JsonPretty"] = "json-pretty";
1407
- return ValidationReportFormat;
1408
- }({});
1409
- /**
1410
- * Format a validation issue as a string.
1411
- */
1412
- function formatValidationIssue(issue) {
1413
- switch (issue.type) {
1414
- case "HashMismatch": return `hash mismatch: expected ${issue.expected}, got ${issue.actual}`;
1415
- case "KeyMismatch": return "key mismatch: current hash was not generated from next key";
1416
- case "SequenceGap": return `sequence number gap: expected ${issue.expected}, got ${issue.actual}`;
1417
- case "DateOrdering": return `date must be equal or later: previous is ${issue.previous}, next is ${issue.next}`;
1418
- case "NonGenesisAtZero": return "non-genesis mark at sequence 0";
1419
- case "InvalidGenesisKey": return "genesis mark must have key equal to chain_id";
1420
- }
1421
- }
1422
- /**
1423
- * Get the chain ID as a hex string for display.
1424
- */
1425
- function chainIdHex(report) {
1426
- return hexEncode(report.chainId);
1427
- }
1428
- /**
1429
- * Check if the validation report has any issues.
1430
- */
1431
- function hasIssues(report) {
1432
- for (const chain of report.chains) if (!chain.hasGenesis) return true;
1433
- for (const chain of report.chains) for (const seq of chain.sequences) for (const mark of seq.marks) if (mark.issues.length > 0) return true;
1434
- if (report.chains.length > 1) return true;
1435
- if (report.chains.length === 1 && report.chains[0].sequences.length > 1) return true;
1436
- return false;
1437
- }
1438
- /**
1439
- * Check if the validation report contains interesting information.
1440
- */
1441
- function isInteresting(report) {
1442
- if (report.chains.length === 0) return false;
1443
- for (const chain of report.chains) if (!chain.hasGenesis) return true;
1444
- if (report.chains.length === 1) {
1445
- const chain = report.chains[0];
1446
- if (chain.sequences.length === 1) {
1447
- if (chain.sequences[0].marks.every((m) => m.issues.length === 0)) return false;
1448
- }
1449
- }
1450
- return true;
1451
- }
1452
- /**
1453
- * Format the validation report as human-readable text.
1454
- */
1455
- function formatText(report) {
1456
- if (!isInteresting(report)) return "";
1457
- const lines = [];
1458
- lines.push(`Total marks: ${report.marks.length}`);
1459
- lines.push(`Chains: ${report.chains.length}`);
1460
- lines.push("");
1461
- for (let chainIdx = 0; chainIdx < report.chains.length; chainIdx++) {
1462
- const chain = report.chains[chainIdx];
1463
- const chainIdStr = chainIdHex(chain);
1464
- const shortChainId = chainIdStr.length > 8 ? chainIdStr.slice(0, 8) : chainIdStr;
1465
- lines.push(`Chain ${chainIdx + 1}: ${shortChainId}`);
1466
- if (!chain.hasGenesis) lines.push(" Warning: No genesis mark found");
1467
- for (const seq of chain.sequences) for (const flaggedMark of seq.marks) {
1468
- const mark = flaggedMark.mark;
1469
- const shortId = mark.identifier();
1470
- const seqNum = mark.seq();
1471
- const annotations = [];
1472
- if (mark.isGenesis()) annotations.push("genesis mark");
1473
- for (const issue of flaggedMark.issues) {
1474
- let issueStr;
1475
- switch (issue.type) {
1476
- case "SequenceGap":
1477
- issueStr = `gap: ${issue.expected} missing`;
1478
- break;
1479
- case "DateOrdering":
1480
- issueStr = `date ${issue.previous} < ${issue.next}`;
1481
- break;
1482
- case "HashMismatch":
1483
- issueStr = "hash mismatch";
1484
- break;
1485
- case "KeyMismatch":
1486
- issueStr = "key mismatch";
1487
- break;
1488
- case "NonGenesisAtZero":
1489
- issueStr = "non-genesis at seq 0";
1490
- break;
1491
- case "InvalidGenesisKey":
1492
- issueStr = "invalid genesis key";
1493
- break;
1494
- }
1495
- annotations.push(issueStr);
1496
- }
1497
- if (annotations.length === 0) lines.push(` ${seqNum}: ${shortId}`);
1498
- else lines.push(` ${seqNum}: ${shortId} (${annotations.join(", ")})`);
1499
- }
1500
- lines.push("");
1501
- }
1502
- return lines.join("\n").trimEnd();
1503
- }
1504
- /**
1505
- * Format the validation report.
1506
- */
1507
- function formatReport(report, format) {
1508
- switch (format) {
1509
- case ValidationReportFormat.Text: return formatText(report);
1510
- case ValidationReportFormat.JsonCompact: return JSON.stringify(reportToJSON(report));
1511
- case ValidationReportFormat.JsonPretty: return JSON.stringify(reportToJSON(report), null, 2);
1512
- }
1513
- }
1514
- /**
1515
- * Convert a report to a JSON-serializable object.
1516
- */
1517
- function reportToJSON(report) {
1518
- return {
1519
- marks: report.marks.map((m) => m.toUrlEncoding()),
1520
- chains: report.chains.map((chain) => ({
1521
- chain_id: hexEncode(chain.chainId),
1522
- has_genesis: chain.hasGenesis,
1523
- marks: chain.marks.map((m) => m.toUrlEncoding()),
1524
- sequences: chain.sequences.map((seq) => ({
1525
- start_seq: seq.startSeq,
1526
- end_seq: seq.endSeq,
1527
- marks: seq.marks.map((fm) => ({
1528
- mark: fm.mark.toUrlEncoding(),
1529
- issues: fm.issues
1530
- }))
1531
- }))
1532
- }))
1533
- };
1534
- }
1535
- /**
1536
- * Build sequence bins for a chain.
1537
- */
1538
- function buildSequenceBins(marks) {
1539
- const sequences = [];
1540
- let currentSequence = [];
1541
- for (let i = 0; i < marks.length; i++) {
1542
- const mark = marks[i];
1543
- if (i === 0) currentSequence.push({
1544
- mark,
1545
- issues: []
1546
- });
1547
- else {
1548
- const prev = marks[i - 1];
1549
- try {
1550
- prev.precedesOpt(mark);
1551
- currentSequence.push({
1552
- mark,
1553
- issues: []
1554
- });
1555
- } catch (e) {
1556
- if (currentSequence.length > 0) sequences.push(createSequenceReport(currentSequence));
1557
- currentSequence = [{
1558
- mark,
1559
- issues: [parseValidationError(e, prev, mark)]
1560
- }];
1561
- }
1562
- }
1563
- }
1564
- if (currentSequence.length > 0) sequences.push(createSequenceReport(currentSequence));
1565
- return sequences;
1566
- }
1567
- /**
1568
- * Parse a validation error into a ValidationIssue.
1569
- */
1570
- function parseValidationError(e, prev, next) {
1571
- const message = e instanceof Error ? e.message : "";
1572
- if (message !== "" && message.includes("non-genesis mark at sequence 0")) return { type: "NonGenesisAtZero" };
1573
- if (message !== "" && message.includes("genesis mark must have key equal to chain_id")) return { type: "InvalidGenesisKey" };
1574
- if (message !== "" && message.includes("sequence gap")) {
1575
- const match = /expected (\d+), got (\d+)/.exec(message);
1576
- if (match !== null) return {
1577
- type: "SequenceGap",
1578
- expected: parseInt(match[1], 10),
1579
- actual: parseInt(match[2], 10)
1580
- };
1581
- }
1582
- if (message !== "" && message.includes("date ordering")) return {
1583
- type: "DateOrdering",
1584
- previous: prev.date().toISOString(),
1585
- next: next.date().toISOString()
1586
- };
1587
- if (message !== "" && message.includes("hash mismatch")) {
1588
- const match = /expected: (\w+), actual: (\w+)/.exec(message);
1589
- if (match !== null) return {
1590
- type: "HashMismatch",
1591
- expected: match[1],
1592
- actual: match[2]
1593
- };
1594
- return {
1595
- type: "HashMismatch",
1596
- expected: "",
1597
- actual: ""
1598
- };
1599
- }
1600
- return { type: "KeyMismatch" };
1601
- }
1602
- /**
1603
- * Create a sequence report from flagged marks.
1604
- */
1605
- function createSequenceReport(marks) {
1606
- return {
1607
- startSeq: marks.length > 0 ? marks[0].mark.seq() : 0,
1608
- endSeq: marks.length > 0 ? marks[marks.length - 1].mark.seq() : 0,
1609
- marks
1610
- };
1611
- }
1612
- /**
1613
- * Validate a collection of provenance marks.
1614
- */
1615
- function validate(marks) {
1616
- const seen = /* @__PURE__ */ new Set();
1617
- const deduplicatedMarks = [];
1618
- for (const mark of marks) {
1619
- const key = mark.toUrlEncoding();
1620
- if (!seen.has(key)) {
1621
- seen.add(key);
1622
- deduplicatedMarks.push(mark);
1623
- }
1624
- }
1625
- const chainBins = /* @__PURE__ */ new Map();
1626
- for (const mark of deduplicatedMarks) {
1627
- const chainIdKey = hexEncode(mark.chainId());
1628
- const bin = chainBins.get(chainIdKey);
1629
- if (bin !== void 0) bin.push(mark);
1630
- else chainBins.set(chainIdKey, [mark]);
1631
- }
1632
- const chains = [];
1633
- for (const [chainIdKey, chainMarks] of chainBins) {
1634
- chainMarks.sort((a, b) => a.seq() - b.seq());
1635
- const hasGenesis = chainMarks.length > 0 && chainMarks[0].seq() === 0 && chainMarks[0].isGenesis();
1636
- const sequences = buildSequenceBins(chainMarks);
1637
- chains.push({
1638
- chainId: hexDecode(chainIdKey),
1639
- hasGenesis,
1640
- marks: chainMarks,
1641
- sequences
1642
- });
1643
- }
1644
- chains.sort((a, b) => hexEncode(a.chainId).localeCompare(hexEncode(b.chainId)));
1645
- return {
1646
- marks: deduplicatedMarks,
1647
- chains
1648
- };
1649
- }
1650
- /**
1651
- * Helper function to encode bytes as hex.
1652
- */
1653
- function hexEncode(bytes) {
1654
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1655
- }
1656
- /**
1657
- * Helper function to decode hex to bytes.
1658
- */
1659
- function hexDecode(hex) {
1660
- const bytes = new Uint8Array(hex.length / 2);
1661
- for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
1662
- return bytes;
1663
- }
1664
-
1665
1721
  //#endregion
1666
1722
  //#region src/mark-info.ts
1667
1723
  /**
@@ -1711,7 +1767,7 @@ var ProvenanceMarkInfo = class ProvenanceMarkInfo {
1711
1767
  const lines = [];
1712
1768
  lines.push("---");
1713
1769
  lines.push("");
1714
- lines.push(this._mark.date().toISOString());
1770
+ lines.push(this._mark.date().toISOString().replace(".000Z", "Z"));
1715
1771
  lines.push("");
1716
1772
  lines.push(`#### ${this._ur.toString()}`);
1717
1773
  lines.push("");
@@ -1766,111 +1822,93 @@ var ProvenanceMarkInfo = class ProvenanceMarkInfo {
1766
1822
  /**
1767
1823
  * Registers provenance mark tags in the global format context.
1768
1824
  *
1769
- * This function sets up a summarizer for the PROVENANCE_MARK tag that displays
1770
- * provenance marks in a human-readable format.
1825
+ * Matches Rust: register_tags()
1771
1826
  */
1772
1827
  function registerTags() {
1773
- registerTagsIn(globalTagsContext);
1828
+ (0, _bcts_envelope.withFormatContextMut)((context) => {
1829
+ registerTagsIn(context);
1830
+ });
1774
1831
  }
1775
1832
  /**
1776
1833
  * Registers provenance mark tags in a specific format context.
1777
1834
  *
1835
+ * Matches Rust: register_tags_in()
1836
+ *
1778
1837
  * @param context - The format context to register tags in
1779
1838
  */
1780
1839
  function registerTagsIn(context) {
1781
- context.setSummarizer(Number(_bcts_tags.PROVENANCE_MARK.value), (cborValue) => {
1782
- return ProvenanceMark.fromUntaggedCbor(cborValue).toString();
1840
+ (0, _bcts_envelope.registerTagsIn)(context);
1841
+ context.tags().setSummarizer(BigInt(_bcts_tags.PROVENANCE_MARK.value), (untaggedCbor, _flat) => {
1842
+ try {
1843
+ return {
1844
+ ok: true,
1845
+ value: ProvenanceMark.fromUntaggedCbor(untaggedCbor).toString()
1846
+ };
1847
+ } catch {
1848
+ return {
1849
+ ok: false,
1850
+ error: {
1851
+ type: "Custom",
1852
+ message: "invalid provenance mark"
1853
+ }
1854
+ };
1855
+ }
1783
1856
  });
1784
1857
  }
1785
- const globalTagsContext = { setSummarizer(_tag, _summarizer) {} };
1786
1858
  /**
1787
1859
  * Convert a ProvenanceMark to an Envelope.
1788
1860
  *
1789
- * The envelope contains the tagged CBOR representation of the mark.
1861
+ * Delegates to ProvenanceMark.intoEnvelope() single source of truth.
1790
1862
  *
1791
1863
  * @param mark - The provenance mark to convert
1792
1864
  * @returns An envelope containing the mark
1793
1865
  */
1794
1866
  function provenanceMarkToEnvelope(mark) {
1795
- return _bcts_envelope.Envelope.new(mark.toCborData());
1867
+ return mark.intoEnvelope();
1796
1868
  }
1797
1869
  /**
1798
1870
  * Extract a ProvenanceMark from an Envelope.
1799
1871
  *
1872
+ * Delegates to ProvenanceMark.fromEnvelope() — single source of truth.
1873
+ *
1800
1874
  * @param envelope - The envelope to extract from
1801
1875
  * @returns The extracted provenance mark
1802
1876
  * @throws ProvenanceMarkError if extraction fails
1803
1877
  */
1804
1878
  function provenanceMarkFromEnvelope(envelope) {
1805
- const bytes = envelope.asByteString();
1806
- if (bytes !== void 0) return ProvenanceMark.fromCborData(bytes);
1807
- const envCase = envelope.case();
1808
- if (envCase.type === "node") {
1809
- const subjectBytes = envCase.subject.asByteString();
1810
- if (subjectBytes !== void 0) return ProvenanceMark.fromCborData(subjectBytes);
1811
- }
1812
- throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, void 0, { message: "Could not extract ProvenanceMark from envelope" });
1879
+ return ProvenanceMark.fromEnvelope(envelope);
1813
1880
  }
1814
1881
  /**
1815
1882
  * Convert a ProvenanceMarkGenerator to an Envelope.
1816
1883
  *
1817
- * The envelope contains structured assertions for all generator fields:
1818
- * - type: "provenance-generator"
1819
- * - res: The resolution
1820
- * - seed: The seed
1821
- * - next-seq: The next sequence number
1822
- * - rng-state: The RNG state
1884
+ * Delegates to ProvenanceMarkGenerator.intoEnvelope() single source of truth.
1823
1885
  *
1824
1886
  * @param generator - The generator to convert
1825
1887
  * @returns An envelope containing the generator
1826
1888
  */
1827
1889
  function provenanceMarkGeneratorToEnvelope(generator) {
1828
- let envelope = _bcts_envelope.Envelope.new(generator.chainId());
1829
- envelope = envelope.addType("provenance-generator");
1830
- envelope = envelope.addAssertion("res", resolutionToNumber(generator.res()));
1831
- envelope = envelope.addAssertion("seed", generator.seed().toBytes());
1832
- envelope = envelope.addAssertion("next-seq", generator.nextSeq());
1833
- envelope = envelope.addAssertion("rng-state", generator.rngState().toBytes());
1834
- return envelope;
1890
+ return generator.intoEnvelope();
1835
1891
  }
1836
1892
  /**
1837
1893
  * Extract a ProvenanceMarkGenerator from an Envelope.
1838
1894
  *
1895
+ * Delegates to ProvenanceMarkGenerator.fromEnvelope() — single source of truth.
1896
+ *
1839
1897
  * @param envelope - The envelope to extract from
1840
1898
  * @returns The extracted generator
1841
1899
  * @throws ProvenanceMarkError if extraction fails
1842
1900
  */
1843
1901
  function provenanceMarkGeneratorFromEnvelope(envelope) {
1844
- const env = envelope;
1845
- if (!env.hasType("provenance-generator")) throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, void 0, { message: "Envelope is not a provenance-generator" });
1846
- const chainId = env.subject().asByteString();
1847
- if (chainId === void 0) throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, void 0, { message: "Could not extract chain ID" });
1848
- const extractAssertion = (predicate) => {
1849
- const assertions = env.assertionsWithPredicate(predicate);
1850
- if (assertions.length === 0) throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, void 0, { message: `Missing ${predicate} assertion` });
1851
- const assertionCase = assertions[0].case();
1852
- if (assertionCase.type !== "assertion") throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, void 0, { message: `Invalid ${predicate} assertion` });
1853
- const obj = assertionCase.assertion.object();
1854
- const objCase = obj.case();
1855
- if (objCase.type === "leaf") return {
1856
- cbor: objCase.cbor,
1857
- bytes: obj.asByteString()
1858
- };
1859
- throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, void 0, { message: `Invalid ${predicate} value` });
1860
- };
1861
- const res = resolutionFromCbor(extractAssertion("res").cbor);
1862
- const seedValue = extractAssertion("seed");
1863
- if (seedValue.bytes === void 0) throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, void 0, { message: "Invalid seed data" });
1864
- const seed = ProvenanceSeed.fromBytes(seedValue.bytes);
1865
- const seqValue = extractAssertion("next-seq");
1866
- const nextSeq = Number(seqValue.cbor);
1867
- const rngValue = extractAssertion("rng-state");
1868
- if (rngValue.bytes === void 0) throw new ProvenanceMarkError(ProvenanceMarkErrorType.CborError, void 0, { message: "Invalid rng-state data" });
1869
- const rngState = RngState.fromBytes(rngValue.bytes);
1870
- return ProvenanceMarkGenerator.new(res, seed, chainId, nextSeq, rngState);
1902
+ return ProvenanceMarkGenerator.fromEnvelope(envelope);
1871
1903
  }
1872
1904
 
1873
1905
  //#endregion
1906
+ Object.defineProperty(exports, 'FormatContext', {
1907
+ enumerable: true,
1908
+ get: function () {
1909
+ return _bcts_envelope.FormatContext;
1910
+ }
1911
+ });
1874
1912
  exports.PROVENANCE_SEED_LENGTH = PROVENANCE_SEED_LENGTH;
1875
1913
  exports.ProvenanceMark = ProvenanceMark;
1876
1914
  exports.ProvenanceMarkError = ProvenanceMarkError;