verifyhash 0.1.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.
Files changed (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +883 -0
  3. package/cli/abi/ContributionRegistry.json +881 -0
  4. package/cli/agent.js +2173 -0
  5. package/cli/anchor-artifact.js +853 -0
  6. package/cli/anchor.js +400 -0
  7. package/cli/claim.js +881 -0
  8. package/cli/core/agent-commit.js +448 -0
  9. package/cli/core/agent-session.js +598 -0
  10. package/cli/core/anchor-binding.js +663 -0
  11. package/cli/core/attestation.js +580 -0
  12. package/cli/core/evidence-plans.js +495 -0
  13. package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
  14. package/cli/core/fulfill-intake.js +1082 -0
  15. package/cli/core/go-live-preflight.js +481 -0
  16. package/cli/core/license.js +534 -0
  17. package/cli/core/manifest.js +243 -0
  18. package/cli/core/packetseal.js +591 -0
  19. package/cli/core/registryArtifact.js +49 -0
  20. package/cli/core/revocation.js +539 -0
  21. package/cli/core/rfc3161.js +389 -0
  22. package/cli/core/timestamp.js +482 -0
  23. package/cli/core/trust-asof.js +479 -0
  24. package/cli/dataset.js +2950 -0
  25. package/cli/evidence.js +2227 -0
  26. package/cli/fulfill-webhook-http.js +438 -0
  27. package/cli/git.js +220 -0
  28. package/cli/hash.js +550 -0
  29. package/cli/identity.js +1072 -0
  30. package/cli/journal-cli.js +1110 -0
  31. package/cli/journal-log.js +454 -0
  32. package/cli/journal.js +334 -0
  33. package/cli/lineage.js +447 -0
  34. package/cli/list.js +287 -0
  35. package/cli/parcel.js +1509 -0
  36. package/cli/proof.js +578 -0
  37. package/cli/prove.js +300 -0
  38. package/cli/receipt.js +631 -0
  39. package/cli/registry.js +331 -0
  40. package/cli/reputation.js +344 -0
  41. package/cli/revocation.js +495 -0
  42. package/cli/serve-verify-http.js +298 -0
  43. package/cli/serve-verify.js +333 -0
  44. package/cli/show.js +339 -0
  45. package/cli/verify.js +383 -0
  46. package/cli/vh.js +3927 -0
  47. package/docs/ADOPT.md +183 -0
  48. package/docs/ADOPTION.json +11 -0
  49. package/docs/AGENTTRACE.md +247 -0
  50. package/docs/ANCHORING.md +167 -0
  51. package/docs/AUDIT.md +55 -0
  52. package/docs/CONFORMANCE.md +107 -0
  53. package/docs/DATALEDGER.md +638 -0
  54. package/docs/DECIDE.md +47 -0
  55. package/docs/DECISIONS-PENDING.md +27 -0
  56. package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
  57. package/docs/ENGINE-LEDGER.json +12 -0
  58. package/docs/EVIDENCE.md +519 -0
  59. package/docs/GO-LIVE.md +66 -0
  60. package/docs/IDENTITY.md +123 -0
  61. package/docs/INDEPENDENT-VERIFICATION.md +377 -0
  62. package/docs/INTEGRITY-JOURNAL.md +337 -0
  63. package/docs/KEY-LIFECYCLE.md +179 -0
  64. package/docs/LICENSING.md +46 -0
  65. package/docs/LINEAGE.md +307 -0
  66. package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
  67. package/docs/LOOP-HARDENING-PLAN.md +44 -0
  68. package/docs/MERKLE-LEAVES.md +113 -0
  69. package/docs/METRICS.jsonl +31 -0
  70. package/docs/MORNING.md +204 -0
  71. package/docs/PILOT.md +444 -0
  72. package/docs/PROOFPARCEL.md +227 -0
  73. package/docs/PROOFS.md +262 -0
  74. package/docs/RECEIPTS.md +341 -0
  75. package/docs/REPUTATION.md +158 -0
  76. package/docs/SDK.md +301 -0
  77. package/docs/STRATEGY-ARCHIVE.md +5055 -0
  78. package/docs/SUPERVISOR-RUNBOOK.md +52 -0
  79. package/docs/TRUST-BOUNDARIES.md +335 -0
  80. package/docs/TRUSTLEDGER.md +1976 -0
  81. package/docs/USAGE-BUDGET.json +121 -0
  82. package/docs/VERIFY-SERVICE.md +168 -0
  83. package/index.js +160 -0
  84. package/package.json +41 -0
  85. package/trustledger/build-standalone.js +796 -0
  86. package/trustledger/cli.js +3179 -0
  87. package/trustledger/close.js +391 -0
  88. package/trustledger/corpus.js +159 -0
  89. package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
  90. package/trustledger/dist/trustledger-standalone.html +6197 -0
  91. package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
  92. package/trustledger/door-core.js +442 -0
  93. package/trustledger/fixtures/bank.csv +7 -0
  94. package/trustledger/fixtures/bank.malformed.csv +3 -0
  95. package/trustledger/fixtures/bank.noalias.csv +5 -0
  96. package/trustledger/fixtures/bank.ofx +34 -0
  97. package/trustledger/fixtures/bank.real.csv +5 -0
  98. package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
  99. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
  100. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
  101. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
  102. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
  103. package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
  104. package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
  105. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
  106. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
  107. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
  108. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
  109. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
  110. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
  111. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
  112. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
  113. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
  114. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
  115. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
  116. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
  117. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
  118. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
  119. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
  120. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
  121. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
  122. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
  123. package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
  124. package/trustledger/fixtures/e2e/bank.csv +4 -0
  125. package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
  126. package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
  127. package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
  128. package/trustledger/fixtures/e2e/rentroll.csv +6 -0
  129. package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
  130. package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
  131. package/trustledger/fixtures/plans/baseline.json +25 -0
  132. package/trustledger/fixtures/plans/price-binding.example.json +27 -0
  133. package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
  134. package/trustledger/fixtures/policy/baseline.json +19 -0
  135. package/trustledger/fixtures/policy/ca-example.json +12 -0
  136. package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
  137. package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
  138. package/trustledger/fixtures/quickbooks.csv +7 -0
  139. package/trustledger/fixtures/quickbooks.real.csv +5 -0
  140. package/trustledger/fixtures/rentroll.csv +6 -0
  141. package/trustledger/fixtures/rentroll.real.csv +4 -0
  142. package/trustledger/ingest.js +1163 -0
  143. package/trustledger/lib/policy-bundled-loader.js +44 -0
  144. package/trustledger/lib/sha256-vendored.js +227 -0
  145. package/trustledger/license.js +563 -0
  146. package/trustledger/match.js +551 -0
  147. package/trustledger/plans.js +551 -0
  148. package/trustledger/policy.js +398 -0
  149. package/trustledger/public/index.html +512 -0
  150. package/trustledger/reconcile.js +1486 -0
  151. package/trustledger/report.js +887 -0
  152. package/trustledger/seal.js +854 -0
  153. package/trustledger/server.js +391 -0
  154. package/trustledger/valueproof.js +350 -0
@@ -0,0 +1,341 @@
1
+ # verifyhash receipts — schema, lifecycle, and diff semantics
2
+
3
+ This is the canonical spec for the on-disk **receipt** artifacts that `cli/receipt.js` reads and
4
+ writes (tasks **T-6.1** and **T-6.2**). A receipt is a versioned, strictly-validated JSON file that
5
+ makes two CLI flows durable and operable:
6
+
7
+ - a **claim receipt** (`kind: "verifyhash.claim-receipt"`) persists everything `reveal()` needs so a
8
+ crashed/interrupted commit-reveal claim can be **resumed** from a fresh process (T-6.1);
9
+ - an **anchor receipt** (`kind: "verifyhash.anchor-receipt"`) records the per-file **manifest** of a
10
+ directory so a later `vh verify <dir> --receipt <p>` can **localize** which file diverged (T-6.2).
11
+
12
+ > **Trust posture (read this first).** A receipt is an **UNTRUSTED local convenience**, exactly as
13
+ > stated in [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md). The authoritative result always comes
14
+ > from the on-chain record — for `verify`, from re-deriving the Merkle root and comparing it to that
15
+ > record. A receipt's `manifest` only **localizes** which file diverged; it can never, by itself, make
16
+ > content "verified". Rule of thumb: **the on-chain root decides MATCH/MISMATCH; the receipt manifest
17
+ > only points at the file.** The one field that is operationally load-bearing is the claim receipt's
18
+ > secret `salt` — see [Trust vs hints](#trust-vs-hints).
19
+
20
+ The receipt is **never** consumed by the contract and is **never** uploaded anywhere. **A claim receipt
21
+ holds the SECRET `salt`**, so where it is written is always something you opt into — `vh` never silently
22
+ drops it into your repo unless you ask:
23
+
24
+ - the durable **`vh commit`** writes a claim receipt to `--receipt <path>` (exact file) or
25
+ `--receipt-dir <dir>` (into that folder under a tidy default file name), or — with neither — defaults
26
+ to `<cwd>/<contentHashPrefix>.vhclaim.json` **and prints the EXACT absolute path it wrote**
27
+ (`receipt written: <abs path>`), so you can always see, move, or delete it. It is never written
28
+ somewhere you can't find;
29
+ - the one-shot **`vh claim`** persists a receipt **only if you pass `--receipt`/`--receipt-dir`**; with
30
+ neither it writes **nothing** (the in-memory receipt is just returned to the caller). Use `vh commit`
31
+ for a durable, resumable claim;
32
+ - the pure helper `defaultReceiptPath(contentHash)` only computes a **relative** file name
33
+ (`./<prefix>.vhclaim.json`); the caller is responsible for resolving it against a safe base.
34
+
35
+ Receipt files are also git-ignored (`*.vhclaim.json`). **Keep a claim receipt private until you reveal:**
36
+ anyone holding the `salt` before reveal could front-run the open (after a successful reveal the
37
+ commitment is single-use and spent, so the receipt is no longer sensitive). This reuses the trust posture
38
+ in [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md): the receipt is an **untrusted local convenience**;
39
+ the authoritative result always comes from the on-chain record.
40
+
41
+ ---
42
+
43
+ ## Schema
44
+
45
+ Two `kind`s share a common header. `readReceipt` validates **strictly** and throws on ANY deviation
46
+ rather than filling defaults: a partial claim receipt could make you re-derive a wrong commitment or
47
+ reveal with the wrong salt and waste (or burn) a transaction, so a corrupt receipt is rejected outright,
48
+ never silently half-accepted.
49
+
50
+ ### Common header (every receipt)
51
+
52
+ | Field | Type | Required | Trust | Meaning |
53
+ |-------|------|----------|-------|---------|
54
+ | `kind` | string | yes | structural | `"verifyhash.claim-receipt"` or `"verifyhash.anchor-receipt"`. A discriminator so a random JSON file is never mistaken for a receipt. |
55
+ | `schemaVersion` | integer | yes | structural | On-disk schema version. This build **writes** `4` and **reads** `1`, `2`, `3`, or `4`. Any other version is rejected, so a future/foreign file is never misread. (`1` → base, `2` added the optional `manifest`, `3` added the optional `git` block, `4` added the optional `parent` on a CLAIM receipt — all additive.) |
56
+ | `contentHash` | `0x`+64 hex (32 bytes) | yes | **trusted-as-target** | The digest being claimed/anchored: a file's `keccak256`, or a directory's Merkle **root** (see [`docs/MERKLE-LEAVES.md`](MERKLE-LEAVES.md)). This is the only thing the chain attests to; everything else is metadata. |
57
+ | `contractAddress` | `0x`+40 hex (address) | yes | hint | The `ContributionRegistry` the receipt is about. Used to target the right contract on resume. |
58
+ | `chainId` | non-negative integer | yes | hint | Chain the commit/anchor was sent to (e.g. `31337` local, `80002` Amoy). |
59
+ | `uri` | string | yes (may be `""`) | **UNTRUSTED hint** | Off-chain pointer (IPFS CID, commit URL, …). The contract **never fetches, validates, or hashes it**; consumers must re-fetch + re-hash and compare to `contentHash`. Defaulted to `""`, never `undefined`. |
60
+ | `path` | string | optional | informational | The source path that was hashed. For humans only. |
61
+ | `targetKind` | `"file"` \| `"dir"` | optional | informational | Whether the target was a single file or a directory. |
62
+ | `manifest` | array | optional (v2+) | **UNTRUSTED hint** | Per-file breakdown of a directory target; see [Manifest](#the-manifest-directory-targets). A v1 receipt that carries a manifest is rejected (the version must not lie). |
63
+ | `git` | object | optional (v3+) | **UNTRUSTED hint** | Git provenance `{ commit, scope }` recorded by a `--git` anchor/claim; see [Git provenance](#the-git-provenance-block-git-scoped-targets). A v1/v2 receipt that carries a git block is rejected (the version must not lie). |
64
+
65
+ ### Claim receipt — additional fields (`kind: "verifyhash.claim-receipt"`)
66
+
67
+ A claim receipt carries the **secret material** that lets a separate process finish a commit-reveal claim.
68
+
69
+ | Field | Type | Required | Trust | Meaning |
70
+ |-------|------|----------|-------|---------|
71
+ | `salt` | `0x`+64 hex (32 bytes) | yes | **SECRET — keep private** | The blinding salt bound into the commitment. `reveal()` needs this exact value; lose it and the claim is **unrevealable by anyone**. |
72
+ | `commitment` | `0x`+64 hex (32 bytes) | yes | trusted-as-derived | `keccak256(abi.encode(contentHash, committer, salt))` — the blinded value that went on-chain in `commit()`. |
73
+ | `committer` | `0x`+40 hex (address) | yes | trusted-as-target | The address that committed and is the only one that can reveal (it is hashed into the commitment). |
74
+ | `commitTxHash` | `0x`+64 hex (32 bytes) | optional | informational | The `commit()` transaction hash. |
75
+ | `commitBlockNumber` | non-negative integer | optional | operational | Block the commit mined in; used to compute when the reveal window matures. |
76
+ | `minRevealDelay` | non-negative integer | optional | operational | `MIN_REVEAL_DELAY` read from the contract at commit time; how many blocks must pass before `reveal()`. |
77
+ | `parent` | `0x`+64 hex (32 bytes) | optional (v4+) | **UNTRUSTED hint** | The lineage edge (B-10.1): an **already-anchored** predecessor's `contentHash`, recorded by `vh commit --parent`. Present only for a revision; **omitted entirely** for a lineage root (the all-zero hash is rejected, never recorded). On resume, `vh reveal` routes to `revealWithParent(contentHash, salt, uri, parent)` and records the edge; the **authoritative** edge is what that on-chain call records, not this field. It is a *claim* of a predecessor — never proof of content ancestry or any transfer of the parent's authorship. **Back-compat is total:** this field is purely additive, so a v1/v2/v3 receipt simply has no `parent` and is read **unchanged** (and reveals via the legacy `reveal`, as a lineage root). Rejected on an anchor receipt, on a receipt below v4, when malformed/zero, or when equal to `contentHash` (`SelfParent`). |
78
+
79
+ ### Anchor receipt — additional fields (`kind: "verifyhash.anchor-receipt"`)
80
+
81
+ An anchor receipt has **no secret material at all** (anchoring needs none). Its only reason to exist
82
+ beyond the header is the optional directory `manifest`.
83
+
84
+ | Field | Type | Required | Trust | Meaning |
85
+ |-------|------|----------|-------|---------|
86
+ | `anchorTxHash` | `0x`+64 hex (32 bytes) | optional | informational | The `anchor()` transaction hash, when one was sent. |
87
+ | `anchorBlockNumber` | non-negative integer | optional | informational | Block the anchor mined in. |
88
+
89
+ > An anchor receipt deliberately has **no `salt`, `commitment`, or `committer`** — there is no secret to
90
+ > protect and no signer needed to verify a hash you already know. `readReceipt` rejects an anchor receipt
91
+ > that smuggles those in, and a claim receipt missing any of them.
92
+
93
+ ### The manifest (directory targets)
94
+
95
+ For a directory target the receipt may carry a `manifest` (schemaVersion ≥ 2): the **sorted list of
96
+ every file's `{ path, contentHash, leaf }`** — exactly what `vh hash <dir>` / `hashDir()` computes and
97
+ then would otherwise discard. Each entry:
98
+
99
+ | Field | Type | Meaning |
100
+ |-------|------|---------|
101
+ | `path` | non-empty string | the file's POSIX relative path inside the directory |
102
+ | `contentHash` | `0x`+64 hex | `keccak256` of the file's bytes (the bare content digest `c` in [`docs/MERKLE-LEAVES.md`](MERKLE-LEAVES.md)) |
103
+ | `leaf` | `0x`+64 hex | the **path-bound** leaf `keccak256(DIR_LEAF_DOMAIN ‖ relPath ‖ 0x00 ‖ c)`, which is what the tree is actually built from |
104
+
105
+ The manifest is stored **sorted ascending by `leaf` value** (the same total order `hashDir` uses to
106
+ build the tree), so a written manifest is deterministic regardless of input enumeration order. Because
107
+ the `leaf` binds the path, two files at different paths can never collide, and a leaf change with the
108
+ same path is unambiguously a **content** change.
109
+
110
+ ### The git provenance block (`--git`-scoped targets)
111
+
112
+ When a directory is anchored/claimed with **`--git`** (T-8.2), the root and `manifest` are computed over
113
+ **exactly the files git tracks** at a commit — the same reproducible, untracked-junk-ignoring enumeration
114
+ as `vh hash <dir> --git` (see [`docs/MERKLE-LEAVES.md`](MERKLE-LEAVES.md)). The receipt then records a
115
+ `git` block (schemaVersion ≥ 3):
116
+
117
+ | Field | Type | Meaning |
118
+ |-------|------|---------|
119
+ | `commit` | 40-hex (bare, no `0x`) | the resolved commit object id the tracked set was enumerated from (paste straight into `git show <oid>`) |
120
+ | `scope` | non-empty string | the repo-relative POSIX path the operator pointed `vh` at (`"."` for the repo root) — *how* the tracked set was scoped |
121
+
122
+ The `git` block is an **UNTRUSTED convenience hint**, consistent with
123
+ [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md): it records *how* a root was produced so a reader can
124
+ reproduce the `--git --ref <oid>` enumeration, but the authoritative verdict is still the **recomputed
125
+ root vs the on-chain record**. A receipt's `git.commit` is never re-checked against the chain — the chain
126
+ attests only to `contentHash`. `vh verify <dir> --git [--ref <ref>]` re-derives the root over the tracked
127
+ set at that ref and reports MATCH/MISMATCH; with `--receipt` it localizes ADDED/REMOVED/CHANGED over the
128
+ tracked set. Untracked junk in the work tree never affects the verdict.
129
+
130
+ ### Trust vs hints
131
+
132
+ Summarizing the columns above, in the same spirit as the [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md)
133
+ one-liner table:
134
+
135
+ | Field(s) | Trust it for | Do NOT trust it for |
136
+ |----------|--------------|---------------------|
137
+ | `contentHash` | the exact digest/root the on-chain record is keyed by | being "valid" without the on-chain lookup + (for dirs) a recomputed root |
138
+ | `salt` (claim only) | finishing **your** reveal — **keep it secret until revealed** | sharing; anyone with it before reveal could front-run the open (and after a successful reveal it is spent and harmless) |
139
+ | `commitment`, `committer` | knowing who can reveal and what was committed | proving authorship by themselves — that comes from the on-chain `Record.authorBound` |
140
+ | `uri` | a human hint of where the content might be | anything security-relevant — re-fetch + re-hash |
141
+ | `manifest` | **localizing** which file diverged (ADDED/REMOVED/CHANGED) | deciding MATCH/MISMATCH — the recomputed root vs the on-chain record decides that |
142
+ | `git` (`{ commit, scope }`) | reproducing *how* a `--git` root was enumerated (which commit/scope) | proving the root — it is never re-checked against the chain; the chain attests only to `contentHash` |
143
+ | `*TxHash`, `*BlockNumber`, `path`, `targetKind` | operational convenience (resume timing, display) | any security claim |
144
+
145
+ ---
146
+
147
+ ## Commit → reveal resume lifecycle (claim receipts, T-6.1)
148
+
149
+ The front-running-resistant claim (`vh claim`) is a **two-transaction** commit-reveal flow separated by
150
+ a maturation window of `MIN_REVEAL_DELAY` blocks. The commitment is
151
+ `keccak256(abi.encode(contentHash, committer, salt))`; only that opaque hash goes on-chain first, so a
152
+ mempool watcher cannot copy your content hash. After the window the committer reveals `(contentHash,
153
+ salt)`; an attacker who replays the revealed values as themselves recomputes a **different** commitment
154
+ they never registered, so their reveal reverts with `NoSuchCommitment` and `contributor` stays the
155
+ original committer (full threat model: [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md)).
156
+
157
+ **The durability problem (why the receipt exists).** On a live testnet the maturation window is minutes.
158
+ The single-process `vh claim` holds the secret `salt` only in memory while it waits. If that process
159
+ crashes or is interrupted between the two legs, the salt is lost — and since `reveal()` needs that exact
160
+ salt, the `contentHash` becomes **committed-but-unrevealable by anyone**, permanently burning the
161
+ attribution. The receipt fixes this by persisting the salt (and everything `reveal()` needs) to disk
162
+ **before** the commit step returns, so a separate process can finish later.
163
+
164
+ ### The split: `vh commit` then `vh reveal`
165
+
166
+ ```
167
+ vh commit ./src --uri ipfs://cid # sends commit(), writes the receipt, PRINTS its exact path, exits
168
+ # ...wait out MIN_REVEAL_DELAY (a few blocks)...
169
+ vh reveal --receipt <that exact path> # resumes from the receipt and reveals
170
+ ```
171
+
172
+ 1. **`vh commit <path>`** (`runCommit`) hashes the target, derives `(salt, commitment)`, sends
173
+ `commit(commitment)`, reads `MIN_REVEAL_DELAY`, then **writes the claim receipt before it returns**.
174
+ For a directory it also records the `manifest`. The receipt destination is **always opted into**:
175
+ `--receipt <path>` (exact file), `--receipt-dir <dir>` (default name inside that folder), or — with
176
+ neither — `<cwd>/<contentHashPrefix>.vhclaim.json`; in every case the success line names the **exact
177
+ absolute path written** (`receipt written: <abs path>`), so the secret-bearing file is never dropped
178
+ silently. From this point on, a crash is survivable: the salt is durable.
179
+ 2. **Wait** out `MIN_REVEAL_DELAY` blocks. The commit-block height and the delay are in the receipt, so
180
+ any process can compute when the window matures.
181
+ 3. **`vh reveal --receipt <p>`** (`runReveal`) `readReceipt`s the file (strict — a corrupt receipt throws
182
+ here rather than producing a wrong reveal), checks the signer **is** the receipt's `committer` (else
183
+ the reveal would hit `NoSuchCommitment`; it fails fast with a clear message instead), waits out the
184
+ window, then sends `reveal(contentHash, salt, uri)` — **or, if the receipt carries a `parent` (v4),
185
+ `revealWithParent(contentHash, salt, uri, parent)`** so the resumed reveal records the lineage edge
186
+ (B-10.1). This needs **no** information that was not durably written at commit time, so it works from a
187
+ completely fresh process — even after a reboot.
188
+
189
+ **Resumable lineage edge (`vh commit --parent`, schema v4).** `vh commit --parent <hash>` validates the
190
+ parent's *shape* locally and persists it into the claim receipt (the additive v4 `parent` field above);
191
+ `commit()` itself never carries a parent (it encodes only the opaque commitment). The edge is therefore
192
+ checked **on-chain at reveal time, not at commit time**: a separate `vh reveal --receipt <p>` reads
193
+ `parent` back and routes to `revealWithParent`. If the parent is stale (never anchored, or anchored only
194
+ after the commit) the **reveal** reverts `UnknownParent` while the **commit stays valid** — so the receipt
195
+ is left untouched and reusable: anchor the missing parent and re-run the same `vh reveal --receipt <p>`,
196
+ with no lost salt. A receipt with **no** `parent` reveals exactly as before (legacy `reveal`, a lineage
197
+ root). See [`docs/LINEAGE.md`](LINEAGE.md) for the full write/read flow.
198
+
199
+ **Retry semantics.** If you reveal before the window matures the contract reverts with `RevealTooSoon`;
200
+ `runReveal` lets that propagate and **leaves the receipt file untouched**, so you simply retry later. The
201
+ receipt is also unaffected by an unrelated crash, so resume is idempotent up to the single successful
202
+ reveal.
203
+
204
+ **`vh claim` is still the one-shot convenience** (commit + reveal in one process). To keep it safe by
205
+ default it persists a receipt **only if you ask** — pass `--receipt <path>` or `--receipt-dir <dir>` and
206
+ it writes the secret-bearing receipt to that exact, named location (so even the one-shot path is then
207
+ crash-recoverable: resume with `vh reveal --receipt <p>`). With **neither**, `vh claim` writes
208
+ **nothing** to disk (the in-memory `runClaim` helper just returns the receipt object); use `vh commit`
209
+ for a durable, resumable claim rather than relying on a silent cwd drop.
210
+
211
+ > **Keep the receipt private until you reveal:** it contains the secret `salt`. After a successful reveal
212
+ > the commitment is single-use and spent, so the receipt is no longer sensitive.
213
+
214
+ ---
215
+
216
+ ## Directory-manifest diff semantics (anchor receipts, T-6.2)
217
+
218
+ A one-shot `vh anchor <dir>` records only the Merkle **root** on-chain. So plain `vh verify <dir>` can
219
+ only ever say "the whole tree's root matches / does not match" — it cannot say WHICH file diverged.
220
+ `vh anchor <dir> --receipt <p>` records the directory's `manifest` (every `{ path, contentHash, leaf }`),
221
+ and `vh verify <dir> --receipt <p>` then prints a precise per-file diff.
222
+
223
+ ### How the diff is computed (`diffManifest`)
224
+
225
+ `diffManifest(recordedManifest, currentLeaves)` is a **pure** localizer. It keys both sides by `path` and
226
+ compares the path-bound `leaf`:
227
+
228
+ - **ADDED** — a path present in the current tree but not in the receipt's manifest.
229
+ - **REMOVED** — a path in the receipt's manifest, gone from the current tree.
230
+ - **CHANGED** — same `path`, different `leaf`. Because the path is bound into the leaf, an identical key
231
+ with a different leaf is unambiguously a **content** change; the diff reports `oldContentHash` → `newContentHash`.
232
+ - **unchanged** — same `path`, same `leaf`.
233
+ - `identical: true` iff there are zero added, removed, or changed entries.
234
+
235
+ ### What decides the verdict (and what does not)
236
+
237
+ The diff **does not** decide MATCH/MISMATCH. The authoritative verdict is the same re-derive-and-compare
238
+ check the trust model requires: `vh verify` recomputes the directory's Merkle **root** from the files on
239
+ disk and compares that root to the on-chain record. **MATCH/MISMATCH comes only from that comparison.**
240
+ The manifest never participates in the verdict.
241
+
242
+ A malicious or stale receipt can at worst mislabel which file moved, and even that is caught: `verify`
243
+ flags a receipt whose recorded root does not match the recomputed root (`receiptHashMismatch`) and reports
244
+ it as a **different directory snapshot** rather than silently pretending the files line up. The verify
245
+ output leads with the caveat:
246
+
247
+ ```
248
+ --- receipt manifest diff (UNTRUSTED hint) ---
249
+ NOTE: the receipt is an untrusted convenience. The authoritative verdict is the
250
+ MATCH/MISMATCH above (recomputed root vs the on-chain record). This diff only localizes
251
+ WHICH file diverged; it cannot make content valid or invalid on its own.
252
+ ```
253
+
254
+ If you pass `--receipt` for a **file** target (not a directory), the manifest diff is simply ignored with
255
+ a note — there are no per-file leaves to localize.
256
+
257
+ ---
258
+
259
+ ## Worked example
260
+
261
+ ### A. Resumable claim (claim receipt)
262
+
263
+ ```
264
+ $ vh commit ./src --uri ipfs://bafy... # step 1
265
+ commit: committing ./src (file) as 0x7099...79C8...
266
+ commit tx: 0x...
267
+ receipt written: /work/0c271a48a26d075d.vhclaim.json
268
+ KEEP THIS PRIVATE — it holds the secret salt. Resume with: vh reveal --receipt /work/0c271a48a26d075d.vhclaim.json
269
+ ```
270
+
271
+ The `receipt written:` line names the **exact absolute path** so you can see, relocate, or delete the
272
+ secret-bearing file. Choose where it goes with `--receipt <path>` or `--receipt-dir <dir>`.
273
+
274
+ The receipt on disk (a real v1 claim receipt; v2 adds an optional `manifest` for a directory target):
275
+
276
+ ```json
277
+ {
278
+ "kind": "verifyhash.claim-receipt",
279
+ "schemaVersion": 1,
280
+ "contentHash": "0x0c271a48a26d075dabf24d6d9474fe3dde105ed15d05638972142c1c5a2a02b5",
281
+ "committer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
282
+ "salt": "0x59b7ae3c7fba3c5420517d1897b82bbd17d6d568aa303ebf3dbd5347cf6df1b6",
283
+ "commitment": "0x0cf4ffdbdc94d8fab10bceb57af65c02c9daa109cc080c54d49ca7481271dd43",
284
+ "contractAddress": "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A",
285
+ "chainId": 31337,
286
+ "uri": "ipfs://cid-alice",
287
+ "path": "/work/src",
288
+ "targetKind": "file",
289
+ "commitTxHash": "0x7470198b01e39a7e11270e826f6305cd15fc1ee34f6d28ee11d80c1f38e50831",
290
+ "commitBlockNumber": 649,
291
+ "minRevealDelay": 1
292
+ }
293
+ ```
294
+
295
+ Later — even after a reboot, from a fresh process:
296
+
297
+ ```
298
+ $ vh reveal --receipt ./0c271a48a26d075d.vhclaim.json # step 2
299
+ reveal: revealing 0x0c271a48...02b5 as 0x7099...79C8...
300
+ Claimed (authorBound) at index 3 by 0x7099...79C8 in tx 0x...
301
+ ```
302
+
303
+ ### B. Localized directory verify (anchor receipt + manifest)
304
+
305
+ After `vh anchor ./repo --receipt ./repo.vhclaim.json`, the receipt records the manifest. If `src/b.js`
306
+ is later edited and `src/new.js` is added, `vh verify ./repo --receipt ./repo.vhclaim.json` prints:
307
+
308
+ ```
309
+ MISMATCH: recomputed root is NOT the anchored record.
310
+ ...
311
+ --- receipt manifest diff (UNTRUSTED hint) ---
312
+ NOTE: the receipt is an untrusted convenience. The authoritative verdict is the
313
+ MATCH/MISMATCH above (recomputed root vs the on-chain record). This diff only localizes
314
+ WHICH file diverged; it cannot make content valid or invalid on its own.
315
+ files: 1 CHANGED, 1 ADDED, 0 REMOVED (1 unchanged)
316
+ CHANGED src/b.js
317
+ old: 0x0202...0202
318
+ new: 0x0909...0909
319
+ ADDED src/new.js (0x0303...0303) present now, not in the receipt
320
+ ```
321
+
322
+ The `MISMATCH` is decided by the recomputed root vs the on-chain record; the diff only tells you it was
323
+ `src/b.js` (and the new file) that moved the root.
324
+
325
+ ---
326
+
327
+ ## Tests
328
+
329
+ - `test/cli.receipt.test.js` round-trips both receipt kinds, proves strict validation (rejects wrong
330
+ version/kind, missing/malformed fields, a v1 receipt smuggling a v2 manifest), and exercises
331
+ `diffManifest`'s ADDED/REMOVED/CHANGED localization.
332
+ - `test/cli.claim.test.js` covers the `commit`/`reveal` split end-to-end against a live node, including a
333
+ resume-from-a-fresh-process path and the front-run-resistance proof.
334
+ - `test/cli.verify.test.js` covers `vh verify <dir> --receipt` localization and the `receiptHashMismatch`
335
+ caveat.
336
+ - `test/cli.receipt.docs.test.js` is a docs-rot guard: it asserts this file and the README keep the
337
+ schema, the resume lifecycle, and the untrusted/localizes-not-decides caveats in sync with the code.
338
+
339
+
340
+ ---
341
+ <sub>© 2026 verifyhash.com · Licensed under Apache-2.0 (SPDX-License-Identifier: Apache-2.0) — see the [LICENSE](https://verifyhash.com/LICENSE) and [NOTICE](https://verifyhash.com/NOTICE) served with this file.</sub>
@@ -0,0 +1,158 @@
1
+ # verifyhash contribution score — what it is, what it does and does NOT prove
2
+
3
+ This is the canonical spec for the **contribution score** surfaced by `vh reputation <addr>` (task
4
+ **T-12.2**) over the bounded per-contributor on-chain index (task **T-12.1**). It is **pure
5
+ documentation** of behaviour that already ships; no new runtime behaviour is introduced here.
6
+
7
+ The single sentence to keep in mind before everything below:
8
+
9
+ > **The score is a NON-TRANSFERABLE DERIVED VIEW over records that already exist on-chain. It is
10
+ > re-derivable by anyone from the same registry. It is NOT a token, holds no value, grants no rights,
11
+ > and is only as meaningful as the `authorBound` bar.**
12
+
13
+ It exists to answer one question — *"who are the real contributors, and how much have they verifiably
14
+ contributed?"* — without inventing any new on-chain object. It groups the registry's existing,
15
+ immutable records by address and counts them, separating the strong signal (commit-reveal,
16
+ front-running-resistant claims) from the weak one (front-runnable plain anchors).
17
+
18
+ > **Read [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md) first.** Every caveat here is the same
19
+ > caveat the record fields already carry; this doc reuses that wording verbatim so the boundaries stay
20
+ > consistent. If the two ever drift, TRUST-BOUNDARIES (and the contract NatSpec it mirrors) is
21
+ > authoritative.
22
+
23
+ ---
24
+
25
+ ## EXACT definition — which on-chain read it aggregates
26
+
27
+ The score is computed entirely off-chain from **a single ownerless `view` read** on
28
+ `contracts/ContributionRegistry.sol` (the T-12.1 per-contributor index): the paged
29
+ `getRecordsByContributor` walk. It reads **nothing else** and re-derives everything — there is no
30
+ stored "score" on-chain.
31
+
32
+ | Read | What it returns | Used for |
33
+ |------|-----------------|----------|
34
+ | `getRecordsByContributor(addr, start, count)` | a clamped, forgiving page of `{ contentHash, Record }` for `addr`'s own records, in insertion order | **the only read the command issues** — `total` and every breakdown below are derived from the records this walk returns |
35
+ | `contributorRecordCount(addr)` | how many records carry `addr` (0 for an unknown address) | **companion read, NOT issued by `vh reputation`.** The T-12.1 O(1) count an external consumer can call to get the same `total` *without* paging; it equals the CLI's `total` because both count the same records |
36
+
37
+ `getRecordsByContributor` is paged in fixed-size chunks (`cli/reputation.js` walks pages of
38
+ `DEFAULT_PAGE = 100`, stopping on a **short/empty page** — that short-page stop, not any count read, is
39
+ the page-walk's bound). Because the contract clamps an out-of-range window to empty (it **never** reverts
40
+ on a tail), enumerating one address is **O(that address's own records), never O(total)** — that is the
41
+ whole point of the T-12.1 index.
42
+
43
+ So `vh reputation` makes exactly **one read shape** (`getRecordsByContributor`), not two.
44
+ `contributorRecordCount` is the matching O(1) count an indexer/UI may call independently; the command
45
+ itself never calls it (`computeScore` sets `total = records.length` from the walked page set).
46
+
47
+ From that page-walk, `computeScore` (pure, no I/O, fully re-derivable from the same input) produces:
48
+
49
+ - **`total`** — `records.length` from the page-walk: the number of records the walk returned for the
50
+ address (which equals what `contributorRecordCount` would return, since both count the same records).
51
+ - **the attribution breakdown — reported SEPARATELY, never summed into one number:**
52
+ - **`authorBound`** — records with `record.authorBound == true`: written via the commit-reveal path
53
+ (`vh claim` / `vh commit`+`vh reveal`). The **proven first claimant** — front-running-resistant.
54
+ - **`anchorOnly`** — records with `record.authorBound == false`: written via the one-shot `anchor()`
55
+ path (`vh anchor`). The **first anchorer only — NOT authorship** (see anti-sybil below).
56
+ - **the lineage breakdown** (orthogonal to attribution; uses `record.parent`):
57
+ - **`lineageRoots`** — records whose `parent == bytes32(0)` (a lineage root; `cli/show.js › isRoot`).
58
+ - **`revisions`** — records whose `parent != bytes32(0)` (a **CLAIMED** predecessor edge — see
59
+ `parent` in TRUST-BOUNDARIES; it is a claim, not proof of ancestry).
60
+ - **the block/time bounds** — the **earliest** and **latest** `{ blockNumber, timestamp }` seen across
61
+ the address's records. These are the same `block.number` / `block.timestamp` the records carry, with
62
+ the same meaning: on-chain ordering + an **UPPER BOUND on existence time**, never authorship time,
63
+ and `timestamp` is validator-influenced (prefer `blockNumber` for hard ordering).
64
+
65
+ The attribution counts are kept SEPARATE on purpose. `authorBound` and `anchorOnly` are **never**
66
+ collapsed into a single opaque number that would hide the difference between a front-running-resistant
67
+ claim and a cheap first-anchor.
68
+
69
+ ---
70
+
71
+ ## It is a NON-TRANSFERABLE DERIVED VIEW — NOT a token
72
+
73
+ The score is a **read**, not an asset:
74
+
75
+ - **Re-derivable by anyone.** Hand someone the same `(rpc, address)` and they recompute the identical
76
+ numbers. There is no privileged issuer, no per-address balance stored on-chain, nothing to mint, hold,
77
+ or move. `vh reputation` takes a **provider only — never a signer, never a key.**
78
+ - **Non-transferable.** There is nothing to transfer. The "score" is just `count`s over immutable
79
+ records; it cannot be sent, sold, or assigned. It confers no rights and holds no value.
80
+ - **NOT a token, NOT a security.** Issuing a transferable/tradeable reputation **token** on top of this
81
+ view is a **separate, human-gated decision** — proposal **D-2 / P-1** in
82
+ [`STRATEGY.md`](../STRATEGY.md), tagged `needs-human`, and **NOT built here**. This document and the
83
+ `vh reputation` command stay strictly on the non-transferable derived-view side of that line.
84
+
85
+ ---
86
+
87
+ ## What the score does NOT prove
88
+
89
+ The score inherits every limit of the records it counts. It adds **no** trust beyond them.
90
+
91
+ 1. **It does NOT validate record CONTENT.** "This address has N records" says nothing about whether
92
+ those records correspond to real, untampered bytes. A record only ever attested to a `contentHash`;
93
+ the `uri` is an **UNTRUSTED hint** the contract never fetched or validated. To bind any record to
94
+ actual content you must independently obtain it, **re-derive its hash** (`vh hash`), and run
95
+ `vh verify <path>` (re-derive-and-compare). The score never does this and never claims to.
96
+ 2. **It does NOT upgrade a front-runnable anchor's attribution.** Grouping records by `contributor` is
97
+ a **RAW ENUMERATION, NOT AN ENDORSEMENT** (the contract's own NatSpec on
98
+ `getRecordsByContributor` / `contributorRecordCount`). A record written via the front-runnable
99
+ `anchor()` is counted under its writer's address while staying `authorBound == false` — still only
100
+ "first anchorer", never proven authorship. Counting it does not make it stronger.
101
+ 3. **For anchor-only records, the grouping address is merely "first anchorer".** When
102
+ `authorBound == false`, `contributor` is whoever broadcast the `anchor()` transaction first — anyone
103
+ who learned a `contentHash` (e.g. from the public mempool) could have anchored it. So the address an
104
+ anchor-only record is grouped under is **not** a proven author; it is the first broadcaster. Only the
105
+ `authorBound` count groups under a **proven first claimant**.
106
+
107
+ ---
108
+
109
+ ## Anti-sybil: the meaningful signal is the `authorBound` count
110
+
111
+ Addresses are free to create and one-shot `anchor()` calls are cheap, so any metric that treats every
112
+ record equally is trivially **sybil-inflatable** — a single actor can spin up many addresses and anchor
113
+ many hashes (including hashes copied from someone else's mempool) at near-zero cost. None of that
114
+ proves authorship of anything.
115
+
116
+ The defense is **not** a gate or a stake; it is **reading the breakdown correctly**:
117
+
118
+ > **The meaningful signal is the `authorBound` (commit-reveal) count.** Producing a
119
+ > front-running-resistant claim has a real, irreducible cost: you must `commit` a sender-bound,
120
+ > salt-blinded commitment, wait out the `MIN_REVEAL_DELAY` maturation window, and then `reveal` — and
121
+ > only the original committer can ever reveal it (a copier who lifts the revealed values recomputes a
122
+ > commitment they never registered and reverts). That is the only count that reflects a proven,
123
+ > front-running-resistant claim of authorship.
124
+
125
+ By contrast, the `anchorOnly` count and the raw `total` are **cheap to inflate** (free address creation
126
+ + front-runnable single-tx anchors) and prove only order-of-anchoring. That is exactly why
127
+ `vh reputation` reports `authorBound` and `anchorOnly` **separately and never sums them**: a consumer
128
+ who wants a sybil-resistant reading should weight (or restrict to) `authorBound`, and treat `anchorOnly`
129
+ / `total` as the weak, inflatable figures they are. The score makes the distinction visible; it does not
130
+ make the weak signal strong.
131
+
132
+ ---
133
+
134
+ ## One-line summary
135
+
136
+ | Field | What it is | Do NOT read it as |
137
+ |-------|-----------|-------------------|
138
+ | `total` | how many records carry this address | a sybil-resistant measure (cheap to inflate) |
139
+ | `authorBound` | proven first-claimant (commit-reveal) records — the **meaningful, costly** signal | content validation (re-derive + `vh verify`) |
140
+ | `anchorOnly` | first-anchorer-only records — front-runnable, **weak**, cheap to inflate | proven authorship |
141
+ | `lineageRoots` / `revisions` | `parent == 0x0` vs a CLAIMED predecessor edge | proof of genuine content ancestry |
142
+ | `earliest` / `latest` block+ts | on-chain ordering + upper bound on existence time | authorship time; a precise wall clock |
143
+ | the whole score | a non-transferable, re-derivable DERIVED VIEW | a token, an asset, an endorsement, or content validation |
144
+
145
+ ## Tests
146
+
147
+ `test/cli.reputation.docs.test.js` is a docs-rot guard (pure: no chain, no fixtures). It asserts that
148
+ this file and README.md keep documenting the score the way `cli/reputation.js` actually behaves — that
149
+ the single read it aggregates is the paged `getRecordsByContributor` walk (with `contributorRecordCount`
150
+ named as the companion O(1) count, not a read the command issues), the authorBound vs
151
+ anchor-only and root vs revision breakdowns, that it is a non-transferable derived view (NOT a token;
152
+ any tradeable layer is D-2/P-1), what it does NOT prove, and the anti-sybil note that the meaningful
153
+ signal is the `authorBound` count — pinned to the caveats `cli/reputation.js` / `cli/list.js` export so
154
+ the prose can't silently drift from the implementation.
155
+
156
+
157
+ ---
158
+ <sub>© 2026 verifyhash.com · Licensed under Apache-2.0 (SPDX-License-Identifier: Apache-2.0) — see the [LICENSE](https://verifyhash.com/LICENSE) and [NOTICE](https://verifyhash.com/NOTICE) served with this file.</sub>