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