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,391 @@
1
+ "use strict";
2
+
3
+ // TrustLedger — close.js
4
+ //
5
+ // T-24.1: the versioned PERIOD-CLOSE artifact + pure build / read / validate,
6
+ // plus a pure continuity check that chains one period to the next.
7
+ //
8
+ // THE PROBLEM THIS SOLVES.
9
+ // A three-way trust reconciliation is a MONTHLY ritual. Each month's reconciled
10
+ // ending balances become the NEXT month's opening balances — the "roll-forward".
11
+ // If June closes at a bank balance of $12,345.67, July MUST open at exactly
12
+ // $12,345.67; any other opening means a period was skipped, edited, or re-keyed,
13
+ // and the chain of custody over the trust money is broken. This module emits a
14
+ // small, strictly-validated JSON "close" artifact at the end of a period so the
15
+ // next period can SEED its opening from it and the tool can CHECK the roll-forward
16
+ // is penny-exact.
17
+ //
18
+ // PURE + DETERMINISTIC. `buildClose(model)` derives the artifact purely from the
19
+ // report packet model (no clock, no I/O, no randomness). Given the same model it
20
+ // returns a byte-identical artifact — including a deterministic `inputsDigest`
21
+ // (a SHA-256 over the normalized inputs the packet already holds, via the
22
+ // vendored pure-JS SHA-256 in ./lib/sha256-vendored — NO new dependency, NO
23
+ // Node-only builtin, so this module is browser-portable; the vendored digest is
24
+ // proven byte-identical to Node's `crypto` by test) that BINDS the close to the
25
+ // data it summarizes, so a tampered or swapped close is detectable.
26
+ //
27
+ // HONEST POSTURE — the close is an UNTRUSTED CONVENIENCE HINT.
28
+ // Consistent with the codebase's standing trust boundary (docs/TRUST-BOUNDARIES.md
29
+ // and the receipt NatSpec): this artifact carries the prior period's ASSERTED
30
+ // ending so the next run can seed + check the opening, but the AUTHORITATIVE
31
+ // verdict is always the freshly RECOMPUTED reconciliation — never the value
32
+ // written here. A broker who edits this file changes a hint, not the truth: the
33
+ // next reconciliation recomputes the three balances from the source files and the
34
+ // continuity check merely reports whether the asserted roll-forward matched. The
35
+ // close is NOT signed and NOT timestamped; like every other artifact in this repo
36
+ // it rides the human trust-root (the broker remains the legal custodian and a CPA
37
+ // review still governs). It does not, and cannot, replace that review.
38
+
39
+ const { sha256HexUtf8 } = require("./lib/sha256-vendored");
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Schema version. Bumped only on a breaking shape change. `validateClose`
43
+ // REJECTS any other value rather than guessing — a close from a future/older
44
+ // tool must be handled deliberately, never silently coerced.
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const SCHEMA_VERSION = "trustledger.period-close/v1";
48
+
49
+ // A SHA-256 hex digest is exactly 64 lowercase hex chars.
50
+ const DIGEST_RE = /^[0-9a-f]{64}$/;
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Errors — STRICT. A malformed/ambiguous close raises a NAMED error rather than
54
+ // being silently dropped, coerced, or partially accepted.
55
+ // ---------------------------------------------------------------------------
56
+
57
+ class CloseError extends Error {
58
+ constructor(message) {
59
+ super(message);
60
+ this.name = "CloseError";
61
+ }
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Small strict helpers
66
+ // ---------------------------------------------------------------------------
67
+
68
+ function isPlainObject(v) {
69
+ return v != null && typeof v === "object" && !Array.isArray(v);
70
+ }
71
+
72
+ // An integer-cents money value: a JS integer (no floats, no NaN, no Infinity).
73
+ function isCents(v) {
74
+ return Number.isInteger(v);
75
+ }
76
+
77
+ // A { bank, book } balance pair where both legs are integer cents.
78
+ function isBalancePair(v) {
79
+ return isPlainObject(v) && isCents(v.bank) && isCents(v.book);
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // The deterministic inputs digest.
84
+ //
85
+ // We hash a CANONICAL, order-stable JSON projection of the inputs the packet
86
+ // already summarizes — the period, report date, opening, ending, subledger, and
87
+ // the input record counts — so the digest is reproducible to the byte for the
88
+ // same model and CHANGES if any of those summarized facts change. This binds the
89
+ // close to its data without pulling in a new dependency: the vendored pure-JS
90
+ // SHA-256 (./lib/sha256-vendored), byte-identical to Node's built-in crypto by
91
+ // test, and portable to a browser where the Node `crypto` builtin does not exist.
92
+ //
93
+ // NOTE: this is a convenience integrity tag over the SUMMARY the close carries,
94
+ // NOT a cryptographic proof of the underlying source files (which are the
95
+ // authoritative inputs and are re-read on the next reconciliation). It lets a
96
+ // reader detect a hand-edited close field; it is not a signature.
97
+ // ---------------------------------------------------------------------------
98
+
99
+ function canonicalInputs(parts) {
100
+ // Build the object with keys in a fixed, explicit order so JSON.stringify is
101
+ // byte-stable regardless of how the caller's model was assembled.
102
+ return JSON.stringify({
103
+ schemaVersion: SCHEMA_VERSION,
104
+ period: parts.period,
105
+ reportDate: parts.reportDate,
106
+ opening: { bank: parts.opening.bank, book: parts.opening.book },
107
+ ending: { bank: parts.ending.bank, book: parts.ending.book },
108
+ subledger: parts.subledger,
109
+ tiesOut: parts.tiesOut,
110
+ pass: parts.pass,
111
+ inputs: {
112
+ bankRecords: parts.inputs.bankRecords,
113
+ bookRecords: parts.inputs.bookRecords,
114
+ rentrollRecords: parts.inputs.rentrollRecords,
115
+ },
116
+ });
117
+ }
118
+
119
+ function digestInputs(parts) {
120
+ // Exactly crypto.createHash("sha256").update(canonicalInputs(parts), "utf8")
121
+ // .digest("hex"), computed by the vendored pure-JS SHA-256 so the browser
122
+ // path needs no Node builtin. Byte-identity with node:crypto is pinned by
123
+ // test/trustledger.browser-core.test.js (incl. the committed close fixtures).
124
+ return sha256HexUtf8(canonicalInputs(parts));
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // buildClose(model) — derive the close artifact PURELY from the packet model.
129
+ //
130
+ // Reuses model.opening / model.balances / model.period / model.reportDate and
131
+ // the model's pass/tiesOut verdict. The `ending` balances are the period's
132
+ // CLOSING bank/book ({ bank: model.balances.bank, book: model.balances.book });
133
+ // `subledger` is model.balances.subledger. Computes the deterministic
134
+ // inputsDigest. Returns a JSON-serializable object; byte-deterministic for a
135
+ // given model.
136
+ // ---------------------------------------------------------------------------
137
+
138
+ function buildClose(model) {
139
+ if (!isPlainObject(model)) {
140
+ throw new CloseError("buildClose requires the report packet model object");
141
+ }
142
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(model.reportDate || ""))) {
143
+ throw new CloseError('model.reportDate must be a "YYYY-MM-DD" string');
144
+ }
145
+ if (!isBalancePair(model.balances)) {
146
+ throw new CloseError(
147
+ "model.balances must carry integer-cents bank/book balances"
148
+ );
149
+ }
150
+ if (!isCents(model.balances.subledger)) {
151
+ throw new CloseError("model.balances.subledger must be integer cents");
152
+ }
153
+
154
+ // opening: reuse model.opening when present (integer cents both legs), else
155
+ // treat the period as opening from zero. We never coerce a non-integer opening
156
+ // into something else — a present-but-garbled opening is a hard error.
157
+ let opening;
158
+ if (model.opening == null) {
159
+ opening = { bank: 0, book: 0 };
160
+ } else if (isBalancePair(model.opening)) {
161
+ opening = { bank: model.opening.bank, book: model.opening.book };
162
+ } else {
163
+ throw new CloseError(
164
+ "model.opening, when present, must carry integer-cents bank/book balances"
165
+ );
166
+ }
167
+
168
+ const ending = { bank: model.balances.bank, book: model.balances.book };
169
+ const subledger = model.balances.subledger;
170
+
171
+ // The verdict the close records: prefer the explicit pass flag, fall back to
172
+ // tiesOut. Both are booleans on the packet model.
173
+ const tiesOut = model.tiesOut === true;
174
+ const pass = model.pass === undefined ? tiesOut : model.pass === true;
175
+
176
+ const inputs = {
177
+ bankRecords: countOf(model, "bankRecords"),
178
+ bookRecords: countOf(model, "bookRecords"),
179
+ rentrollRecords: countOf(model, "rentrollRecords"),
180
+ };
181
+
182
+ const digestParts = {
183
+ period: model.period == null ? null : String(model.period),
184
+ reportDate: model.reportDate,
185
+ opening,
186
+ ending,
187
+ subledger,
188
+ tiesOut,
189
+ pass,
190
+ inputs,
191
+ };
192
+
193
+ const close = {
194
+ schemaVersion: SCHEMA_VERSION,
195
+ period: model.period == null ? null : String(model.period),
196
+ reportDate: model.reportDate,
197
+ opening,
198
+ ending,
199
+ subledger,
200
+ tiesOut,
201
+ pass,
202
+ inputs,
203
+ inputsDigest: digestInputs(digestParts),
204
+ };
205
+
206
+ // Self-check: the artifact we just built must itself validate. This guarantees
207
+ // build/validate stay in lock-step — a build can never emit something read
208
+ // back as corrupt.
209
+ validateClose(close);
210
+ return close;
211
+ }
212
+
213
+ // Pull a record count off the packet model's `inputs` block, defaulting to 0
214
+ // when the model did not carry it. Always a non-negative integer in the digest.
215
+ function countOf(model, key) {
216
+ const n = model.inputs && model.inputs[key];
217
+ return Number.isInteger(n) && n >= 0 ? n : 0;
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // readClose(text|obj) — parse + validate a close. Accepts either a JSON string
222
+ // (parsed strictly — a parse error is a CloseError, not a thrown SyntaxError) or
223
+ // an already-parsed object. Returns the validated object. STRICT: a partial or
224
+ // corrupt close NEVER round-trips silently — validateClose rejects it.
225
+ // ---------------------------------------------------------------------------
226
+
227
+ function readClose(input) {
228
+ let obj;
229
+ if (typeof input === "string") {
230
+ try {
231
+ obj = JSON.parse(input);
232
+ } catch (e) {
233
+ throw new CloseError(`close is not valid JSON: ${e.message}`);
234
+ }
235
+ } else if (isPlainObject(input)) {
236
+ obj = input;
237
+ } else {
238
+ throw new CloseError("readClose requires a JSON string or a close object");
239
+ }
240
+ validateClose(obj);
241
+ return obj;
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // validateClose(obj) — STRICT structural + value validation. Throws a named
246
+ // CloseError on the FIRST problem found; returns the object unchanged on success.
247
+ // Rejects: a wrong schemaVersion; a missing/garbled period / reportDate /
248
+ // opening / ending / subledger; a non-integer-cents balance; a malformed digest.
249
+ // ---------------------------------------------------------------------------
250
+
251
+ function validateClose(obj) {
252
+ if (!isPlainObject(obj)) {
253
+ throw new CloseError("close must be an object");
254
+ }
255
+
256
+ if (obj.schemaVersion !== SCHEMA_VERSION) {
257
+ throw new CloseError(
258
+ `unsupported schemaVersion: expected "${SCHEMA_VERSION}", got ${JSON.stringify(
259
+ obj.schemaVersion
260
+ )}`
261
+ );
262
+ }
263
+
264
+ // period: required key; null is allowed (a period label is optional metadata),
265
+ // but a present period MUST be a string — an object/number is garbled.
266
+ if (!("period" in obj)) {
267
+ throw new CloseError("close is missing `period`");
268
+ }
269
+ if (obj.period !== null && typeof obj.period !== "string") {
270
+ throw new CloseError("close.period must be a string or null");
271
+ }
272
+
273
+ // reportDate: required, a strict "YYYY-MM-DD" string.
274
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(obj.reportDate || ""))) {
275
+ throw new CloseError('close.reportDate must be a "YYYY-MM-DD" string');
276
+ }
277
+
278
+ // opening / ending: required integer-cents { bank, book } pairs.
279
+ if (!("opening" in obj)) {
280
+ throw new CloseError("close is missing `opening`");
281
+ }
282
+ if (!isBalancePair(obj.opening)) {
283
+ throw new CloseError(
284
+ "close.opening must carry integer-cents bank/book balances"
285
+ );
286
+ }
287
+ if (!("ending" in obj)) {
288
+ throw new CloseError("close is missing `ending`");
289
+ }
290
+ if (!isBalancePair(obj.ending)) {
291
+ throw new CloseError(
292
+ "close.ending must carry integer-cents bank/book balances"
293
+ );
294
+ }
295
+
296
+ // subledger: required integer cents.
297
+ if (!("subledger" in obj)) {
298
+ throw new CloseError("close is missing `subledger`");
299
+ }
300
+ if (!isCents(obj.subledger)) {
301
+ throw new CloseError("close.subledger must be integer cents");
302
+ }
303
+
304
+ // verdict flags: required booleans.
305
+ if (typeof obj.tiesOut !== "boolean") {
306
+ throw new CloseError("close.tiesOut must be a boolean");
307
+ }
308
+ if (typeof obj.pass !== "boolean") {
309
+ throw new CloseError("close.pass must be a boolean");
310
+ }
311
+
312
+ // inputs: required record-count block, each a non-negative integer.
313
+ if (!isPlainObject(obj.inputs)) {
314
+ throw new CloseError("close is missing `inputs` record counts");
315
+ }
316
+ for (const k of ["bankRecords", "bookRecords", "rentrollRecords"]) {
317
+ const n = obj.inputs[k];
318
+ if (!Number.isInteger(n) || n < 0) {
319
+ throw new CloseError(`close.inputs.${k} must be a non-negative integer`);
320
+ }
321
+ }
322
+
323
+ // inputsDigest: required, a well-formed lowercase SHA-256 hex string.
324
+ if (typeof obj.inputsDigest !== "string" || !DIGEST_RE.test(obj.inputsDigest)) {
325
+ throw new CloseError(
326
+ "close.inputsDigest must be a 64-char lowercase hex SHA-256 digest"
327
+ );
328
+ }
329
+
330
+ return obj;
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // checkContinuity(priorClose, opening) — pure roll-forward check.
335
+ //
336
+ // Compares the prior period's asserted `ending` to THIS period's `opening`,
337
+ // PENNY-EXACT. The comparison takes NO tolerance: a roll-forward must be exact,
338
+ // so a one-cent drift is a real gap, not noise.
339
+ //
340
+ // Returns a structured result, never throwing on a gap (the CALLER decides how
341
+ // to surface it — T-24.2 turns a gap into a continuity exception):
342
+ // { ok: <bool>, bankGap: <int cents>, bookGap: <int cents> }
343
+ // where bankGap = opening.bank - priorEnding.bank (signed; positive means this
344
+ // period opened HIGHER than the prior period closed) and likewise bookGap. `ok`
345
+ // is true iff both gaps are exactly zero.
346
+ //
347
+ // A null/undefined priorClose means there is NO prior period to chain from (the
348
+ // first period a broker runs): that is `{ ok: true }` — nothing to reconcile
349
+ // against, so nothing can be out of continuity.
350
+ //
351
+ // SIDE-EFFECT FREE. Reads its arguments, returns a fresh object, mutates nothing.
352
+ // Honest-posture reminder: a passing continuity check confirms the asserted
353
+ // roll-forward is internally consistent; it does NOT independently verify the
354
+ // prior period — the prior close is an untrusted hint, and the authoritative
355
+ // numbers are the freshly recomputed reconciliation.
356
+ // ---------------------------------------------------------------------------
357
+
358
+ function checkContinuity(priorClose, opening) {
359
+ // No prior period to chain from.
360
+ if (priorClose == null) {
361
+ return { ok: true };
362
+ }
363
+
364
+ // The prior close must be a well-formed close to be compared against — a
365
+ // garbled prior is a hard error here (not a silent pass), because chaining
366
+ // from corrupt data would defeat the whole point of the check. Use readClose
367
+ // so a JSON string or object both work, and a corrupt one is rejected loudly.
368
+ const prior = readClose(priorClose);
369
+
370
+ if (!isBalancePair(opening)) {
371
+ throw new CloseError(
372
+ "checkContinuity opening must carry integer-cents bank/book balances"
373
+ );
374
+ }
375
+
376
+ const bankGap = opening.bank - prior.ending.bank;
377
+ const bookGap = opening.book - prior.ending.book;
378
+ const ok = bankGap === 0 && bookGap === 0;
379
+ return { ok, bankGap, bookGap };
380
+ }
381
+
382
+ module.exports = {
383
+ SCHEMA_VERSION,
384
+ CloseError,
385
+ buildClose,
386
+ readClose,
387
+ validateClose,
388
+ checkContinuity,
389
+ // exported for focused tests / reuse
390
+ digestInputs,
391
+ };
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+
3
+ // TrustLedger — corpus loader + runner (T-44.2).
4
+ //
5
+ // A PURE loader/runner over the COMMITTED out-of-trust corpus under
6
+ // trustledger/fixtures/corpus/. It discovers every scenario folder, runs each
7
+ // one through the REAL reconcile + buildPacket verdict path (the SAME path the
8
+ // CLI/--json reconcile exit code uses), and returns one structured ROW per
9
+ // scenario describing what the corpus EXPECTS versus what the live engine
10
+ // ACTUALLY does:
11
+ //
12
+ // { id, control, principle, expected, actual, match }
13
+ //
14
+ // This module adds NO crypto, NO control logic, NO severity, and NO verdict
15
+ // rule. It is a faithful caller: the verdict is read straight off
16
+ // buildPacket's `model.pass` flag, exactly as the shipped reconcile gate exits.
17
+ // The CLI (`vh trust corpus`) renders these rows; tests assert them. The whole
18
+ // point is that a CPA or broker can RUN one command to confirm the gate FAILs
19
+ // the exact frauds it claims to catch — WITHOUT reading test/.
20
+ //
21
+ // The runScenario body is a deliberate sibling of test/trustledger.corpus.test.js
22
+ // (T-44.1): both drive the same fixture shape through the same real engine path.
23
+ // Keeping the runner here (not only in the test) is what makes the corpus a
24
+ // shippable, human-runnable artifact rather than a developer-only assertion.
25
+
26
+ const fs = require("fs");
27
+ const path = require("path");
28
+
29
+ const report = require("./report");
30
+
31
+ const CORPUS_DIR = path.join(__dirname, "fixtures", "corpus");
32
+
33
+ // A scenario verdict is one of these two strings; the corpus meta records the
34
+ // EXPECTED one and the live engine produces the ACTUAL one.
35
+ const VERDICT = Object.freeze({ PASS: "PASS", FAIL: "FAIL" });
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Discovery + load.
39
+ // ---------------------------------------------------------------------------
40
+
41
+ function loadJSON(p) {
42
+ return JSON.parse(fs.readFileSync(p, "utf8"));
43
+ }
44
+
45
+ // Every scenario folder under the corpus root, sorted for a DETERMINISTIC row
46
+ // order. A leading-underscore folder (e.g. _shared) holds shared artifacts (a
47
+ // prior-close), not a scenario, and is skipped.
48
+ function scenarioIds(corpusDir = CORPUS_DIR) {
49
+ return fs
50
+ .readdirSync(corpusDir, { withFileTypes: true })
51
+ .filter((d) => d.isDirectory() && !d.name.startsWith("_"))
52
+ .map((d) => d.name)
53
+ .sort();
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Run ONE scenario through the REAL reconcile + buildPacket verdict path.
58
+ //
59
+ // inputs.json carries already-normalized record arrays (the shape ingest.js
60
+ // produces) plus optional opening / policy / toleranceCents / a priorClosePath
61
+ // (resolved relative to the corpus root, read as the raw close JSON buildPacket
62
+ // re-validates). No engine code is touched — this is a faithful caller, the
63
+ // SAME wiring the corpus regression test uses.
64
+ // ---------------------------------------------------------------------------
65
+
66
+ function buildArgs(inputs, corpusDir) {
67
+ const args = {
68
+ bank: inputs.bank,
69
+ book: inputs.book,
70
+ rentroll: inputs.rentroll,
71
+ reportDate: inputs.reportDate,
72
+ period: inputs.period,
73
+ };
74
+ if (inputs.opening) args.opening = inputs.opening;
75
+ if (inputs.policy) args.policy = inputs.policy;
76
+ if (inputs.toleranceCents !== undefined) {
77
+ args.toleranceCents = inputs.toleranceCents;
78
+ }
79
+ if (inputs.priorClosePath) {
80
+ // Read the prior close as the raw JSON string buildPacket accepts (it calls
81
+ // close.readClose, which re-validates). Resolved relative to the corpus root.
82
+ args.priorClose = fs.readFileSync(
83
+ path.join(corpusDir, inputs.priorClosePath),
84
+ "utf8"
85
+ );
86
+ }
87
+ return args;
88
+ }
89
+
90
+ // Run a single scenario folder -> the live packet model + a normalized result.
91
+ // Returns { model, actual } where `actual` is "PASS"/"FAIL" read off model.pass
92
+ // (the SAME flag the CLI reconcile exit code uses).
93
+ function runScenario(id, corpusDir = CORPUS_DIR) {
94
+ const dir = path.join(corpusDir, id);
95
+ const inputs = loadJSON(path.join(dir, "inputs.json"));
96
+ const model = report.buildPacket(buildArgs(inputs, corpusDir));
97
+ const actual = model.pass ? VERDICT.PASS : VERDICT.FAIL;
98
+ return { model, actual };
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Run the WHOLE corpus -> structured rows + an aggregate result.
103
+ // ---------------------------------------------------------------------------
104
+
105
+ // Reduce a meta.principle (which may be a full paragraph) to a SINGLE sentence,
106
+ // so the table/JSON `principle` is the one-sentence trust-law annotation the
107
+ // task calls for, not a wall of text. Deterministic: takes the text up to and
108
+ // including the first sentence terminator, collapsing internal whitespace.
109
+ function oneSentence(text) {
110
+ const s = String(text == null ? "" : text).replace(/\s+/g, " ").trim();
111
+ if (s === "") return "";
112
+ const m = s.match(/^[^.!?]*[.!?]/);
113
+ return (m ? m[0] : s).trim();
114
+ }
115
+
116
+ // Build the row for one scenario. `expected` comes from the committed meta;
117
+ // `actual` from the live engine; `match` is their equality. A row also carries
118
+ // the meta `control` and the one-sentence `principle` for the human table.
119
+ function buildRow(id, corpusDir = CORPUS_DIR) {
120
+ const meta = loadJSON(path.join(corpusDir, id, "meta.json"));
121
+ const { actual } = runScenario(id, corpusDir);
122
+ const expected = meta.expectedVerdict;
123
+ return {
124
+ id,
125
+ control: meta.control,
126
+ principle: oneSentence(meta.principle),
127
+ expected,
128
+ actual,
129
+ match: expected === actual,
130
+ };
131
+ }
132
+
133
+ // Run every scenario. Returns:
134
+ // { rows, total, matched, mismatched, ok }
135
+ // `ok` is true iff EVERY row matched — the CI-gateable condition the CLI exits
136
+ // 0 on (and exits 3 on any mismatch / corpus drift). Rows are in deterministic
137
+ // (sorted-id) order, so the output is reproducible run to run.
138
+ function runCorpus(corpusDir = CORPUS_DIR) {
139
+ const rows = scenarioIds(corpusDir).map((id) => buildRow(id, corpusDir));
140
+ const matched = rows.filter((r) => r.match).length;
141
+ const mismatched = rows.length - matched;
142
+ return {
143
+ rows,
144
+ total: rows.length,
145
+ matched,
146
+ mismatched,
147
+ ok: mismatched === 0,
148
+ };
149
+ }
150
+
151
+ module.exports = {
152
+ CORPUS_DIR,
153
+ VERDICT,
154
+ scenarioIds,
155
+ runScenario,
156
+ buildRow,
157
+ runCorpus,
158
+ oneSentence,
159
+ };
@@ -0,0 +1,99 @@
1
+ {
2
+ "schema": "verifyhash/build-provenance@1",
3
+ "description": "Maps the published TrustLedger standalone offline app's sha256 to the ordered, individually-hashed in-tree source files it inlines. Reproduce + attest the whole chain offline with: node trustledger/build-standalone.js --check",
4
+ "targets": {
5
+ "trustledger-standalone": {
6
+ "bundle": "trustledger-standalone.html",
7
+ "sidecar": "trustledger-standalone.html.sha256",
8
+ "bundleBytes": 273125,
9
+ "bundleSha256": "1afa0e08c9624505ad967ab88c91553c6af00b0e5e0f2b790ddf315741ed1742",
10
+ "sidecarLine": "1afa0e08c9624505ad967ab88c91553c6af00b0e5e0f2b790ddf315741ed1742 trustledger-standalone.html",
11
+ "page": {
12
+ "sourceFile": "trustledger/public/index.html",
13
+ "sourceSha256": "eee5780a7e7cc62dd4e4baf2bcbb2af47fb68babd95d4ad2d4fe1e54b0fee9d4"
14
+ },
15
+ "modules": [
16
+ {
17
+ "id": "sha256-vendored",
18
+ "synthetic": false,
19
+ "sourceFile": "trustledger/lib/sha256-vendored.js",
20
+ "sourceSha256": "9aa8ca87a66261e36b2468d0bf20f0598b97f818d3dfb3308b9452eb1b5eacbb",
21
+ "inlinedSha256": "9aa8ca87a66261e36b2468d0bf20f0598b97f818d3dfb3308b9452eb1b5eacbb",
22
+ "entry": false
23
+ },
24
+ {
25
+ "id": "policy-bundled-loader",
26
+ "synthetic": true,
27
+ "sourceFile": null,
28
+ "sourceSha256": null,
29
+ "inlinedSha256": "693cadeb0fb79f339532fd0df91bb655ae5c5055b32fd2644017a9fc86c135cb",
30
+ "note": "swapped body (bundled-policy loader shim; inlines trustledger/fixtures/policy/*.json) — defined in build-standalone.js, not a source file"
31
+ },
32
+ {
33
+ "id": "reconcile",
34
+ "synthetic": false,
35
+ "sourceFile": "trustledger/reconcile.js",
36
+ "sourceSha256": "0177964804e3e993891a3cca617b1a0421f14579ce4a5407741ab902eab44322",
37
+ "inlinedSha256": "0177964804e3e993891a3cca617b1a0421f14579ce4a5407741ab902eab44322",
38
+ "entry": false
39
+ },
40
+ {
41
+ "id": "policy",
42
+ "synthetic": false,
43
+ "sourceFile": "trustledger/policy.js",
44
+ "sourceSha256": "a07cf5252991281441e251df31c6336057a61bc0fe45aa81e5321f88b8353b74",
45
+ "inlinedSha256": "ce3f2b8902d7a94d7e5190808696f53f649fc1357c2c512e2c80884db4823b5e",
46
+ "entry": false
47
+ },
48
+ {
49
+ "id": "match",
50
+ "synthetic": false,
51
+ "sourceFile": "trustledger/match.js",
52
+ "sourceSha256": "e06f9bc39c66ee748b422270e4edddc67640d8ebdda27838b95ece0e0ba09988",
53
+ "inlinedSha256": "e06f9bc39c66ee748b422270e4edddc67640d8ebdda27838b95ece0e0ba09988",
54
+ "entry": false
55
+ },
56
+ {
57
+ "id": "ingest",
58
+ "synthetic": false,
59
+ "sourceFile": "trustledger/ingest.js",
60
+ "sourceSha256": "6076b1f88e05ee175ff134f2ba018454885c71659ce40fbb91f4fb96e2944920",
61
+ "inlinedSha256": "6076b1f88e05ee175ff134f2ba018454885c71659ce40fbb91f4fb96e2944920",
62
+ "entry": false
63
+ },
64
+ {
65
+ "id": "close",
66
+ "synthetic": false,
67
+ "sourceFile": "trustledger/close.js",
68
+ "sourceSha256": "ef9312871b59763d76de48e7b6e1225e920577d813e98c25bff66c95fc276134",
69
+ "inlinedSha256": "10f83eaa35167ef076cd03c03781480b660fb87e97d319a56ba1dd6a731a1f94",
70
+ "entry": false
71
+ },
72
+ {
73
+ "id": "report",
74
+ "synthetic": false,
75
+ "sourceFile": "trustledger/report.js",
76
+ "sourceSha256": "4fc13974d2b85482c972f2f9617f402a4e6ced04c712042b6bb727f146134bc6",
77
+ "inlinedSha256": "6e5623fd5f4560925b2dc06d135a1fcab9170b503b37ecfc1b7295f47de0a7c8",
78
+ "entry": false
79
+ },
80
+ {
81
+ "id": "license",
82
+ "synthetic": true,
83
+ "sourceFile": null,
84
+ "sourceSha256": null,
85
+ "inlinedSha256": "b9efa9635ddfd6c4beb6f8bf10f268b5b896a72898b42ba3ec73c5b50811380a",
86
+ "note": "swapped body (fail-closed offline license shim) — defined in build-standalone.js, not a source file"
87
+ },
88
+ {
89
+ "id": "door-core",
90
+ "synthetic": false,
91
+ "sourceFile": "trustledger/door-core.js",
92
+ "sourceSha256": "7c04cb3fbe338f3fd97059974b14d2977828434bd649ff23bef4c85ee57f5b46",
93
+ "inlinedSha256": "d3b2672104c4c2ed9a97f292f569c4f013bfa120c8085f33f9f054cf47c98654",
94
+ "entry": true
95
+ }
96
+ ]
97
+ }
98
+ }
99
+ }