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,398 @@
1
+ "use strict";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // trustledger/policy.js — versioned, strictly-validated per-state trust-rule
5
+ // policy + a PURE applyPolicy() that overrides exception severities.
6
+ // ---------------------------------------------------------------------------
7
+ //
8
+ // WHY THIS EXISTS
9
+ // ---------------
10
+ // reconcile.js ships a single, hard-coded DEFAULT_SEVERITY baseline. But what
11
+ // counts as "out of trust" vs. "needs a human eye" is a matter of the STATE's
12
+ // trust-account statute (e.g. some states make an owner draw against tenant
13
+ // money a per-se ERROR; others treat an NSF reversal as merely a warning until
14
+ // it is re-deposited). A policy lets a CPA/broker pin each exception type to the
15
+ // severity their jurisdiction's rule demands, and to CITE the statute so the
16
+ // control is defensible in an audit.
17
+ //
18
+ // HONEST POSTURE / DISCLAIMER
19
+ // ---------------------------
20
+ // A policy file is an AID to reconciliation. Editing severities here does NOT
21
+ // make the result legal advice and does NOT discharge the broker's duty as the
22
+ // responsible legal custodian of trust funds, nor does it replace a CPA's
23
+ // review. The shipped fixtures are DRAFT / NOT-LEGAL-ADVICE skeletons a
24
+ // qualified human edits. This module makes NO claim of regulatory compliance.
25
+ //
26
+ // DESIGN PROPERTIES
27
+ // -----------------
28
+ // * PURE: readPolicy/validatePolicy/applyPolicy have no clock, no I/O, no
29
+ // hidden state; the same inputs always produce byte-identical output.
30
+ // LOADING this module executes NO impure require at all (browser-portable);
31
+ // the ONLY filesystem code — the bundled-fixture loader — is isolated in
32
+ // ./lib/policy-bundled-loader and required LAZILY, only when a caller
33
+ // actually asks for bundled policies (see the loader section below).
34
+ // * STRICT: a wrong schemaVersion, an unknown exception type key, a severity
35
+ // not in {info,warning,error}, or a malformed toleranceCents is a NAMED
36
+ // hard error — never a silent no-op or partial accept.
37
+ // * GROUNDED IN reconcile.js: the legal exception type strings and severity
38
+ // values are REUSED from EXCEPTION/SEVERITY, so a typo'd type is a
39
+ // validation error rather than a silently-ignored key.
40
+
41
+ const { EXCEPTION, SEVERITY, compareExceptions } = require("./reconcile");
42
+
43
+ // Bump only on an INCOMPATIBLE schema change. readPolicy rejects anything else.
44
+ const SCHEMA_VERSION = 1;
45
+
46
+ // The set of legal exception type strings, derived from reconcile.js (NOT
47
+ // re-declared here) so the two can never drift. Because it is enum-derived, a
48
+ // new engine exception type (e.g. `ambiguous_deposit`) becomes an accepted
49
+ // `severities`/`citations` key automatically — no re-listing here — so a state
50
+ // can grade it (escalate the default WARNING to a hard ERROR) the day the
51
+ // engine learns to detect it.
52
+ const EXCEPTION_TYPES = Object.freeze(new Set(Object.values(EXCEPTION)));
53
+ // The set of legal severity strings, likewise derived.
54
+ const SEVERITY_VALUES = Object.freeze(new Set(Object.values(SEVERITY)));
55
+
56
+ class PolicyError extends Error {
57
+ constructor(message) {
58
+ super(message);
59
+ this.name = "PolicyError";
60
+ }
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // readPolicy(textOrObj) -> validated, frozen policy object
65
+ // ---------------------------------------------------------------------------
66
+ //
67
+ // Accepts either a JSON string (e.g. a fixture file's contents) or an already
68
+ // parsed plain object. Parsing and validation are separated so callers can
69
+ // validate an in-memory object without serializing it. PURE: no file I/O here;
70
+ // the caller reads the file and passes the text.
71
+ function readPolicy(input) {
72
+ let obj = input;
73
+ if (typeof input === "string") {
74
+ try {
75
+ obj = JSON.parse(input);
76
+ } catch (e) {
77
+ throw new PolicyError(`policy is not valid JSON: ${e.message}`);
78
+ }
79
+ }
80
+ return validatePolicy(obj);
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // validatePolicy(obj) -> validated, frozen policy object
85
+ // ---------------------------------------------------------------------------
86
+ //
87
+ // Strictly validates and returns a NEW frozen, canonical policy object. Throws
88
+ // PolicyError on the first defect found. Never mutates the input.
89
+ function validatePolicy(obj) {
90
+ if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
91
+ throw new PolicyError("policy must be a JSON object");
92
+ }
93
+
94
+ // ---- schemaVersion: must be present and exactly the supported integer ----
95
+ if (!Object.prototype.hasOwnProperty.call(obj, "schemaVersion")) {
96
+ throw new PolicyError("policy is missing required field: schemaVersion");
97
+ }
98
+ if (obj.schemaVersion !== SCHEMA_VERSION) {
99
+ throw new PolicyError(
100
+ `unsupported policy schemaVersion ${JSON.stringify(
101
+ obj.schemaVersion
102
+ )}; this build understands schemaVersion ${SCHEMA_VERSION}`
103
+ );
104
+ }
105
+
106
+ // ---- state: a non-empty human label --------------------------------------
107
+ if (typeof obj.state !== "string" || obj.state.trim() === "") {
108
+ throw new PolicyError("policy.state must be a non-empty string label");
109
+ }
110
+
111
+ // ---- severities: type -> severity override map ---------------------------
112
+ if (!Object.prototype.hasOwnProperty.call(obj, "severities")) {
113
+ throw new PolicyError("policy is missing required field: severities");
114
+ }
115
+ if (
116
+ obj.severities === null ||
117
+ typeof obj.severities !== "object" ||
118
+ Array.isArray(obj.severities)
119
+ ) {
120
+ throw new PolicyError("policy.severities must be an object map");
121
+ }
122
+ const severities = {};
123
+ for (const key of Object.keys(obj.severities)) {
124
+ if (!EXCEPTION_TYPES.has(key)) {
125
+ throw new PolicyError(
126
+ `policy.severities has unknown exception type ${JSON.stringify(key)}; ` +
127
+ `legal types are: ${[...EXCEPTION_TYPES].sort().join(", ")}`
128
+ );
129
+ }
130
+ const val = obj.severities[key];
131
+ if (!SEVERITY_VALUES.has(val)) {
132
+ throw new PolicyError(
133
+ `policy.severities[${JSON.stringify(key)}] has invalid severity ` +
134
+ `${JSON.stringify(val)}; must be one of: ` +
135
+ `${[...SEVERITY_VALUES].sort().join(", ")}`
136
+ );
137
+ }
138
+ severities[key] = val;
139
+ }
140
+
141
+ // ---- citations: optional type -> statute/rule string map -----------------
142
+ // Carried into the report so each severity override is grounded in the rule
143
+ // it rests on. A citation for a type NOT present in severities is rejected:
144
+ // citing a rule you do not actually apply is misleading in an audit.
145
+ const citations = {};
146
+ if (Object.prototype.hasOwnProperty.call(obj, "citations")) {
147
+ if (
148
+ obj.citations === null ||
149
+ typeof obj.citations !== "object" ||
150
+ Array.isArray(obj.citations)
151
+ ) {
152
+ throw new PolicyError("policy.citations must be an object map");
153
+ }
154
+ for (const key of Object.keys(obj.citations)) {
155
+ if (!EXCEPTION_TYPES.has(key)) {
156
+ throw new PolicyError(
157
+ `policy.citations has unknown exception type ${JSON.stringify(key)}`
158
+ );
159
+ }
160
+ if (!Object.prototype.hasOwnProperty.call(severities, key)) {
161
+ throw new PolicyError(
162
+ `policy.citations[${JSON.stringify(key)}] cites a rule for a type ` +
163
+ "with no severity override; cite only the overrides you apply"
164
+ );
165
+ }
166
+ const cite = obj.citations[key];
167
+ if (typeof cite !== "string" || cite.trim() === "") {
168
+ throw new PolicyError(
169
+ `policy.citations[${JSON.stringify(key)}] must be a non-empty string`
170
+ );
171
+ }
172
+ citations[key] = cite;
173
+ }
174
+ }
175
+
176
+ // ---- toleranceCents: optional non-negative integer cents -----------------
177
+ let toleranceCents;
178
+ if (Object.prototype.hasOwnProperty.call(obj, "toleranceCents")) {
179
+ const t = obj.toleranceCents;
180
+ if (!Number.isInteger(t) || t < 0) {
181
+ throw new PolicyError(
182
+ `policy.toleranceCents must be a non-negative integer (cents); got ` +
183
+ `${JSON.stringify(t)}`
184
+ );
185
+ }
186
+ toleranceCents = t;
187
+ }
188
+
189
+ // Build a canonical, frozen result. Keys are inserted in a fixed order so the
190
+ // object's own enumeration order is deterministic regardless of input order.
191
+ const out = { schemaVersion: SCHEMA_VERSION, state: obj.state };
192
+ out.severities = Object.freeze(sortedMap(severities));
193
+ out.citations = Object.freeze(sortedMap(citations));
194
+ if (toleranceCents !== undefined) out.toleranceCents = toleranceCents;
195
+ return Object.freeze(out);
196
+ }
197
+
198
+ // Return a new object with the same entries in sorted-key order (deterministic
199
+ // enumeration; pure).
200
+ function sortedMap(m) {
201
+ const out = {};
202
+ for (const k of Object.keys(m).sort()) out[k] = m[k];
203
+ return out;
204
+ }
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // applyPolicy(reconcileResult, policy) -> NEW reconcile-shaped result
208
+ // ---------------------------------------------------------------------------
209
+ //
210
+ // Returns a NEW result whose exceptions have their `severity` replaced by the
211
+ // policy override when one is present for that type, and a `citation` attached
212
+ // when the policy supplies one. Records, amounts, labels, details, and balances
213
+ // are left untouched. Deterministic and side-effect-free: the input result is
214
+ // not mutated.
215
+ //
216
+ // RE-SORT AFTER ESCALATION. reconcile.js sorts its exceptions errors-first, and
217
+ // the HTML/CSV renderers (and the human reading the signed packet) rely on that
218
+ // order so an out-of-trust ERROR sits at the top. Because a policy can ESCALATE
219
+ // a warning/info row to ERROR (or de-escalate), the input order is no longer
220
+ // valid for the new severities. We re-apply reconcile's exact stable comparator
221
+ // (compareExceptions, imported — not re-implemented — so the two cannot drift)
222
+ // so a freshly-escalated ERROR re-sorts to the top exactly as a natively
223
+ // detected one would. Order-only; the verdict/counts are order-independent.
224
+ //
225
+ // When `policy` is null/undefined the INPUT is returned UNCHANGED (same object
226
+ // reference) — the no-policy path is byte-for-byte today's DEFAULT_SEVERITY
227
+ // baseline behaviour.
228
+ function applyPolicy(reconcileResult, policy) {
229
+ if (policy === null || policy === undefined) {
230
+ return reconcileResult;
231
+ }
232
+ // Defensive: a caller must hand us a validated policy. Re-validate cheaply by
233
+ // checking the shape we depend on rather than trusting a foreign object.
234
+ if (
235
+ typeof policy !== "object" ||
236
+ policy.severities === null ||
237
+ typeof policy.severities !== "object"
238
+ ) {
239
+ throw new PolicyError("applyPolicy requires a validated policy object");
240
+ }
241
+ if (
242
+ reconcileResult === null ||
243
+ typeof reconcileResult !== "object" ||
244
+ !Array.isArray(reconcileResult.exceptions)
245
+ ) {
246
+ throw new PolicyError(
247
+ "applyPolicy requires a reconcile result with an exceptions array"
248
+ );
249
+ }
250
+
251
+ const severities = policy.severities;
252
+ const citations = policy.citations || {};
253
+
254
+ const exceptions = reconcileResult.exceptions.map((ex) => {
255
+ const hasOverride = Object.prototype.hasOwnProperty.call(
256
+ severities,
257
+ ex.type
258
+ );
259
+ const hasCitation = Object.prototype.hasOwnProperty.call(
260
+ citations,
261
+ ex.type
262
+ );
263
+ // No override and no citation: pass the exception through unchanged. We
264
+ // still return a shallow copy so the result is a fresh tree (no aliasing
265
+ // back into the input's array), keeping applyPolicy side-effect-free.
266
+ const next = {
267
+ type: ex.type,
268
+ severity: hasOverride ? severities[ex.type] : ex.severity,
269
+ amount: ex.amount,
270
+ label: ex.label,
271
+ detail: ex.detail,
272
+ records: ex.records,
273
+ };
274
+ if (hasCitation) next.citation = citations[ex.type];
275
+ return next;
276
+ });
277
+
278
+ // Re-sort under the NEW (possibly escalated) severities, using reconcile's
279
+ // own stable comparator. .sort is in-place on our freshly-built array (the
280
+ // input result and its exceptions array are untouched), so this stays pure.
281
+ exceptions.sort(compareExceptions);
282
+
283
+ return {
284
+ balances: reconcileResult.balances,
285
+ tiesOut: reconcileResult.tiesOut,
286
+ exceptions,
287
+ };
288
+ }
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // Bundled per-state fixture policies + `--state <code>` resolution.
292
+ // ---------------------------------------------------------------------------
293
+ //
294
+ // The product ships a small set of DRAFT / NOT-LEGAL-ADVICE skeleton policies
295
+ // under trustledger/fixtures/policy. `vh trust reconcile --state <code>` lets a
296
+ // broker pick one WITHOUT having to point at a file path, by naming the policy's
297
+ // `state` label (or, equivalently, the fixture filename). This is the ONLY part
298
+ // of this module that touches the filesystem, and it reads only from the
299
+ // package's own bundled fixtures directory — never a caller path — so the result
300
+ // stays deterministic and the rest of the module stays pure.
301
+ //
302
+ // ISOLATED IMPURE SEAM (T-65.1). The actual fs/path calls live in ONE separate
303
+ // module, ./lib/policy-bundled-loader, and that module is required LAZILY
304
+ // inside bundledPolicies() — never at this file's top level. So loading
305
+ // policy.js on the browser path executes no impure require, and a bundler can
306
+ // shim the loader (e.g. inline the fixture JSON) without touching the pure
307
+ // functions above. Validation, sorting, and PolicyError naming all remain HERE,
308
+ // so the loader is raw I/O only and the observable behavior is byte-identical
309
+ // to when the fs calls were inline.
310
+
311
+ // Normalize a state code/label for comparison: lowercase, collapse runs of
312
+ // non-alphanumerics to a single space, trim. So "California", "california",
313
+ // and "CALIFORNIA " all resolve alike, and a verbose label like
314
+ // "EXAMPLE-STATE (illustrative override)" can be addressed by its leading code.
315
+ function normStateCode(s) {
316
+ return String(s == null ? "" : s)
317
+ .toLowerCase()
318
+ .replace(/[^a-z0-9]+/g, " ")
319
+ .trim();
320
+ }
321
+
322
+ // List the bundled fixture policies as { code, file, policy } entries, where
323
+ // `code` is the fixture filename without ".json". Validates each on load so a
324
+ // shipped fixture that drifts out of schema is a hard, named error rather than a
325
+ // silent miss. Deterministic (filenames are sorted).
326
+ function bundledPolicies() {
327
+ let names;
328
+ try {
329
+ // LAZY require of the isolated impure seam: fs/path load only when a
330
+ // caller actually asks for bundled policies, never when policy.js loads.
331
+ names = require("./lib/policy-bundled-loader").listBundledPolicyNames();
332
+ } catch (e) {
333
+ throw new PolicyError(`cannot read bundled policy directory: ${e.message}`);
334
+ }
335
+ return names
336
+ .sort()
337
+ .map((file) => {
338
+ let full;
339
+ let policy;
340
+ try {
341
+ const loaded = require("./lib/policy-bundled-loader").readBundledPolicyFile(file);
342
+ full = loaded.full;
343
+ policy = readPolicy(loaded.text);
344
+ } catch (e) {
345
+ throw new PolicyError(`bundled policy ${file} is invalid: ${e.message}`);
346
+ }
347
+ return { code: file.replace(/\.json$/, ""), file: full, policy };
348
+ });
349
+ }
350
+
351
+ // Resolve a `--state <code>` to a validated bundled policy. A code matches when
352
+ // it equals (after normalization) EITHER the fixture filename code OR the
353
+ // policy's `state` label. An unknown code is a clear PolicyError that lists the
354
+ // codes that ARE available, so the usage error is actionable.
355
+ function resolveState(code) {
356
+ const want = normStateCode(code);
357
+ if (want === "") {
358
+ throw new PolicyError("--state requires a non-empty state code");
359
+ }
360
+ const all = bundledPolicies();
361
+ for (const entry of all) {
362
+ if (
363
+ normStateCode(entry.code) === want ||
364
+ normStateCode(entry.policy.state) === want
365
+ ) {
366
+ return entry.policy;
367
+ }
368
+ }
369
+ const codes = all.map((e) => e.code).sort().join(", ");
370
+ throw new PolicyError(
371
+ `unknown --state "${code}"; bundled states are: ${codes}`
372
+ );
373
+ }
374
+
375
+ module.exports = {
376
+ SCHEMA_VERSION,
377
+ PolicyError,
378
+ readPolicy,
379
+ validatePolicy,
380
+ applyPolicy,
381
+ // bundled per-state fixtures + --state resolution
382
+ bundledPolicies,
383
+ resolveState,
384
+ normStateCode,
385
+ // exported for focused tests / reuse
386
+ EXCEPTION_TYPES,
387
+ SEVERITY_VALUES,
388
+ };
389
+
390
+ // BUNDLED_DIR stays on the export surface (same value as always) but resolves
391
+ // LAZILY through the isolated loader, so exporting it does not drag fs/path
392
+ // into the browser path at load time.
393
+ Object.defineProperty(module.exports, "BUNDLED_DIR", {
394
+ enumerable: true,
395
+ get() {
396
+ return require("./lib/policy-bundled-loader").BUNDLED_DIR;
397
+ },
398
+ });