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,512 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>TrustLedger — Three-Way Trust-Account Reconciliation</title>
7
+ <style>
8
+ body { font: 15px/1.5 -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
9
+ color: #1a1a1a; max-width: 880px; margin: 2rem auto; padding: 0 1rem; }
10
+ h1 { font-size: 1.4rem; }
11
+ h2 { font-size: 1.1rem; margin-top: 1.6rem; }
12
+ .note { color: #555; font-size: .9rem; }
13
+ fieldset { border: 1px solid #ddd; border-radius: 6px; margin: 1rem 0; padding: 1rem; }
14
+ legend { font-weight: 600; padding: 0 .4rem; }
15
+ label { display: block; margin: .6rem 0 .2rem; font-weight: 600; }
16
+ select, input[type=file] { font: inherit; }
17
+ button { font: inherit; padding: .5rem 1rem; border-radius: 6px; border: 1px solid #0a6b2f;
18
+ background: #0a6b2f; color: #fff; cursor: pointer; }
19
+ button:disabled { opacity: .5; cursor: default; }
20
+ .verdict { display: inline-block; padding: .5rem 1rem; border-radius: 6px; font-weight: 700;
21
+ font-size: 1.05rem; margin: 1rem 0; }
22
+ .verdict.pass { background: #e6f4ea; color: #0a6b2f; border: 1px solid #0a6b2f; }
23
+ .verdict.fail { background: #fdeaea; color: #b00020; border: 1px solid #b00020; }
24
+ .err { color: #b00020; font-weight: 600; }
25
+ table { border-collapse: collapse; width: 100%; margin: .6rem 0; }
26
+ th, td { border: 1px solid #ddd; padding: .4rem .6rem; text-align: left; vertical-align: top; }
27
+ th { background: #f5f5f5; }
28
+ td.num { text-align: right; font-variant-numeric: tabular-nums; }
29
+ .sev { display: inline-block; padding: .05rem .45rem; border-radius: 4px; font-size: .8rem; font-weight: 700; }
30
+ .sev.error { background: #fdeaea; color: #b00020; }
31
+ .sev.warning { background: #fff6e0; color: #8a5a00; }
32
+ .sev.info { background: #eef3fb; color: #244a87; }
33
+ .none { color: #555; font-style: italic; }
34
+ .downloads a { display: inline-block; margin-right: 1rem; }
35
+ .check { font: inherit; padding: .2rem .55rem; margin-left: .4rem; border-radius: 5px;
36
+ border: 1px solid #888; background: #fff; color: #1a1a1a; cursor: pointer; }
37
+ .inspect { border: 1px solid #c9c9c9; border-radius: 6px; padding: .9rem 1rem; margin: 1rem 0;
38
+ background: #fafafa; }
39
+ .inspect h2, .inspect h3 { margin-top: .4rem; }
40
+ .inspect .ok { color: #0a6b2f; font-weight: 700; }
41
+ .inspect .bad { color: #b00020; font-weight: 700; }
42
+ .inspect select { padding: .2rem; }
43
+ .chips code { background: #eee; border-radius: 4px; padding: .05rem .35rem; margin-right: .3rem;
44
+ font-size: .85rem; }
45
+ iframe { width: 100%; height: 480px; border: 1px solid #ddd; border-radius: 6px; margin-top: 1rem; }
46
+ .disclaimer { background: #fffbe6; border: 1px solid #e6d77a; border-radius: 6px;
47
+ padding: .6rem .9rem; font-size: .85rem; margin: 1rem 0; }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <h1>TrustLedger — Three-Way Trust-Account Reconciliation</h1>
52
+ <!-- __TL_TRANSPORT_SEAM:NOTE:BEGIN__ (T-65.2: the offline single-file build,
53
+ trustledger/build-standalone.js, swaps this server-transport claim for the
54
+ honest offline one; the served page below is unchanged) -->
55
+ <p class="note">Drop your three monthly files. Your browser reads them and sends
56
+ their contents to this server; the reconciliation runs in memory and nothing is
57
+ stored on disk. The result, including the downloadable HTML and CSV audit packet,
58
+ comes straight back to your browser.</p>
59
+ <!-- __TL_TRANSPORT_SEAM:NOTE:END__ -->
60
+ <div class="disclaimer"><strong>Disclaimer.</strong> This tool AIDS reconciliation.
61
+ The broker remains the legal trust-account custodian and is solely responsible for
62
+ the accuracy of the trust-account records. It is tamper-evidence and a reconciliation
63
+ aid only — NOT a trusted timestamp, NOT legal, accounting, or audit advice, and NOT a
64
+ substitute for a CPA's review.</div>
65
+
66
+ <form id="f">
67
+ <fieldset>
68
+ <legend>The three files</legend>
69
+ <label for="bank">Bank statement (CSV or OFX/QFX)</label>
70
+ <input id="bank" type="file" accept=".csv,.ofx,.qfx,.txt" required>
71
+ <button type="button" class="check" data-source="bank">Check this file</button>
72
+ <label for="ledger">QuickBooks trust ledger (CSV)</label>
73
+ <input id="ledger" type="file" accept=".csv,.txt" required>
74
+ <button type="button" class="check" data-source="ledger">Check this file</button>
75
+ <label for="rentroll">Rent roll / tenant sub-ledger (CSV)</label>
76
+ <input id="rentroll" type="file" accept=".csv,.txt" required>
77
+ <button type="button" class="check" data-source="rentroll">Check this file</button>
78
+ </fieldset>
79
+ <fieldset>
80
+ <legend>Governing policy (optional)</legend>
81
+ <label for="state">State trust-account rules</label>
82
+ <select id="state">
83
+ <option value="">Baseline (no per-state policy)</option>
84
+ <option value="ca-example">CA (example / illustrative)</option>
85
+ </select>
86
+ <p class="note">The baseline reconcile is free. Selecting a per-state policy is a
87
+ paid feature and requires a license below.</p>
88
+ </fieldset>
89
+ <fieldset>
90
+ <legend>License (only needed for paid features)</legend>
91
+ <!-- __TL_TRANSPORT_SEAM:LICENSE_NOTE:BEGIN__ (T-65.2: the offline build swaps
92
+ this note — the offline app is the FREE tier and cannot verify a license;
93
+ the served page below is unchanged) -->
94
+ <p class="note">Per-state policy packs and the tamper-evident seal are paid
95
+ features. Paste the signed <code>*.vhlicense.json</code> your vendor sent you and
96
+ the vendor address it is pinned to. The baseline reconcile and file inspection
97
+ need no license. Verification happens offline against the vendor address — the
98
+ server never holds a key.</p>
99
+ <!-- __TL_TRANSPORT_SEAM:LICENSE_NOTE:END__ -->
100
+ <label for="license">Signed license (the contents of your *.vhlicense.json)</label>
101
+ <textarea id="license" rows="4" style="width:100%;font:inherit" placeholder="paste your signed license JSON here (optional)"></textarea>
102
+ <label for="vendorAddress">Vendor address (0x…)</label>
103
+ <input id="vendorAddress" type="text" style="width:100%;font:inherit" placeholder="0x… (optional; required only with a license)">
104
+ </fieldset>
105
+ <button id="go" type="submit">Reconcile</button>
106
+ </form>
107
+
108
+ <div id="inspect"></div>
109
+ <div id="result"></div>
110
+
111
+ <script>
112
+ (function () {
113
+ "use strict";
114
+ var form = document.getElementById("f");
115
+ var result = document.getElementById("result");
116
+ var inspectBox = document.getElementById("inspect");
117
+
118
+ // ---- which file input feeds which /api/inspect + /api/reconcile source. ----
119
+ // The map KEYS are the SAME three keys the reconcile body uses (bank / ledger /
120
+ // rentroll); the inspect `source` value uses the identical spelling, so a map
121
+ // fixed here threads straight into the reconcile body under the same key.
122
+ var SOURCES = ["bank", "ledger", "rentroll"];
123
+
124
+ // Column maps the broker has CONFIRMED in the inspect panel, keyed by source.
125
+ // Threaded into the reconcile body as `maps` so the fix applies to the REAL run,
126
+ // not just the preview. Empty until the broker maps a missing field.
127
+ var pendingMaps = {};
128
+
129
+ // ---- read a chosen file as text via the browser's FileReader. -------------
130
+ function read(input) {
131
+ return new Promise(function (resolve, reject) {
132
+ var file = input.files && input.files[0];
133
+ if (!file) { reject(new Error("please choose the " + input.id + " file")); return; }
134
+ var r = new FileReader();
135
+ r.onload = function () { resolve(String(r.result)); };
136
+ r.onerror = function () { reject(new Error("could not read " + input.id)); };
137
+ r.readAsText(file);
138
+ });
139
+ }
140
+
141
+ // ---- integer cents -> "$1,234.56" (exact; no float drift). ----------------
142
+ // The server is the source of truth for cents; this is display-only.
143
+ function fmtCents(cents) {
144
+ if (cents === null || cents === undefined) return "—";
145
+ var n = Number(cents);
146
+ var neg = n < 0;
147
+ var abs = Math.abs(n);
148
+ var whole = Math.floor(abs / 100);
149
+ var frac = abs % 100;
150
+ var dollars = String(whole).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
151
+ var cc = frac < 10 ? "0" + frac : String(frac);
152
+ return (neg ? "-$" : "$") + dollars + "." + cc;
153
+ }
154
+
155
+ function escapeHtml(s) {
156
+ return String(s === null || s === undefined ? "" : s).replace(/[&<>"']/g, function (c) {
157
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
158
+ });
159
+ }
160
+
161
+ // ---- a friendly label per logical field, for non-technical brokers. -------
162
+ function fieldLabel(field) {
163
+ var L = {
164
+ date: "Transaction date", amount: "Amount (signed)",
165
+ debit: "Debit / money out", credit: "Credit / money in",
166
+ memo: "Memo / description", type: "Type", tenant: "Tenant / beneficiary",
167
+ unit: "Unit", payment: "Payment", charge: "Charge", name: "Name"
168
+ };
169
+ return L[field] || field;
170
+ }
171
+
172
+ // ---- POST { source, text[, columnMap] } to /api/inspect. ------------------
173
+ function postInspect(source, text, columnMap) {
174
+ var body = { source: source, text: text };
175
+ if (columnMap) { body.columnMap = columnMap; }
176
+ // __TL_TRANSPORT_SEAM:INSPECT:BEGIN__ (T-65.2: the offline single-file build
177
+ // replaces this transport with a direct in-page call into the SAME door core
178
+ // — trustledger/door-core.js — the server routes this POST to.)
179
+ return fetch("/api/inspect", {
180
+ method: "POST",
181
+ headers: { "content-type": "application/json" },
182
+ body: JSON.stringify(body)
183
+ }).then(function (resp) {
184
+ return resp.json().then(function (data) { return { ok: resp.ok, data: data }; });
185
+ });
186
+ // __TL_TRANSPORT_SEAM:INSPECT:END__
187
+ }
188
+
189
+ // ---- render the diagnose report in a clear, non-technical layout. ----------
190
+ // No raw JSON, no stack traces: the detected header, a field->column table, the
191
+ // missing-field SELECTs (populated from the file's actual header), the row tally,
192
+ // and the first failing rows.
193
+ function renderInspect(source, text, rep) {
194
+ var miss = rep.requiredMissing || [];
195
+ var html = "<div class='inspect'>";
196
+ html += "<h2>Inspecting your <strong>" + escapeHtml(source) + "</strong> file</h2>";
197
+
198
+ html += "<p>Detected " + (rep.rowCount || 0) + " data row(s); " +
199
+ (rep.okCount || 0) + " read cleanly. Format: " + escapeHtml(rep.format || "csv") + ".</p>";
200
+
201
+ // Detected header as chips.
202
+ html += "<h3>Columns found in your file</h3><p class='chips'>";
203
+ (rep.header || []).forEach(function (h) { html += "<code>" + escapeHtml(h) + "</code>"; });
204
+ if (!(rep.header || []).length) { html += "<span class='none'>none</span>"; }
205
+ html += "</p>";
206
+
207
+ // field -> column table.
208
+ html += "<h3>How we matched each field</h3><table><thead><tr><th>Field</th>" +
209
+ "<th>Matched column</th></tr></thead><tbody>";
210
+ var mapped = rep.mapped || {};
211
+ Object.keys(mapped).forEach(function (field) {
212
+ var col = mapped[field];
213
+ var matchedMiss = miss.indexOf(field) !== -1;
214
+ html += "<tr><td>" + escapeHtml(fieldLabel(field)) + "</td><td>" +
215
+ (col === null || col === undefined
216
+ ? (matchedMiss ? "<span class='bad'>not matched (required)</span>"
217
+ : "<span class='none'>not matched</span>")
218
+ : "<code>" + escapeHtml(col) + "</code>") +
219
+ "</td></tr>";
220
+ });
221
+ html += "</tbody></table>";
222
+
223
+ // Per-missing-required-field SELECT, populated from the actual header.
224
+ if (miss.length) {
225
+ html += "<h3 class='bad'>Required fields we could not find</h3>" +
226
+ "<p>Pick which column in your file holds each one, then confirm.</p>";
227
+ miss.forEach(function (field) {
228
+ html += "<label>" + escapeHtml(fieldLabel(field)) + " (<code>" +
229
+ escapeHtml(field) + "</code>)</label>" +
230
+ "<select class='mapsel' data-field='" + escapeHtml(field) + "'>" +
231
+ "<option value=''>— choose a column —</option>";
232
+ (rep.header || []).forEach(function (h) {
233
+ html += "<option value='" + escapeHtml(h) + "'>" + escapeHtml(h) + "</option>";
234
+ });
235
+ html += "</select>";
236
+ });
237
+ html += "<p><button type='button' class='check' id='confirmMap'>Confirm mapping</button></p>";
238
+ html += "<p id='mapStatus'></p>";
239
+ } else {
240
+ html += "<p class='ok' id='inspectClean'>Every required field is matched — this file is ready to reconcile.</p>";
241
+ }
242
+
243
+ // First failing rows (sample of errors), human-readable.
244
+ var errs = (rep.errors || []).filter(function (e) { return e.row !== null && e.row !== undefined; });
245
+ if (errs.length) {
246
+ html += "<h3>First rows that did not parse</h3><table><thead><tr><th>Row</th>" +
247
+ "<th>What went wrong</th></tr></thead><tbody>";
248
+ errs.slice(0, 5).forEach(function (e) {
249
+ html += "<tr><td class='num'>" + escapeHtml(e.row) + "</td><td>" +
250
+ escapeHtml(e.message) + "</td></tr>";
251
+ });
252
+ html += "</tbody></table>";
253
+ } else if ((rep.sample || []).length) {
254
+ html += "<h3>First rows we read</h3><p class='note'>" + rep.sample.length +
255
+ " row(s) parsed cleanly.</p>";
256
+ }
257
+
258
+ html += "</div>";
259
+ inspectBox.innerHTML = html;
260
+
261
+ // Wire the confirm button: build a columnMap from the chosen SELECTs, re-POST
262
+ // /api/inspect to confirm the miss clears, and stash the map for the real run.
263
+ var confirm = document.getElementById("confirmMap");
264
+ if (confirm) {
265
+ confirm.addEventListener("click", function () {
266
+ var sels = inspectBox.querySelectorAll(".mapsel");
267
+ var columnMap = {};
268
+ var incomplete = false;
269
+ for (var i = 0; i < sels.length; i++) {
270
+ var v = sels[i].value;
271
+ if (!v) { incomplete = true; continue; }
272
+ columnMap[sels[i].getAttribute("data-field")] = v;
273
+ }
274
+ var status = document.getElementById("mapStatus");
275
+ if (incomplete) {
276
+ status.innerHTML = "<span class='bad'>Please choose a column for every required field.</span>";
277
+ return;
278
+ }
279
+ status.textContent = "Confirming…";
280
+ postInspect(source, text, columnMap).then(function (r) {
281
+ if (!r.ok) {
282
+ status.innerHTML = "<span class='bad'>" +
283
+ escapeHtml(r.data.message || "could not apply mapping") + "</span>";
284
+ return;
285
+ }
286
+ var still = (r.data.requiredMissing || []).length;
287
+ if (still) {
288
+ // Re-render with the still-missing fields under the new map.
289
+ renderInspect(source, text, r.data);
290
+ return;
291
+ }
292
+ // Clean: remember the map for the real reconcile run.
293
+ pendingMaps[source] = columnMap;
294
+ renderInspect(source, text, r.data);
295
+ var ok = document.getElementById("inspectClean");
296
+ if (ok) {
297
+ ok.innerHTML = "Mapping saved. Click <strong>Reconcile</strong> and this " +
298
+ "file will be read with your column mapping.";
299
+ }
300
+ }).catch(function (e) {
301
+ status.innerHTML = "<span class='bad'>" + escapeHtml(e.message || String(e)) + "</span>";
302
+ });
303
+ });
304
+ }
305
+ }
306
+
307
+ // ---- "Check this file": read the chosen file and inspect it. --------------
308
+ function checkFile(source) {
309
+ var input = document.getElementById(source);
310
+ inspectBox.innerHTML = "<p>Inspecting…</p>";
311
+ return read(input).then(function (text) {
312
+ return postInspect(source, text, pendingMaps[source]).then(function (r) {
313
+ if (!r.ok) {
314
+ inspectBox.innerHTML = "<div class='inspect'><p class='bad'>" +
315
+ escapeHtml(r.data.message || "could not inspect this file") + "</p></div>";
316
+ return;
317
+ }
318
+ renderInspect(source, text, r.data);
319
+ });
320
+ }).catch(function (e) {
321
+ inspectBox.innerHTML = "<div class='inspect'><p class='bad'>" +
322
+ escapeHtml(e.message || String(e)) + "</p></div>";
323
+ });
324
+ }
325
+
326
+ // ---- automatic fallback after a reconcile ingest_error. -------------------
327
+ // Inspect each already-read file and open the panel on the FIRST one that has a
328
+ // required field unmatched (the typical aliased-column cause). If none has a
329
+ // structural miss, inspect the bank file so the broker still sees row diagnoses.
330
+ function autoInspect(texts) {
331
+ var queue = SOURCES.filter(function (s) { return texts[s] != null; });
332
+ function tryNext(i) {
333
+ if (i >= queue.length) return;
334
+ var src = queue[i];
335
+ postInspect(src, texts[src], pendingMaps[src]).then(function (r) {
336
+ if (r.ok && (r.data.requiredMissing || []).length) {
337
+ renderInspect(src, texts[src], r.data);
338
+ return;
339
+ }
340
+ if (i + 1 < queue.length) { tryNext(i + 1); return; }
341
+ // No structural miss anywhere: show the first file's row-level diagnosis.
342
+ if (r.ok) { renderInspect(src, texts[src], r.data); }
343
+ }).catch(function () { tryNext(i + 1); });
344
+ }
345
+ tryNext(0);
346
+ }
347
+
348
+ // Wire each per-file "Check this file" button.
349
+ var checks = document.querySelectorAll("button.check[data-source]");
350
+ for (var ci = 0; ci < checks.length; ci++) {
351
+ (function (btn) {
352
+ btn.addEventListener("click", function () { checkFile(btn.getAttribute("data-source")); });
353
+ })(checks[ci]);
354
+ }
355
+
356
+ // ---- build an in-browser download link from text (no network round-trip). -
357
+ function downloadLink(text, mime, filename, caption) {
358
+ var a = document.createElement("a");
359
+ a.href = URL.createObjectURL(new Blob([text], { type: mime }));
360
+ a.download = filename;
361
+ a.textContent = caption;
362
+ return a;
363
+ }
364
+
365
+ // ---- render the three balances the broker watches. ------------------------
366
+ function renderBalances(b) {
367
+ var rows = [
368
+ ["Bank balance (per statement)", b.bank],
369
+ ["Adjusted bank balance", b.adjustedBank],
370
+ ["Book balance (per ledger)", b.book],
371
+ ["Sub-ledger total (beneficiaries)", b.subledger],
372
+ ["Reconciled balance", b.reconciled]
373
+ ];
374
+ var body = rows.map(function (r) {
375
+ return "<tr><td>" + escapeHtml(r[0]) + "</td><td class='num'>" +
376
+ escapeHtml(fmtCents(r[1])) + "</td></tr>";
377
+ }).join("");
378
+ return "<h2>The three balances</h2><table><thead><tr><th>Line</th>" +
379
+ "<th class='num'>Amount</th></tr></thead><tbody>" + body + "</tbody></table>";
380
+ }
381
+
382
+ // ---- render the exception table. ------------------------------------------
383
+ function renderExceptions(exceptions) {
384
+ var head = "<h2>Exceptions (" + exceptions.length + ")</h2><table><thead><tr>" +
385
+ "<th>Severity</th><th>Type</th><th>Label</th><th class='num'>Amount</th>" +
386
+ "<th>Detail</th></tr></thead><tbody>";
387
+ var body;
388
+ if (!exceptions.length) {
389
+ body = "<tr><td colspan='5' class='none'>No exceptions — every line reconciled.</td></tr>";
390
+ } else {
391
+ body = exceptions.map(function (e) {
392
+ var sev = String(e.severity || "info");
393
+ return "<tr><td><span class='sev " + escapeHtml(sev) + "'>" +
394
+ escapeHtml(sev.toUpperCase()) + "</span></td>" +
395
+ "<td>" + escapeHtml(e.type) + "</td>" +
396
+ "<td>" + escapeHtml(e.label) + "</td>" +
397
+ "<td class='num'>" + escapeHtml(fmtCents(e.amount)) + "</td>" +
398
+ "<td>" + escapeHtml(e.detail) + "</td></tr>";
399
+ }).join("");
400
+ }
401
+ return head + body + "</tbody></table>";
402
+ }
403
+
404
+ function render(d) {
405
+ var cls = d.pass ? "pass" : "fail";
406
+ var verdict = d.pass ? "PASS — three-way reconciliation ties out"
407
+ : "FAIL — see exceptions";
408
+
409
+ var html = "<div class='verdict " + cls + "' id='verdict'>" +
410
+ escapeHtml(verdict) + "</div>" +
411
+ "<p id='summary'>" + escapeHtml(d.summary || "") + "</p>" +
412
+ renderBalances(d.balances) +
413
+ renderExceptions(d.exceptions || []) +
414
+ "<h2>Download the audit packet</h2>" +
415
+ "<p class='downloads' id='downloads'></p>";
416
+ result.innerHTML = html;
417
+
418
+ var dl = document.getElementById("downloads");
419
+ dl.appendChild(downloadLink(
420
+ d.reportHtml, "text/html", "reconciliation-packet.html", "Download HTML packet"));
421
+ dl.appendChild(downloadLink(
422
+ d.reportCsv, "text/csv", "reconciliation-exceptions.csv", "Download CSV exceptions"));
423
+
424
+ var frame = document.createElement("iframe");
425
+ frame.title = "Audit packet preview";
426
+ result.appendChild(frame);
427
+ frame.srcdoc = d.reportHtml;
428
+ }
429
+
430
+ form.addEventListener("submit", function (ev) {
431
+ ev.preventDefault();
432
+ var go = document.getElementById("go");
433
+ go.disabled = true;
434
+ result.innerHTML = "<p>Reconciling…</p>";
435
+
436
+ var texts = {};
437
+ Promise.all([
438
+ read(document.getElementById("bank")),
439
+ read(document.getElementById("ledger")),
440
+ read(document.getElementById("rentroll"))
441
+ ]).then(function (t) {
442
+ texts.bank = t[0]; texts.ledger = t[1]; texts.rentroll = t[2];
443
+ var body = { bank: t[0], ledger: t[1], rentroll: t[2] };
444
+ var state = document.getElementById("state").value;
445
+ if (state) { body.state = state; }
446
+ // T-29.3: thread the OPTIONAL license gate fields. Only paid features
447
+ // (a per-state policy, the seal) need them; the server verifies the license
448
+ // OFFLINE against the vendorAddress and holds no key. Omitted entirely when
449
+ // blank, so the free baseline reconcile posts the identical body it always did.
450
+ var licenseText = document.getElementById("license").value.trim();
451
+ var vendorAddress = document.getElementById("vendorAddress").value.trim();
452
+ if (licenseText) { body.license = licenseText; }
453
+ if (vendorAddress) { body.vendorAddress = vendorAddress; }
454
+ // Thread any column maps the broker confirmed in the inspect panel so the
455
+ // REAL run honours them. Omitted entirely when no map was fixed, so the
456
+ // happy path posts the identical body it always did.
457
+ var maps = {};
458
+ var any = false;
459
+ SOURCES.forEach(function (s) {
460
+ if (pendingMaps[s]) { maps[s] = pendingMaps[s]; any = true; }
461
+ });
462
+ if (any) { body.maps = maps; }
463
+ // __TL_TRANSPORT_SEAM:RECONCILE:BEGIN__ (T-65.2: the offline single-file
464
+ // build replaces this transport with a direct in-page call into the SAME
465
+ // door core — trustledger/door-core.js — the server routes this POST to.)
466
+ return fetch("/api/reconcile", {
467
+ method: "POST",
468
+ headers: { "content-type": "application/json" },
469
+ body: JSON.stringify(body)
470
+ }).then(function (resp) {
471
+ return resp.json().then(function (data) { return { ok: resp.ok, data: data }; });
472
+ });
473
+ // __TL_TRANSPORT_SEAM:RECONCILE:END__
474
+ }).then(function (r) {
475
+ go.disabled = false;
476
+ if (!r.ok) {
477
+ // T-29.3: a license-gate refusal is shown as a clear "this feature requires
478
+ // a license" notice — never a raw error — so the broker understands the
479
+ // paid surface needs a license rather than reading a verification reason.
480
+ if (r.data.error === "license_required" || r.data.error === "license_invalid") {
481
+ result.innerHTML = "<div class='disclaimer' id='licenseNotice'>" +
482
+ "<strong>This feature requires a license.</strong> " +
483
+ escapeHtml(r.data.message || "") +
484
+ " The baseline reconcile and file inspection are free — clear the State" +
485
+ " selection to run them without a license, or paste your signed license" +
486
+ " and vendor address above.</div>";
487
+ return;
488
+ }
489
+ result.innerHTML = "<p class='err' id='error'>Error (" +
490
+ escapeHtml(r.data.error || "error") + "): " +
491
+ escapeHtml(r.data.message || "") + "</p>";
492
+ // Automatic fallback: if the parse failed on ingest, open the inspector on
493
+ // the offending file so the broker can map columns and try again. The
494
+ // engine error names the source only when a row is located; otherwise we
495
+ // inspect each file and surface the FIRST one with a required field
496
+ // unmatched (the most likely cause of an aliased-column parse failure).
497
+ if (r.data.error === "ingest_error") {
498
+ autoInspect(texts);
499
+ }
500
+ return;
501
+ }
502
+ render(r.data);
503
+ }).catch(function (e) {
504
+ go.disabled = false;
505
+ result.innerHTML = "<p class='err' id='error'>" +
506
+ escapeHtml(e.message || String(e)) + "</p>";
507
+ });
508
+ });
509
+ })();
510
+ </script>
511
+ </body>
512
+ </html>