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.
- package/LICENSE +201 -0
- package/README.md +883 -0
- package/cli/abi/ContributionRegistry.json +881 -0
- package/cli/agent.js +2173 -0
- package/cli/anchor-artifact.js +853 -0
- package/cli/anchor.js +400 -0
- package/cli/claim.js +881 -0
- package/cli/core/agent-commit.js +448 -0
- package/cli/core/agent-session.js +598 -0
- package/cli/core/anchor-binding.js +663 -0
- package/cli/core/attestation.js +580 -0
- package/cli/core/evidence-plans.js +495 -0
- package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
- package/cli/core/fulfill-intake.js +1082 -0
- package/cli/core/go-live-preflight.js +481 -0
- package/cli/core/license.js +534 -0
- package/cli/core/manifest.js +243 -0
- package/cli/core/packetseal.js +591 -0
- package/cli/core/registryArtifact.js +49 -0
- package/cli/core/revocation.js +539 -0
- package/cli/core/rfc3161.js +389 -0
- package/cli/core/timestamp.js +482 -0
- package/cli/core/trust-asof.js +479 -0
- package/cli/dataset.js +2950 -0
- package/cli/evidence.js +2227 -0
- package/cli/fulfill-webhook-http.js +438 -0
- package/cli/git.js +220 -0
- package/cli/hash.js +550 -0
- package/cli/identity.js +1072 -0
- package/cli/journal-cli.js +1110 -0
- package/cli/journal-log.js +454 -0
- package/cli/journal.js +334 -0
- package/cli/lineage.js +447 -0
- package/cli/list.js +287 -0
- package/cli/parcel.js +1509 -0
- package/cli/proof.js +578 -0
- package/cli/prove.js +300 -0
- package/cli/receipt.js +631 -0
- package/cli/registry.js +331 -0
- package/cli/reputation.js +344 -0
- package/cli/revocation.js +495 -0
- package/cli/serve-verify-http.js +298 -0
- package/cli/serve-verify.js +333 -0
- package/cli/show.js +339 -0
- package/cli/verify.js +383 -0
- package/cli/vh.js +3927 -0
- package/docs/ADOPT.md +183 -0
- package/docs/ADOPTION.json +11 -0
- package/docs/AGENTTRACE.md +247 -0
- package/docs/ANCHORING.md +167 -0
- package/docs/AUDIT.md +55 -0
- package/docs/CONFORMANCE.md +107 -0
- package/docs/DATALEDGER.md +638 -0
- package/docs/DECIDE.md +47 -0
- package/docs/DECISIONS-PENDING.md +27 -0
- package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
- package/docs/ENGINE-LEDGER.json +12 -0
- package/docs/EVIDENCE.md +519 -0
- package/docs/GO-LIVE.md +66 -0
- package/docs/IDENTITY.md +123 -0
- package/docs/INDEPENDENT-VERIFICATION.md +377 -0
- package/docs/INTEGRITY-JOURNAL.md +337 -0
- package/docs/KEY-LIFECYCLE.md +179 -0
- package/docs/LICENSING.md +46 -0
- package/docs/LINEAGE.md +307 -0
- package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
- package/docs/LOOP-HARDENING-PLAN.md +44 -0
- package/docs/MERKLE-LEAVES.md +113 -0
- package/docs/METRICS.jsonl +31 -0
- package/docs/MORNING.md +204 -0
- package/docs/PILOT.md +444 -0
- package/docs/PROOFPARCEL.md +227 -0
- package/docs/PROOFS.md +262 -0
- package/docs/RECEIPTS.md +341 -0
- package/docs/REPUTATION.md +158 -0
- package/docs/SDK.md +301 -0
- package/docs/STRATEGY-ARCHIVE.md +5055 -0
- package/docs/SUPERVISOR-RUNBOOK.md +52 -0
- package/docs/TRUST-BOUNDARIES.md +335 -0
- package/docs/TRUSTLEDGER.md +1976 -0
- package/docs/USAGE-BUDGET.json +121 -0
- package/docs/VERIFY-SERVICE.md +168 -0
- package/index.js +160 -0
- package/package.json +41 -0
- package/trustledger/build-standalone.js +796 -0
- package/trustledger/cli.js +3179 -0
- package/trustledger/close.js +391 -0
- package/trustledger/corpus.js +159 -0
- package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
- package/trustledger/dist/trustledger-standalone.html +6197 -0
- package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
- package/trustledger/door-core.js +442 -0
- package/trustledger/fixtures/bank.csv +7 -0
- package/trustledger/fixtures/bank.malformed.csv +3 -0
- package/trustledger/fixtures/bank.noalias.csv +5 -0
- package/trustledger/fixtures/bank.ofx +34 -0
- package/trustledger/fixtures/bank.real.csv +5 -0
- package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
- package/trustledger/fixtures/e2e/bank.csv +4 -0
- package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
- package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
- package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.csv +6 -0
- package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
- package/trustledger/fixtures/plans/baseline.json +25 -0
- package/trustledger/fixtures/plans/price-binding.example.json +27 -0
- package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
- package/trustledger/fixtures/policy/baseline.json +19 -0
- package/trustledger/fixtures/policy/ca-example.json +12 -0
- package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
- package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
- package/trustledger/fixtures/quickbooks.csv +7 -0
- package/trustledger/fixtures/quickbooks.real.csv +5 -0
- package/trustledger/fixtures/rentroll.csv +6 -0
- package/trustledger/fixtures/rentroll.real.csv +4 -0
- package/trustledger/ingest.js +1163 -0
- package/trustledger/lib/policy-bundled-loader.js +44 -0
- package/trustledger/lib/sha256-vendored.js +227 -0
- package/trustledger/license.js +563 -0
- package/trustledger/match.js +551 -0
- package/trustledger/plans.js +551 -0
- package/trustledger/policy.js +398 -0
- package/trustledger/public/index.html +512 -0
- package/trustledger/reconcile.js +1486 -0
- package/trustledger/report.js +887 -0
- package/trustledger/seal.js +854 -0
- package/trustledger/server.js +391 -0
- 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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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>
|