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,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
+ };