@sanning/proof 0.13.0 → 0.15.0

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/case-file.js CHANGED
@@ -24,7 +24,7 @@
24
24
  * Pure: bytes and a file list in, a reading out. No filesystem, no zip, no
25
25
  * network. How the files were listed and hashed is the caller's.
26
26
  */
27
- import { jcsParse } from "./verifier.js";
27
+ import { DuplicateKeyError, jcsParse } from "./verifier.js";
28
28
  /** The index's dispatch key (5.6.2). An absent or unknown value makes the index unreadable. */
29
29
  export const CASE_FILE_VERSION = "sanning.case-file/v1";
30
30
  /** The index, at the root of the case file. */
@@ -176,7 +176,11 @@ function readIndex(raw) {
176
176
  try {
177
177
  value = jcsParse(raw);
178
178
  }
179
- catch {
179
+ catch (e) {
180
+ // Named, because an index that lists one pack twice under one member name
181
+ // reads as whichever copy came last (I-JSON, proof#144).
182
+ if (e instanceof DuplicateKeyError)
183
+ throw new Unreadable(e.message);
180
184
  throw new Unreadable("not readable as JSON");
181
185
  }
182
186
  if (!isObject(value))
package/dist/case.d.ts CHANGED
@@ -638,13 +638,21 @@ export declare const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
638
638
  /** The reader was handed no packs at all. */
639
639
  export declare const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
640
640
  /**
641
- * The two NOTEs (5.5.3 v1.20): findings on the reading with no contribution,
642
- * printed in the scope sentence so a clean verdict never hides either.
643
- * `pack_undeclared` is a note only where the reading holds no hand-off record;
644
- * where it holds one, it is the exception of that name.
641
+ * The three NOTEs (5.5.3 v1.20, extended proof#148): findings on the reading
642
+ * with no contribution, printed in the scope sentence so a clean verdict
643
+ * never hides any of them. `pack_undeclared` is a note only where the
644
+ * reading holds no hand-off record; where it holds one, it is the exception
645
+ * of that name.
645
646
  */
646
647
  export declare const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
647
- export declare const CASE_NOTE_KINDS: readonly ["pack_undeclared", "content_withheld_by_declaration"];
648
+ /**
649
+ * A pack's wrapper carries a present `co_signatures` (evidence-bundle.md §2,
650
+ * decided by Phil 2026-09-24 on proof#148). The field verifies nothing and
651
+ * sits outside the wrapper's signed scope, so the case reading names it
652
+ * rather than presenting it as evidence, and it contributes no verdict.
653
+ */
654
+ export declare const CO_SIGNATURES_PRESENT = "co_signatures_present";
655
+ export declare const CASE_NOTE_KINDS: readonly ["pack_undeclared", "content_withheld_by_declaration", "co_signatures_present"];
648
656
  export type CaseNoteKind = (typeof CASE_NOTE_KINDS)[number];
649
657
  /**
650
658
  * Every exception this reader can report, as a closed set. A surface may render
@@ -872,10 +880,30 @@ export declare function renderCaseSummary(reading: CaseReading): string;
872
880
  export declare const VERDICT_DISPLAY_TITLES: Readonly<Record<string, string>>;
873
881
  /**
874
882
  * What a reader may rely on, for each of the six words a case reading reaches
875
- * (5.5.6 v1.20, A26), the same for every drop. `pending` and `not_found`
876
- * never head a case reading: they are witness states.
883
+ * (5.5.6 v1.20, A26; the `verified` text at v1.28, A35), the same for every
884
+ * drop. `pending` and `not_found` never head a case reading: they are
885
+ * witness states. `verified` is the base sentence, printed alone where the
886
+ * drop holds no hand-off record; a caller reads `caseReliance` for
887
+ * `verified`, never this mapping directly.
877
888
  */
878
889
  export declare const CASE_RELIANCE: Readonly<Record<CaseVerdict, string>>;
890
+ /**
891
+ * The clause a `verified` drop that holds a hand-off record gains, inserted
892
+ * after `CASE_RELIANCE`'s first sentence (5.5.6, A35). Never printed alone,
893
+ * and never where `handoffRecords` is zero: most drops hold one agent's
894
+ * pack and no hand-off to say anything about.
895
+ */
896
+ export declare const CASE_RELIANCE_HANDOFF_CLAUSE = "Every hand-off with both sides in this drop is intact.";
897
+ /**
898
+ * What a reader may rely on for one case reading (5.5.6 v1.28, A35), byte
899
+ * for byte the text `case_reliance`'s Python sibling returns.
900
+ *
901
+ * For every word but `verified` this is `CASE_RELIANCE` unchanged. For
902
+ * `verified`, the hand-off clause is inserted after the first sentence
903
+ * where `handoffRecords` is not zero, and left out where it is zero: the
904
+ * base sentence never claims a hand-off the drop does not hold.
905
+ */
906
+ export declare function caseReliance(verdict: CaseVerdict, handoffRecords: number): string;
879
907
  /** The three witness states, and how each is printed (5.5.6, D7). No other. */
880
908
  export declare const WITNESS_STATES: {
881
909
  readonly confirmed: "Confirmed on the public record";
@@ -894,8 +922,24 @@ export interface WitnessLine {
894
922
  state: WitnessState;
895
923
  reasons: string[];
896
924
  services: string[];
925
+ /**
926
+ * Witness times a pack caches that no lookup settled, as `[blockHeight,
927
+ * blockTime]`, distinct and in time order (5.6.6, v1.25, A30). Printed labelled
928
+ * not checked, and never as a time a reader may rely on. Absent reads as none.
929
+ */
930
+ cached?: [number, number][];
931
+ /**
932
+ * Witness times the public record returned where they matched a pack's
933
+ * cache, distinct and in time order. The gateway's answer, named with the
934
+ * service, never the cache's value. Absent reads as none.
935
+ */
936
+ recorded?: [number, number][];
897
937
  }
898
938
  /** The line printed where no lookup ran, which is every reading this kernel produces on its own. */
899
939
  export declare const WITNESS_NOT_LOOKED_UP: Readonly<WitnessLine>;
900
940
  /** The witness line as both kernels print it. */
941
+ /** How a witness time a pack caches is printed where nothing checked it (5.5.6, 5.6.6, v1.25, A30). */
942
+ export declare const WITNESS_TIME_CACHED = "the pack dates the witness, not checked";
943
+ /** How a witness time the public record returned is printed where it matched the cache. */
944
+ export declare const WITNESS_TIME_RECORDED = "the public record dates the witness";
901
945
  export declare function renderWitnessLine(line: WitnessLine): string;
package/dist/case.js CHANGED
@@ -243,8 +243,9 @@
243
243
  */
244
244
  import { DECLARATION_VERSION, DECLARATION_VERSION_V2, checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
245
245
  import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, } from "./handoff.js";
246
- import { verifyEvidenceBundle } from "./evidence.js";
246
+ import { coSignaturesPresent, verifyEvidenceBundle } from "./evidence.js";
247
247
  import { accountDisclosure, completenessNotes, gradePack, } from "./grading.js";
248
+ import { renderWitnessTime } from "./witness-cache.js";
248
249
  // ---- The vocabulary ---------------------------------------------------------
249
250
  /**
250
251
  * The verdicts a case can reach. Every one is already in use in
@@ -665,13 +666,25 @@ export const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
665
666
  /** The reader was handed no packs at all. */
666
667
  export const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
667
668
  /**
668
- * The two NOTEs (5.5.3 v1.20): findings on the reading with no contribution,
669
- * printed in the scope sentence so a clean verdict never hides either.
670
- * `pack_undeclared` is a note only where the reading holds no hand-off record;
671
- * where it holds one, it is the exception of that name.
669
+ * The three NOTEs (5.5.3 v1.20, extended proof#148): findings on the reading
670
+ * with no contribution, printed in the scope sentence so a clean verdict
671
+ * never hides any of them. `pack_undeclared` is a note only where the
672
+ * reading holds no hand-off record; where it holds one, it is the exception
673
+ * of that name.
672
674
  */
673
675
  export const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
674
- export const CASE_NOTE_KINDS = [PACK_UNDECLARED, CONTENT_WITHHELD_BY_DECLARATION];
676
+ /**
677
+ * A pack's wrapper carries a present `co_signatures` (evidence-bundle.md §2,
678
+ * decided by Phil 2026-09-24 on proof#148). The field verifies nothing and
679
+ * sits outside the wrapper's signed scope, so the case reading names it
680
+ * rather than presenting it as evidence, and it contributes no verdict.
681
+ */
682
+ export const CO_SIGNATURES_PRESENT = "co_signatures_present";
683
+ export const CASE_NOTE_KINDS = [
684
+ PACK_UNDECLARED,
685
+ CONTENT_WITHHELD_BY_DECLARATION,
686
+ CO_SIGNATURES_PRESENT,
687
+ ];
675
688
  /**
676
689
  * Every exception this reader can report, as a closed set. A surface may render
677
690
  * them and a test may assert over them; nothing outside this array is emitted.
@@ -1048,6 +1061,15 @@ export async function readCase(packs) {
1048
1061
  continue;
1049
1062
  }
1050
1063
  holderKeys.add(holder);
1064
+ if (coSignaturesPresent(pack.bundle)) {
1065
+ // A NOTE, with no contribution (5.5.3 v1.20, proof#148): the wrapper's
1066
+ // co_signatures verifies nothing and sits outside its signed scope, so
1067
+ // the case reading names it rather than moving anything on its account.
1068
+ caseNotes.push({
1069
+ position,
1070
+ note: { kind: CO_SIGNATURES_PRESENT, packRef: pack.ref },
1071
+ });
1072
+ }
1051
1073
  const bodyType = bodyTypeOf(pack.bundle);
1052
1074
  const records = packRecords(pack.bundle, pack.content);
1053
1075
  // The per-record gradings below are joined to `records` BY INDEX, so a
@@ -1591,6 +1613,9 @@ export function renderCaseNote(note) {
1591
1613
  "checked, their content was not");
1592
1614
  case PACK_UNDECLARED:
1593
1615
  return `${note.packRef} does not say what it holds`;
1616
+ case CO_SIGNATURES_PRESENT:
1617
+ return (`${note.packRef} carries unverified co-signatures in its wrapper; ` +
1618
+ "they are not part of the evidence");
1594
1619
  default:
1595
1620
  return note.kind;
1596
1621
  }
@@ -1645,16 +1670,17 @@ export const VERDICT_DISPLAY_TITLES = {
1645
1670
  };
1646
1671
  /**
1647
1672
  * What a reader may rely on, for each of the six words a case reading reaches
1648
- * (5.5.6 v1.20, A26), the same for every drop. `pending` and `not_found`
1649
- * never head a case reading: they are witness states.
1673
+ * (5.5.6 v1.20, A26; the `verified` text at v1.28, A35), the same for every
1674
+ * drop. `pending` and `not_found` never head a case reading: they are
1675
+ * witness states. `verified` is the base sentence, printed alone where the
1676
+ * drop holds no hand-off record; a caller reads `caseReliance` for
1677
+ * `verified`, never this mapping directly.
1650
1678
  */
1651
1679
  export const CASE_RELIANCE = {
1652
- verified: "Every pack in this drop is genuine and unaltered and agrees with its " +
1653
- "own signed declaration. Every hand-off with both sides here is " +
1654
- "intact. Every other record is one its holder signed as a step. " +
1655
- "Nothing here could not be checked. This was read offline, on this " +
1656
- "device. It does not say these packs are everything the parties " +
1657
- "recorded.",
1680
+ verified: "Every pack in this drop is genuine, unaltered, and matches its own " +
1681
+ "signed declaration. Every record in it was signed by its holder's " +
1682
+ "key. This was read offline, on this device. It does not say this " +
1683
+ "drop holds everything that was recorded.",
1658
1684
  partial: "Nothing here was proven wrong, and a check could not be completed: a " +
1659
1685
  "pack names no witness, or a pack in a hand-off reading does not say " +
1660
1686
  "what it holds. The exceptions say which.",
@@ -1671,6 +1697,32 @@ export const CASE_RELIANCE = {
1671
1697
  "and some could not be checked. Read each pack's row and each " +
1672
1698
  "exception before relying on any of it.",
1673
1699
  };
1700
+ /**
1701
+ * The clause a `verified` drop that holds a hand-off record gains, inserted
1702
+ * after `CASE_RELIANCE`'s first sentence (5.5.6, A35). Never printed alone,
1703
+ * and never where `handoffRecords` is zero: most drops hold one agent's
1704
+ * pack and no hand-off to say anything about.
1705
+ */
1706
+ export const CASE_RELIANCE_HANDOFF_CLAUSE = "Every hand-off with both sides in this drop is intact.";
1707
+ /**
1708
+ * What a reader may rely on for one case reading (5.5.6 v1.28, A35), byte
1709
+ * for byte the text `case_reliance`'s Python sibling returns.
1710
+ *
1711
+ * For every word but `verified` this is `CASE_RELIANCE` unchanged. For
1712
+ * `verified`, the hand-off clause is inserted after the first sentence
1713
+ * where `handoffRecords` is not zero, and left out where it is zero: the
1714
+ * base sentence never claims a hand-off the drop does not hold.
1715
+ */
1716
+ export function caseReliance(verdict, handoffRecords) {
1717
+ if (verdict === "verified" && handoffRecords > 0) {
1718
+ return ("Every pack in this drop is genuine, unaltered, and matches its own " +
1719
+ "signed declaration. Every hand-off with both sides in this drop is " +
1720
+ "intact. Every record in it was signed by its holder's key. This " +
1721
+ "was read offline, on this device. It does not say this drop holds " +
1722
+ "everything that was recorded.");
1723
+ }
1724
+ return CASE_RELIANCE[verdict];
1725
+ }
1674
1726
  /** The three witness states, and how each is printed (5.5.6, D7). No other. */
1675
1727
  export const WITNESS_STATES = {
1676
1728
  confirmed: "Confirmed on the public record",
@@ -1684,10 +1736,23 @@ export const WITNESS_NOT_LOOKED_UP = Object.freeze({
1684
1736
  services: [],
1685
1737
  });
1686
1738
  /** The witness line as both kernels print it. */
1739
+ /** How a witness time a pack caches is printed where nothing checked it (5.5.6, 5.6.6, v1.25, A30). */
1740
+ export const WITNESS_TIME_CACHED = "the pack dates the witness, not checked";
1741
+ /** How a witness time the public record returned is printed where it matched the cache. */
1742
+ export const WITNESS_TIME_RECORDED = "the public record dates the witness";
1743
+ function witnessTimes(times) {
1744
+ return times.map(([h, t]) => renderWitnessTime(h, t)).join(", ");
1745
+ }
1687
1746
  export function renderWitnessLine(line) {
1688
1747
  let text = `Witness: ${WITNESS_STATES[line.state]}`;
1689
- if (line.reasons.length > 0)
1690
- text += `: ${line.reasons.join("; ")}`;
1748
+ const parts = [...line.reasons];
1749
+ if (line.cached && line.cached.length > 0)
1750
+ parts.push(`${WITNESS_TIME_CACHED}: ${witnessTimes(line.cached)}`);
1751
+ if (line.recorded && line.recorded.length > 0) {
1752
+ parts.push(`${WITNESS_TIME_RECORDED}: ${witnessTimes(line.recorded)}`);
1753
+ }
1754
+ if (parts.length > 0)
1755
+ text += `: ${parts.join("; ")}`;
1691
1756
  if (line.services.length > 0)
1692
1757
  text += ` (answered: ${line.services.join(", ")})`;
1693
1758
  return text;
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { type WitnessLine } from "./case.js";
3
2
  interface Cli {
4
3
  out: (s: string) => void;
5
4
  err: (s: string) => void;
@@ -33,18 +32,23 @@ export declare class CaseInputError extends Error {
33
32
  /**
34
33
  * `bundle.json` bytes that `readBundleBytes` will not read. `tooDeep` separates
35
34
  * the one refusal that is not a parse error; `detail` is the parser's own words,
36
- * which differ by kernel and are printed by `verify` only.
35
+ * which differ by kernel and are printed by `verify` only. `duplicate` marks the
36
+ * one parse refusal whose `detail` is kernel wording, identical in both kernels
37
+ * (`duplicate key "<name>"`, proof#144), so both commands print it.
37
38
  */
38
39
  export declare class BundleReadError extends Error {
39
40
  readonly tooDeep: boolean;
40
41
  readonly detail: string;
41
- constructor(tooDeep: boolean, detail: string);
42
+ readonly duplicate: boolean;
43
+ constructor(tooDeep: boolean, detail: string, duplicate?: boolean);
42
44
  }
45
+ export declare const NOT_I_JSON = "is not I-JSON (RFC 7493)";
43
46
  /**
44
47
  * How both `verify` and `case` turn `bundle.json` bytes into JSON, in both
45
48
  * kernels (`read_bundle_bytes` in the Python twin). UTF-8 with each ill-formed
46
49
  * sequence read as U+FFFD, and a byte-order mark kept; then nesting deeper than
47
- * MAX_JSON_DEPTH is refused before any parser walks into it; then `JSON.parse`.
50
+ * MAX_JSON_DEPTH is refused before any parser walks into it; then `JSON.parse`,
51
+ * refusing an object that repeats a member name (I-JSON, RFC 7493; proof#144).
48
52
  * Throws `BundleReadError`.
49
53
  */
50
54
  export declare function readBundleBytes(bytes: Uint8Array): unknown;
@@ -100,29 +104,6 @@ export declare function readFileEntry(entry: TreeEntry, key: string): Promise<Ui
100
104
  */
101
105
  export declare function treeFromArguments(paths: readonly string[]): Promise<Map<string, TreeEntry>>;
102
106
  export declare const CASE_USAGE: string;
103
- interface OnChainRow {
104
- ref: string;
105
- witnesses: number;
106
- confirmed: number;
107
- mismatched: number;
108
- unreachable: number;
109
- not_checked: number;
110
- }
111
- interface OnChainHalf {
112
- /** Present only where a lookup ran: each gateway that answered, in the order given. */
113
- answered?: string[];
114
- checked: boolean;
115
- gateways: string[];
116
- packs: OnChainRow[];
117
- verdict: "not_checked" | "confirmed" | "failed" | "undetermined";
118
- }
119
- /**
120
- * The witness line (evidence-bundle 5.5.6 v1.20, A24, D7) from the on-chain
121
- * half: `confirmed` is `confirmed`, `not_checked` and `undetermined` are
122
- * `not_checked` with the reason, and `failed` is `differs`. It never moves the
123
- * headline.
124
- */
125
- export declare function witnessLine(onChain: OnChainHalf): WitnessLine;
126
107
  /**
127
108
  * The case verdict's code, moved only by an on-chain half that was asked for:
128
109
  * a witness that does not match is a failure whatever the reading said, and an