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,350 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// TrustLedger — valueProof (T-45.1).
|
|
4
|
+
//
|
|
5
|
+
// A PURE, OFFLINE, DETERMINISTIC read over the gate's ALREADY-COMPUTED verdict
|
|
6
|
+
// that converts a pilot. It takes:
|
|
7
|
+
//
|
|
8
|
+
// * `model` — a buildPacket reconciliation packet (it carries the
|
|
9
|
+
// already-computed `triage` root-cause rollup, `counts`, and
|
|
10
|
+
// the PASS/FAIL `pass` flag). valueProof CONSUMES that
|
|
11
|
+
// rollup; it NEVER re-derives, re-classifies, or re-runs the
|
|
12
|
+
// engine, and it NEVER mutates the model.
|
|
13
|
+
// * `manualClose` — the broker's OWN asserted result for the SAME period: a
|
|
14
|
+
// manual close they already signed off on. The pilot's
|
|
15
|
+
// highest-signal input is a month the broker manually
|
|
16
|
+
// reconciled and called CLEAN, so the assertion we diff
|
|
17
|
+
// against is `manualClose.assertedClean` (did the manual
|
|
18
|
+
// process flag ANY out-of-trust / data finding for this
|
|
19
|
+
// period?). It MAY also carry an OPTIONAL
|
|
20
|
+
// `assertedNetCents` — the integer-cents net figure the
|
|
21
|
+
// manual close signed off on — which is ECHOED to annotate
|
|
22
|
+
// the result and is NEVER used to change a verdict, severity,
|
|
23
|
+
// count, or the outcome (consistent with this module's
|
|
24
|
+
// read-only posture).
|
|
25
|
+
//
|
|
26
|
+
// and returns a structured "what your manual close missed" result — the count +
|
|
27
|
+
// total abs-cents dollar impact of every finding the gate produced that the
|
|
28
|
+
// manual close did not flag, partitioned by the EXISTING triage root-cause
|
|
29
|
+
// classes, PLUS an explicit outcome:
|
|
30
|
+
//
|
|
31
|
+
// * "out_of_trust_missed" — the gate found >=1 genuine out-of-trust finding the
|
|
32
|
+
// manual close called clean. THE WTP CASE: the dollar
|
|
33
|
+
// figure is the conversion/commingling the manual close
|
|
34
|
+
// let through.
|
|
35
|
+
// * "data_gap_only" — the gate found NO out-of-trust finding but COULD NOT
|
|
36
|
+
// fully reconcile/classify the data (data_completeness
|
|
37
|
+
// gaps). NOT (yet) evidence the money is gone — a
|
|
38
|
+
// data-shape gap to fix and re-run, surfaced honestly so
|
|
39
|
+
// a clean-vs-missed claim is never overstated.
|
|
40
|
+
// * "clean_confirmed" — the gate AGREES with the manual close: no out-of-trust
|
|
41
|
+
// finding and no data gap. The broker now has a signed,
|
|
42
|
+
// independent, one-command proof of a clean trust account
|
|
43
|
+
// to hand their auditor (the recurring-deliverable value).
|
|
44
|
+
//
|
|
45
|
+
// HONEST LIABILITY POSTURE. valueProof asserts NOTHING the gate did not already
|
|
46
|
+
// assert. Every number it reports is read VERBATIM off `model.triage` (the SAME
|
|
47
|
+
// rollup the verdict/--json/HTML packet shows); it adds no new severity, no new
|
|
48
|
+
// finding, no new verdict, and no new exit-code rule. It is a presentation lens
|
|
49
|
+
// for a go-to-market conversation, not a second opinion on the books.
|
|
50
|
+
//
|
|
51
|
+
// NO new dependency. No fs/http/clock/crypto/random. Order-independent (it folds
|
|
52
|
+
// `model.triage.classes`, which reconcile.triage already emits deterministically).
|
|
53
|
+
|
|
54
|
+
// A dedicated error so a malformed model/assertion is a LOUD, typed failure
|
|
55
|
+
// rather than a silent miscount — the same strict-input discipline the rest of
|
|
56
|
+
// this core uses.
|
|
57
|
+
class ValueProofError extends Error {
|
|
58
|
+
constructor(message) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "ValueProofError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The three outcomes a value-proof can have. A CLOSED enum: the load-time guard
|
|
65
|
+
// below proves every triage root-cause class maps into exactly one of these, so
|
|
66
|
+
// a newly-added triage class can never silently fall through to a wrong outcome.
|
|
67
|
+
const VALUE_OUTCOME = Object.freeze({
|
|
68
|
+
OUT_OF_TRUST: "out_of_trust_missed",
|
|
69
|
+
DATA_GAP: "data_gap_only",
|
|
70
|
+
CLEAN_CONFIRMED: "clean_confirmed",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// The triage root-cause classes, mirrored here as a CLOSED set so this module's
|
|
74
|
+
// exhaustiveness guard does not depend on importing reconcile internals (it
|
|
75
|
+
// imports the public enum below and asserts the two agree). Keeping the names
|
|
76
|
+
// local keeps valueproof.js a pure presentation lens with no engine coupling
|
|
77
|
+
// beyond the public triage contract.
|
|
78
|
+
const reconcile = require("./reconcile");
|
|
79
|
+
const ROOT_CAUSE_CLASS = reconcile.ROOT_CAUSE_CLASS;
|
|
80
|
+
|
|
81
|
+
// How each triage root-cause class maps to a value-proof outcome WHEN it is the
|
|
82
|
+
// MOST-URGENT class present. The outcome is decided by the single highest-urgency
|
|
83
|
+
// class the gate found (out_of_trust dominates data_completeness dominates the
|
|
84
|
+
// benign review/timing notes), so this table is keyed by that class:
|
|
85
|
+
//
|
|
86
|
+
// * out_of_trust => OUT_OF_TRUST (a missed genuine shortage)
|
|
87
|
+
// * data_completeness => DATA_GAP (the tool could not fully reconcile)
|
|
88
|
+
// * needs_review => CLEAN_CONFIRMED (a benign note; not a missed finding,
|
|
89
|
+
// not a data gap — the account is not shown out of trust
|
|
90
|
+
// and the data reconciled)
|
|
91
|
+
// * timing => CLEAN_CONFIRMED (a self-clearing reconciling item)
|
|
92
|
+
//
|
|
93
|
+
// Built on a NULL prototype: this is keyed by our own ROOT_CAUSE_CLASS values
|
|
94
|
+
// (never untrusted input), but mirrors reconcile's null-proto discipline so a
|
|
95
|
+
// stray prototype-name key can never resolve to an inherited garbage outcome.
|
|
96
|
+
const OUTCOME_OF_CLASS = Object.freeze(
|
|
97
|
+
Object.assign(Object.create(null), {
|
|
98
|
+
[ROOT_CAUSE_CLASS.OUT_OF_TRUST]: VALUE_OUTCOME.OUT_OF_TRUST,
|
|
99
|
+
[ROOT_CAUSE_CLASS.DATA_COMPLETENESS]: VALUE_OUTCOME.DATA_GAP,
|
|
100
|
+
[ROOT_CAUSE_CLASS.NEEDS_REVIEW]: VALUE_OUTCOME.CLEAN_CONFIRMED,
|
|
101
|
+
[ROOT_CAUSE_CLASS.TIMING]: VALUE_OUTCOME.CLEAN_CONFIRMED,
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// LOAD-TIME EXHAUSTIVENESS GUARD over the triage classes. Proves, on require:
|
|
106
|
+
// 1. EVERY ROOT_CAUSE_CLASS member has an OUTCOME_OF_CLASS mapping (no triage
|
|
107
|
+
// class falls through unclassified into a wrong/undefined outcome), and
|
|
108
|
+
// 2. EVERY mapped outcome is a real VALUE_OUTCOME member (no typo'd target).
|
|
109
|
+
// Any violation is a BUILD error thrown at module load — so adding a new triage
|
|
110
|
+
// class without deciding its value-proof outcome breaks the build, never silently
|
|
111
|
+
// mis-buckets a finding in a customer-facing pilot number.
|
|
112
|
+
(function assertOutcomeExhaustive() {
|
|
113
|
+
const outcomeValues = new Set(Object.values(VALUE_OUTCOME));
|
|
114
|
+
for (const cls of Object.values(ROOT_CAUSE_CLASS)) {
|
|
115
|
+
if (!Object.prototype.hasOwnProperty.call(OUTCOME_OF_CLASS, cls)) {
|
|
116
|
+
throw new ValueProofError(
|
|
117
|
+
`valueProof: triage root-cause class "${cls}" has no value-proof outcome mapping`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const outcome = OUTCOME_OF_CLASS[cls];
|
|
121
|
+
if (!outcomeValues.has(outcome)) {
|
|
122
|
+
throw new ValueProofError(
|
|
123
|
+
`valueProof: triage class "${cls}" maps to unknown value outcome "${outcome}"`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
})();
|
|
128
|
+
|
|
129
|
+
// Format integer cents as a dollar string ("$1,234.56") for the headline. Mirrors
|
|
130
|
+
// reconcile.fmtCentsForDetail's grouping locally so this module takes NO new
|
|
131
|
+
// dependency on report.js. Deterministic; throws on non-integer (no float money).
|
|
132
|
+
function fmtCents(cents) {
|
|
133
|
+
if (!Number.isInteger(cents)) {
|
|
134
|
+
throw new ValueProofError("valueProof: dollar impact must be integer cents");
|
|
135
|
+
}
|
|
136
|
+
const neg = cents < 0;
|
|
137
|
+
const abs = Math.abs(cents);
|
|
138
|
+
const dollars = Math.floor(abs / 100);
|
|
139
|
+
const rem = abs % 100;
|
|
140
|
+
const grouped = String(dollars).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
141
|
+
const body = `$${grouped}.${String(rem).padStart(2, "0")}`;
|
|
142
|
+
return neg ? `-${body}` : body;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// "1 finding" / "2 findings" — a deterministically-pluralized count noun.
|
|
146
|
+
function countNoun(n, noun) {
|
|
147
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Read + validate the triage rollup carried on the model. We require the model to
|
|
151
|
+
// ALREADY carry a `triage` object (buildPacket -> reconcile.triage). valueProof
|
|
152
|
+
// does NOT re-run triage: the whole point is that the numbers a pilot reads EQUAL
|
|
153
|
+
// the verdict the gate already produced, so a re-derivation that drifted would
|
|
154
|
+
// defeat it. A model with no triage is a typed error, not a silent re-compute.
|
|
155
|
+
function readTriage(model) {
|
|
156
|
+
if (!model || typeof model !== "object") {
|
|
157
|
+
throw new ValueProofError("valueProof requires a reconciliation model");
|
|
158
|
+
}
|
|
159
|
+
const t = model.triage;
|
|
160
|
+
if (!t || typeof t !== "object" || !Array.isArray(t.classes)) {
|
|
161
|
+
throw new ValueProofError(
|
|
162
|
+
"valueProof requires model.triage (run buildPacket / reconcile.triage first)"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (!t.totals || !Number.isInteger(t.totals.count) || !Number.isInteger(t.totals.absImpact)) {
|
|
166
|
+
throw new ValueProofError("valueProof: model.triage.totals must carry integer count/absImpact");
|
|
167
|
+
}
|
|
168
|
+
return t;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Validate the broker's manual-close assertion. The pilot's highest-signal input
|
|
172
|
+
// is a period the broker ALREADY closed and signed off as clean, so the assertion
|
|
173
|
+
// we diff against is the boolean `assertedClean`. We require it explicitly (no
|
|
174
|
+
// defaulting) so a caller can never accidentally diff against an UNSTATED baseline
|
|
175
|
+
// and have the result silently read as "clean confirmed".
|
|
176
|
+
//
|
|
177
|
+
// `assertedNetCents` is OPTIONAL: the integer-cents net figure the manual close
|
|
178
|
+
// signed off on. It is ECHOED to ANNOTATE the result only; it NEVER changes the
|
|
179
|
+
// outcome, a verdict, a severity, a count, or any dollar number read off triage.
|
|
180
|
+
// When present it must be an integer (no float money); a non-integer is a typed
|
|
181
|
+
// error, not a silently-coerced figure.
|
|
182
|
+
function readManualClose(manualClose) {
|
|
183
|
+
if (!manualClose || typeof manualClose !== "object") {
|
|
184
|
+
throw new ValueProofError("valueProof requires a manualClose assertion object");
|
|
185
|
+
}
|
|
186
|
+
if (typeof manualClose.assertedClean !== "boolean") {
|
|
187
|
+
throw new ValueProofError(
|
|
188
|
+
"valueProof: manualClose.assertedClean must be a boolean (did the manual close flag any finding?)"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
let assertedNetCents = null;
|
|
192
|
+
if (manualClose.assertedNetCents != null) {
|
|
193
|
+
if (!Number.isInteger(manualClose.assertedNetCents)) {
|
|
194
|
+
throw new ValueProofError(
|
|
195
|
+
"valueProof: manualClose.assertedNetCents must be integer cents when provided (no float money)"
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
assertedNetCents = manualClose.assertedNetCents;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
assertedClean: manualClose.assertedClean,
|
|
202
|
+
assertedNetCents,
|
|
203
|
+
period: manualClose.period == null ? null : String(manualClose.period),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// valueProof(model, manualClose) — the pure "what your manual close missed" diff.
|
|
208
|
+
//
|
|
209
|
+
// Returns a NEW object (model is NEVER mutated):
|
|
210
|
+
// {
|
|
211
|
+
// outcome: "out_of_trust_missed" | "data_gap_only" | "clean_confirmed",
|
|
212
|
+
// period: <string|null>, // echoed from manualClose
|
|
213
|
+
// manualCloseClean: <bool>, // the broker's asserted baseline (assertedClean)
|
|
214
|
+
// assertedNetCents: <int|null>, // echoed annotation only; never changes a verdict
|
|
215
|
+
// missedFindings: {
|
|
216
|
+
// count: <int>, // == model.triage.totals.count
|
|
217
|
+
// absImpact: <int cents>, // == model.triage.totals.absImpact
|
|
218
|
+
// byClass: [ { class, label, count, absImpact }, ... ], // == triage.classes
|
|
219
|
+
// },
|
|
220
|
+
// outOfTrust: <bool>, // == model.triage.outOfTrust
|
|
221
|
+
// dataGap: <bool>, // == model.triage.dataIncomplete
|
|
222
|
+
// topClass: <ROOT_CAUSE_CLASS|null>, // == model.triage.topClass
|
|
223
|
+
// agrees: <bool>, // does the gate agree with the close?
|
|
224
|
+
// headline: <string>, // ONE sentence for the human
|
|
225
|
+
// }
|
|
226
|
+
//
|
|
227
|
+
// EVERY count/dollar number is read VERBATIM off model.triage — the function
|
|
228
|
+
// classifies the OUTCOME and writes a sentence; it computes no new money figure.
|
|
229
|
+
function valueProof(model, manualClose) {
|
|
230
|
+
const t = readTriage(model);
|
|
231
|
+
const mc = readManualClose(manualClose);
|
|
232
|
+
|
|
233
|
+
// The per-class rollup, copied VERBATIM from triage (a fresh array of fresh
|
|
234
|
+
// rows so the returned object shares no reference with the model — guaranteeing
|
|
235
|
+
// valueProof cannot mutate the model even via a returned-and-edited row).
|
|
236
|
+
const byClass = t.classes.map((c) => ({
|
|
237
|
+
class: c.class,
|
|
238
|
+
label: c.label,
|
|
239
|
+
count: c.count,
|
|
240
|
+
absImpact: c.absImpact,
|
|
241
|
+
}));
|
|
242
|
+
|
|
243
|
+
// Pull the booleans + totals straight off the model's triage. These are the
|
|
244
|
+
// SAME flags the verdict line reads; valueProof never recomputes them.
|
|
245
|
+
const outOfTrust = t.outOfTrust === true;
|
|
246
|
+
const dataGap = t.dataIncomplete === true;
|
|
247
|
+
const topClass = t.topClass == null ? null : t.topClass;
|
|
248
|
+
const count = t.totals.count;
|
|
249
|
+
const absImpact = t.totals.absImpact;
|
|
250
|
+
|
|
251
|
+
// The outcome is decided by the MOST-URGENT class the gate found, which is
|
|
252
|
+
// exactly `topClass` (reconcile.triage already rank-sorts classes most-urgent
|
|
253
|
+
// first, so classes[0].class === topClass). When there is no finding at all
|
|
254
|
+
// (topClass === null) the gate found nothing — clean confirmed. Routing
|
|
255
|
+
// through OUTCOME_OF_CLASS (guarded exhaustive above) means a newly-added
|
|
256
|
+
// triage class cannot silently mis-route.
|
|
257
|
+
let outcome;
|
|
258
|
+
if (topClass === null) {
|
|
259
|
+
outcome = VALUE_OUTCOME.CLEAN_CONFIRMED;
|
|
260
|
+
} else {
|
|
261
|
+
const mapped = OUTCOME_OF_CLASS[topClass];
|
|
262
|
+
if (mapped === undefined) {
|
|
263
|
+
// Unreachable for the built-in classes (the load-time guard proves it);
|
|
264
|
+
// defends a forged/hand-built model.triage carrying an unknown topClass.
|
|
265
|
+
throw new ValueProofError(
|
|
266
|
+
`valueProof: triage topClass "${topClass}" has no value-proof outcome`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
outcome = mapped;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Does the gate AGREE with the broker's manual close? The manual close asserted
|
|
273
|
+
// CLEAN (assertedClean === true) iff it flagged nothing; the gate agrees when
|
|
274
|
+
// its outcome is clean_confirmed. So agreement is (assertedClean === gateClean).
|
|
275
|
+
const gateClean = outcome === VALUE_OUTCOME.CLEAN_CONFIRMED;
|
|
276
|
+
const agrees = mc.assertedClean === gateClean;
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
outcome,
|
|
280
|
+
period: mc.period,
|
|
281
|
+
manualCloseClean: mc.assertedClean,
|
|
282
|
+
assertedNetCents: mc.assertedNetCents,
|
|
283
|
+
missedFindings: { count, absImpact, byClass },
|
|
284
|
+
outOfTrust,
|
|
285
|
+
dataGap,
|
|
286
|
+
topClass,
|
|
287
|
+
agrees,
|
|
288
|
+
headline: buildHeadline(outcome, mc, byClass, { count, absImpact }),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Build the ONE plain-English sentence the human reads to decide whether to keep
|
|
293
|
+
// selling. PURE. It leads with the outcome and quotes ONLY numbers already in the
|
|
294
|
+
// triage rollup. The liability posture is honest: a data_gap is NEVER framed as a
|
|
295
|
+
// missed shortage, and a clean_confirmed never overstates beyond "the gate agrees."
|
|
296
|
+
function buildHeadline(outcome, mc, byClass, totals) {
|
|
297
|
+
const baseline = mc.assertedClean
|
|
298
|
+
? "Your manual close signed this period off as clean"
|
|
299
|
+
: "Your manual close flagged this period";
|
|
300
|
+
|
|
301
|
+
if (outcome === VALUE_OUTCOME.OUT_OF_TRUST) {
|
|
302
|
+
// The most-urgent class is out_of_trust; quote ITS count/impact specifically
|
|
303
|
+
// (the WTP figure), read verbatim from the rollup.
|
|
304
|
+
const row = byClass.find((c) => c.class === ROOT_CAUSE_CLASS.OUT_OF_TRUST);
|
|
305
|
+
const c = row || { count: 0, absImpact: 0 };
|
|
306
|
+
return (
|
|
307
|
+
`${baseline}, but the gate found ${countNoun(c.count, "out-of-trust finding")} ` +
|
|
308
|
+
`totaling ${fmtCents(c.absImpact)} the manual close let through. ` +
|
|
309
|
+
`Restore the trust account before relying on this period.`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (outcome === VALUE_OUTCOME.DATA_GAP) {
|
|
314
|
+
const row = byClass.find((c) => c.class === ROOT_CAUSE_CLASS.DATA_COMPLETENESS);
|
|
315
|
+
const c = row || { count: 0, absImpact: 0 };
|
|
316
|
+
return (
|
|
317
|
+
`${baseline}, and the gate found NO out-of-trust finding — but it could not ` +
|
|
318
|
+
`fully reconcile your data (${countNoun(c.count, "item")} totaling ${fmtCents(c.absImpact)}). ` +
|
|
319
|
+
`Resolve these data gaps and re-run; this is not (yet) evidence the money is gone.`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// clean_confirmed: the gate agrees there is nothing out of trust and the data
|
|
324
|
+
// reconciled. Whether the broker's manual close called it clean (agreement) or
|
|
325
|
+
// flagged it (the gate clears items the broker queued for review) is stated
|
|
326
|
+
// honestly without claiming a missed shortage.
|
|
327
|
+
const noted =
|
|
328
|
+
totals.count > 0
|
|
329
|
+
? ` ${countNoun(totals.count, "item")} remain as benign review/timing notes for a human to confirm.`
|
|
330
|
+
: "";
|
|
331
|
+
if (mc.assertedClean) {
|
|
332
|
+
return (
|
|
333
|
+
`The gate AGREES with your manual close: no out-of-trust finding and the data ` +
|
|
334
|
+
`reconciled. This is a signed, independent confirmation of a clean trust account.` +
|
|
335
|
+
noted
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return (
|
|
339
|
+
`Your manual close flagged this period, but the gate found nothing out of trust ` +
|
|
340
|
+
`and the data reconciled.` +
|
|
341
|
+
noted
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
valueProof,
|
|
347
|
+
ValueProofError,
|
|
348
|
+
VALUE_OUTCOME,
|
|
349
|
+
OUTCOME_OF_CLASS,
|
|
350
|
+
};
|