@simpleapps-com/augur-web 2026.11.1 → 2026.11.3

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 (167) hide show
  1. package/EXPORTS.md +1 -1
  2. package/README.md +7 -2
  3. package/dist/add-to-cart-button.cjs +3 -2
  4. package/dist/add-to-cart-button.cjs.map +1 -1
  5. package/dist/add-to-cart-button.js +2 -1
  6. package/dist/add-to-cart-button.js.map +1 -1
  7. package/dist/bin-scan.d.ts +23 -0
  8. package/dist/bin-scan.d.ts.map +1 -0
  9. package/dist/button.d.ts +1 -1
  10. package/dist/{chunk-32XB3REY.cjs → chunk-6SSR5Z6X.cjs} +8 -5
  11. package/dist/chunk-6SSR5Z6X.cjs.map +1 -0
  12. package/dist/{chunk-RCUKSVMF.js → chunk-6USFUHWW.js} +9 -14
  13. package/dist/chunk-6USFUHWW.js.map +1 -0
  14. package/dist/{chunk-WJUDOEVB.cjs → chunk-7RDA26CL.cjs} +7 -7
  15. package/dist/{chunk-WJUDOEVB.cjs.map → chunk-7RDA26CL.cjs.map} +1 -1
  16. package/dist/{chunk-UMNTUD2P.js → chunk-AC3OCGDU.js} +6 -8
  17. package/dist/chunk-AC3OCGDU.js.map +1 -0
  18. package/dist/{chunk-MOC5N2N4.js → chunk-BSRLAXZ2.js} +9 -6
  19. package/dist/chunk-BSRLAXZ2.js.map +1 -0
  20. package/dist/{chunk-UB7HYAAC.js → chunk-EBCKH6JE.js} +6 -3
  21. package/dist/chunk-EBCKH6JE.js.map +1 -0
  22. package/dist/{chunk-WFMP6TZN.js → chunk-I6LMIYVF.js} +3 -3
  23. package/dist/{chunk-RGJT67PN.js → chunk-IDCDOE2L.js} +6 -3
  24. package/dist/chunk-IDCDOE2L.js.map +1 -0
  25. package/dist/{chunk-MXSQYVCK.js → chunk-IXQFZJSN.js} +7 -3
  26. package/dist/chunk-IXQFZJSN.js.map +1 -0
  27. package/dist/{chunk-76RSVRGB.cjs → chunk-KVJQIZSM.cjs} +10 -15
  28. package/dist/chunk-KVJQIZSM.cjs.map +1 -0
  29. package/dist/{chunk-OQV27VC5.cjs → chunk-L7JN7ED7.cjs} +37 -37
  30. package/dist/chunk-L7JN7ED7.cjs.map +1 -0
  31. package/dist/{chunk-JXW4XE2P.cjs → chunk-N57EZBPE.cjs} +10 -7
  32. package/dist/chunk-N57EZBPE.cjs.map +1 -0
  33. package/dist/chunk-NBXA42AW.cjs +534 -0
  34. package/dist/chunk-NBXA42AW.cjs.map +1 -0
  35. package/dist/chunk-NSMOT7GD.js +534 -0
  36. package/dist/chunk-NSMOT7GD.js.map +1 -0
  37. package/dist/{chunk-BXMTQGKV.cjs → chunk-OJS2B6U2.cjs} +7 -3
  38. package/dist/chunk-OJS2B6U2.cjs.map +1 -0
  39. package/dist/{chunk-5VMEEKZ5.cjs → chunk-SJGSBMMN.cjs} +7 -9
  40. package/dist/chunk-SJGSBMMN.cjs.map +1 -0
  41. package/dist/{chunk-PFEPE5UE.js → chunk-TEDFINZP.js} +2 -2
  42. package/dist/{chunk-YTBB63IE.cjs → chunk-VCFKYOVH.cjs} +3 -3
  43. package/dist/{chunk-YTBB63IE.cjs.map → chunk-VCFKYOVH.cjs.map} +1 -1
  44. package/dist/{chunk-Z57QBVUC.cjs → chunk-VXVOYZBN.cjs} +8 -5
  45. package/dist/chunk-VXVOYZBN.cjs.map +1 -0
  46. package/dist/{chunk-JWTTXZDS.js → chunk-YJDVFOVR.js} +40 -40
  47. package/dist/chunk-YJDVFOVR.js.map +1 -0
  48. package/dist/command.cjs +3 -3
  49. package/dist/command.cjs.map +1 -1
  50. package/dist/command.d.ts.map +1 -1
  51. package/dist/command.js +2 -2
  52. package/dist/command.js.map +1 -1
  53. package/dist/confirm-dialog.cjs +3 -2
  54. package/dist/confirm-dialog.cjs.map +1 -1
  55. package/dist/confirm-dialog.js +2 -1
  56. package/dist/confirm-dialog.js.map +1 -1
  57. package/dist/data-table.cjs +65 -46
  58. package/dist/data-table.cjs.map +1 -1
  59. package/dist/data-table.d.ts.map +1 -1
  60. package/dist/data-table.js +59 -40
  61. package/dist/data-table.js.map +1 -1
  62. package/dist/detail-panel.cjs +71 -63
  63. package/dist/detail-panel.cjs.map +1 -1
  64. package/dist/detail-panel.d.ts.map +1 -1
  65. package/dist/detail-panel.js +70 -62
  66. package/dist/detail-panel.js.map +1 -1
  67. package/dist/dropdown-menu.cjs +3 -2
  68. package/dist/dropdown-menu.cjs.map +1 -1
  69. package/dist/dropdown-menu.d.ts.map +1 -1
  70. package/dist/dropdown-menu.js +2 -1
  71. package/dist/financing-widget.cjs +4 -1
  72. package/dist/financing-widget.cjs.map +1 -1
  73. package/dist/financing-widget.d.ts.map +1 -1
  74. package/dist/financing-widget.js +4 -1
  75. package/dist/financing-widget.js.map +1 -1
  76. package/dist/form-field.cjs +3 -2
  77. package/dist/form-field.cjs.map +1 -1
  78. package/dist/form-field.d.ts.map +1 -1
  79. package/dist/form-field.js +2 -1
  80. package/dist/form-input.cjs +4 -3
  81. package/dist/form-input.cjs.map +1 -1
  82. package/dist/form-input.d.ts.map +1 -1
  83. package/dist/form-input.js +3 -2
  84. package/dist/form-select.cjs +5 -4
  85. package/dist/form-select.cjs.map +1 -1
  86. package/dist/form-select.js +4 -3
  87. package/dist/form-textarea.cjs +4 -3
  88. package/dist/form-textarea.cjs.map +1 -1
  89. package/dist/form-textarea.d.ts.map +1 -1
  90. package/dist/form-textarea.js +3 -2
  91. package/dist/gtm.cjs +3 -3
  92. package/dist/gtm.js +2 -2
  93. package/dist/pagination.cjs +3 -3
  94. package/dist/pagination.d.ts +1 -1
  95. package/dist/pagination.d.ts.map +1 -1
  96. package/dist/pagination.js +2 -2
  97. package/dist/payment-modal.cjs +52 -34
  98. package/dist/payment-modal.cjs.map +1 -1
  99. package/dist/payment-modal.d.ts +4 -4
  100. package/dist/payment-modal.d.ts.map +1 -1
  101. package/dist/payment-modal.js +52 -34
  102. package/dist/payment-modal.js.map +1 -1
  103. package/dist/popover.cjs +4 -4
  104. package/dist/popover.cjs.map +1 -1
  105. package/dist/popover.js +3 -3
  106. package/dist/popover.js.map +1 -1
  107. package/dist/price.cjs +2 -2
  108. package/dist/price.js +1 -1
  109. package/dist/provider.cjs +3 -3
  110. package/dist/provider.js +2 -2
  111. package/dist/quantity-input.cjs +125 -111
  112. package/dist/quantity-input.cjs.map +1 -1
  113. package/dist/quantity-input.d.ts +16 -5
  114. package/dist/quantity-input.d.ts.map +1 -1
  115. package/dist/quantity-input.js +127 -113
  116. package/dist/quantity-input.js.map +1 -1
  117. package/dist/resource-tabs.cjs +10 -5
  118. package/dist/resource-tabs.cjs.map +1 -1
  119. package/dist/resource-tabs.d.ts +1 -1
  120. package/dist/resource-tabs.d.ts.map +1 -1
  121. package/dist/resource-tabs.js +10 -5
  122. package/dist/resource-tabs.js.map +1 -1
  123. package/dist/rules.d.ts +315 -17
  124. package/dist/rules.d.ts.map +1 -1
  125. package/dist/select.cjs +3 -2
  126. package/dist/select.cjs.map +1 -1
  127. package/dist/select.d.ts.map +1 -1
  128. package/dist/select.js +2 -1
  129. package/dist/separator.cjs +8 -9
  130. package/dist/separator.cjs.map +1 -1
  131. package/dist/separator.d.ts.map +1 -1
  132. package/dist/separator.js +7 -8
  133. package/dist/separator.js.map +1 -1
  134. package/dist/session-replay.cjs +2 -2
  135. package/dist/session-replay.js +1 -1
  136. package/dist/spinner.cjs +3 -2
  137. package/dist/spinner.cjs.map +1 -1
  138. package/dist/spinner.d.ts +1 -1
  139. package/dist/spinner.d.ts.map +1 -1
  140. package/dist/spinner.js +2 -1
  141. package/dist/stock-label.cjs +28 -33
  142. package/dist/stock-label.cjs.map +1 -1
  143. package/dist/stock-label.d.ts.map +1 -1
  144. package/dist/stock-label.js +30 -35
  145. package/dist/stock-label.js.map +1 -1
  146. package/llms.txt +3 -2
  147. package/package.json +9 -7
  148. package/dist/chunk-32XB3REY.cjs.map +0 -1
  149. package/dist/chunk-5VMEEKZ5.cjs.map +0 -1
  150. package/dist/chunk-76RSVRGB.cjs.map +0 -1
  151. package/dist/chunk-BXMTQGKV.cjs.map +0 -1
  152. package/dist/chunk-EEJYDSXR.cjs +0 -220
  153. package/dist/chunk-EEJYDSXR.cjs.map +0 -1
  154. package/dist/chunk-JWTTXZDS.js.map +0 -1
  155. package/dist/chunk-JXW4XE2P.cjs.map +0 -1
  156. package/dist/chunk-MOC5N2N4.js.map +0 -1
  157. package/dist/chunk-MXSQYVCK.js.map +0 -1
  158. package/dist/chunk-OQV27VC5.cjs.map +0 -1
  159. package/dist/chunk-RCUKSVMF.js.map +0 -1
  160. package/dist/chunk-RGJT67PN.js.map +0 -1
  161. package/dist/chunk-UB7HYAAC.js.map +0 -1
  162. package/dist/chunk-UMNTUD2P.js.map +0 -1
  163. package/dist/chunk-VI2IE6RC.js +0 -220
  164. package/dist/chunk-VI2IE6RC.js.map +0 -1
  165. package/dist/chunk-Z57QBVUC.cjs.map +0 -1
  166. /package/dist/{chunk-WFMP6TZN.js.map → chunk-I6LMIYVF.js.map} +0 -0
  167. /package/dist/{chunk-PFEPE5UE.js.map → chunk-TEDFINZP.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../foundation/src/decimal.ts","../../foundation/src/chars.ts","../../utils/src/strings.ts","../../utils/src/fixed.ts","../../utils/src/lists.ts","../src/bin-scan.ts","../../utils/src/replace.ts","../../utils/src/json-node.ts","../src/rules.ts"],"sourcesContent":["//@ backend dafny\n\n/* Verified with LemmaScript → Dafny (`pnpm proofs`; hand lemmas in decimal.dfy; model:\n models/Platform.dfy Decimal; link: models/links/foundation-decimal.dfy): how every app\n writes an id as text. Used by services/agr-int-mcp (customer grant ids `<site>|<usersUid>`) and\n packages/joomla-auth (staff grant ids `staff-<Joomla user id>`), so both are one-to-one\n by the same proof. */\n\n/**\n * The decimal digits of `n`, exactly what `String(n)` gives for a non-negative integer\n * (tests pin the two together). Recursive so specs can use it. Injective: different numbers\n * are written differently (lemma DecimalInjective, decimal.dfy; Platform.DecimalInjective).\n *\n * @ensures |result| ≥ 1 ∧ ∀ k < |result|. result[k] ∈ '0'..'9'\n */\nexport function decimal(n: number): string {\n //@ verify\n //@ type n nat\n //@ requires n >= 0\n //@ decreases n\n //@ ensures \\result.length > 0\n //@ ensures forall(k: nat, k < \\result.length ==> (\\result.charCodeAt(k) >= 48 && \\result.charCodeAt(k) <= 57))\n const last = \"0123456789\".slice(n % 10, (n % 10) + 1);\n return n < 10 ? last : decimal(Math.floor(n / 10)) + last;\n}\n\n/**\n * Any integer as `String(n)` writes it: a minus for a negative, then `decimal`'s digits\n * (tests pin it to `String` for safe integers). An id written this way can't change a path.\n *\n * @ensures result is digits, after a \"-\" exactly when n < 0 — never empty, so an id written\n * this way can't change a path; exactly the model's Platform.IntText in the link (a contract\n * naming `decimal` couldn't be resolved by an importer, models/LSC-GAPS.md, so importers that\n * need the exact text take the link's equality as a premise)\n */\nexport function intText(n: number): string {\n //@ verify\n //@ type n int\n //@ ensures \\result.length > 0\n //@ ensures n >= 0 ==> forall(k: nat, k < \\result.length ==> (\\result.charCodeAt(k) >= 48 && \\result.charCodeAt(k) <= 57))\n //@ ensures n < 0 ==> (\\result.length > 1 && \\result.charCodeAt(0) === 45 && forall(k: nat, (1 <= k && k < \\result.length) ==> (\\result.charCodeAt(k) >= 48 && \\result.charCodeAt(k) <= 57)))\n return n < 0 ? \"-\" + decimal(-n) : decimal(n);\n}\n","//@ backend dafny\n\n/* Character classes over UTF-16 code units (what charCodeAt returns), verified with\n LemmaScript → Dafny (model: models/Foundation.dfy; link: models/links/foundation-chars.dfy).\n Every contract is an iff, so an importer, which sees only the contract, knows each class\n exactly. */\n\n/**\n * A decimal digit: 0-9.\n *\n * @ensures result ⟺ c ∈ [48, 57]\n */\nexport function isDigit(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> (c >= 48 && c <= 57)\n return c >= 48 && c <= 57;\n}\n\n/**\n * A JS line terminator (what regex `.` doesn't match): LF, CR, LS, PS.\n *\n * @ensures result ⟺ c ∈ {10, 13, 8232, 8233}\n */\nexport function isLineTerminator(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> (c === 10 || c === 13 || c === 8232 || c === 8233)\n return c === 10 || c === 13 || c === 8232 || c === 8233;\n}\n\n/**\n * The code ASCII-lowered: A-Z to a-z, any other value (-1 from `codeAt` included) unchanged.\n *\n * @ensures exactly the model's LowerCode\n */\nexport function lowerCode(c: number): number {\n //@ verify\n //@ ensures \\result === (c >= 65 && c <= 90 ? c + 32 : c)\n return c >= 65 && c <= 90 ? c + 32 : c;\n}\n/**\n * A lowercase ASCII letter: a-z.\n *\n * @ensures result ⟺ c ∈ [97, 122]\n */\nexport function isLowerAlphaChar(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> (c >= 97 && c <= 122)\n return c >= 97 && c <= 122;\n}\n\n/**\n * An ASCII letter or digit: A-Z a-z 0-9.\n *\n * @ensures result ⟺ c ∈ [A-Z] ∪ [a-z] ∪ [0-9]\n */\nexport function isAlnumChar(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> ((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57))\n return (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57);\n}\n\n/**\n * A lowercase hex digit: 0-9 a-f.\n *\n * @ensures result ⟺ c ∈ [0-9] ∪ [a-f]\n */\nexport function isLowerHexChar(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> ((c >= 48 && c <= 57) || (c >= 97 && c <= 102))\n return (c >= 48 && c <= 57) || (c >= 97 && c <= 102);\n}\n\n/**\n * A hex digit in either case: 0-9 a-f A-F.\n *\n * @ensures result ⟺ c ∈ [0-9] ∪ [a-f] ∪ [A-F]\n */\nexport function isHexChar(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> ((c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70))\n return (c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70);\n}\n\n/**\n * A base64url character (RFC 4648 §5): A-Z a-z 0-9 - _.\n *\n * @ensures result ⟺ c ∈ [A-Z] ∪ [a-z] ∪ [0-9] ∪ {\"-\", \"_\"} — never \".\" or \"=\"\n */\nexport function isBase64UrlChar(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> ((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 45 || c === 95)\n return (\n (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 45 || c === 95\n );\n}\n\n/**\n * JS whitespace, the regex `\\s` and what `trim()` strips: tab, LF, VT, FF, CR, space, NBSP,\n * U+1680, U+2000-U+200A, LS, PS, U+202F, U+205F, U+3000 and the BOM.\n *\n * @ensures result ⟺ c ∈ {9..13, 32, 160, 5760, 8192..8202, 8232, 8233, 8239, 8287, 12288, 65279}\n */\nexport function isJsSpace(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> ((c >= 9 && c <= 13) || c === 32 || c === 160 || c === 5760 || (c >= 8192 && c <= 8202) || c === 8232 || c === 8233 || c === 8239 || c === 8287 || c === 12288 || c === 65279)\n return isAsciiSpace(c) || isGeneralPunctuationSpace(c) || isOtherSpace(c);\n}\n\n/**\n * ASCII whitespace: tab, LF, VT, FF, CR and space.\n *\n * @ensures result ⟺ c ∈ [9, 13] ∪ {32}\n */\nfunction isAsciiSpace(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> ((c >= 9 && c <= 13) || c === 32)\n return (c >= 9 && c <= 13) || c === 32;\n}\n\n/**\n * The spaces in the General Punctuation block: U+2000-U+200A, LS, PS, U+202F, U+205F.\n *\n * @ensures result ⟺ c ∈ [8192, 8202] ∪ {8232, 8233, 8239, 8287}\n */\nfunction isGeneralPunctuationSpace(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> ((c >= 8192 && c <= 8202) || c === 8232 || c === 8233 || c === 8239 || c === 8287)\n return (c >= 8192 && c <= 8202) || c === 8232 || c === 8233 || c === 8239 || c === 8287;\n}\n\n/**\n * The other spaces: NBSP, U+1680 (Ogham), U+3000 (ideographic) and the BOM.\n *\n * @ensures result ⟺ c ∈ {160, 5760, 12288, 65279}\n */\nfunction isOtherSpace(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> (c === 160 || c === 5760 || c === 12288 || c === 65279)\n return c === 160 || c === 5760 || c === 12288 || c === 65279;\n}\n\n/**\n * A word character, the regex `\\w`: A-Z a-z 0-9 _.\n *\n * @ensures result ⟺ c ∈ [A-Z] ∪ [a-z] ∪ [0-9] ∪ {\"_\"}\n */\nexport function isWordChar(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> ((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95)\n return (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95;\n}\n\n/**\n * A snake_case character: a-z 0-9 _.\n *\n * @ensures result ⟺ c ∈ [a-z] ∪ [0-9] ∪ {\"_\"}\n */\nexport function isSnakeChar(c: number): boolean {\n //@ verify\n //@ ensures \\result <==> ((c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95)\n return (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95;\n}\n","//@ backend dafny\nimport {\n isAlnumChar,\n isBase64UrlChar,\n isDigit,\n isHexChar,\n isJsSpace,\n isLineTerminator,\n isLowerHexChar,\n isSnakeChar,\n isWordChar,\n lowerCode,\n} from \"@atlas/foundation/chars\";\n\n/* String scans and slices, verified with LemmaScript → Dafny (hand lemmas in strings.dfy;\n model: models/Utils.dfy; link: models/links/utils-strings.dfy). Recursive and\n expression-bodied so specs can use them, and each contract pins the result exactly, so\n an importer (which sees only the contract) knows what it returns. Two of them stand in\n for constructs lsc mistranslates: `prefix` for an unclamped `slice`, and the code-unit\n scans for \"\\r\" / \"\\n\" literals. */\n\n/**\n * The code unit at `i`, or -1 when `i` is outside `s`. JS `charCodeAt` gives NaN there,\n * which no code equals, so a scan's own bounds test would change no result and no test\n * could tell it from none (an equivalent mutant); with the bound here once, the boundary is\n * observable (tests pin -1) and the scans that use it carry no bound of their own. A code\n * unit is never negative, so -1 matches no `code` a caller passes (`requires code >= 0`).\n *\n * @ensures 0 ≤ i < |s| ⟹ result = s[i]; otherwise result = -1\n */\nexport function codeAt(s: string, i: number): number {\n //@ verify\n //@ ensures (i >= 0 && i < s.length) ==> \\result === s.charCodeAt(i)\n //@ ensures !(i >= 0 && i < s.length) ==> \\result === -1\n return i >= 0 && i < s.length ? s.charCodeAt(i) : -1;\n}\n\n/**\n * Every character of `s` from index `i` on is base64url.\n *\n * @ensures result ⟺ ∀ k ∈ [i, |s|). isBase64UrlChar(s[k])\n */\nexport function base64UrlFrom(s: string, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases s.length - i\n //@ ensures \\result <==> forall(k: nat, (i <= k && k < s.length) ==> isBase64UrlChar(s.charCodeAt(k)))\n return i >= s.length || (isBase64UrlChar(s.charCodeAt(i)) && base64UrlFrom(s, i + 1));\n}\n\n/**\n * The base64url digit for `v` (RFC 4648 §5): A-Z a-z 0-9 - _.\n *\n * @ensures exactly the model's Base64UrlDigit\n */\nexport function base64UrlDigit(v: number): string {\n //@ verify\n //@ type v nat\n //@ requires v >= 0 && v < 64\n //@ ensures \\result === \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\".slice(v, v + 1)\n return \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\".slice(v, v + 1);\n}\n\n/**\n * Bytes from `i` on as unpadded base64url: each three as four digits, a last one or two as two or\n * three (JS `btoa` with `+/` as `-_` and the `=` dropped). One frame per three bytes: a JWT part\n * is a few hundred.\n *\n * @ensures exactly the model's Base64UrlFrom (links/utils-strings.dfy)\n */\nexport function encodeBase64UrlFrom(b: number[], i: number): string {\n //@ verify\n //@ type i nat\n //@ requires i >= 0 && forall(k: nat, k < b.length ==> (b[k] >= 0 && b[k] < 256))\n //@ decreases b.length - i\n //@ ensures i >= b.length ==> \\result === \"\"\n if (i >= b.length) return \"\";\n const x = b[i] as number;\n const first = base64UrlDigit(Math.floor(x / 4));\n if (i + 1 === b.length) return first + base64UrlDigit((x % 4) * 16);\n const y = b[i + 1] as number;\n const second = base64UrlDigit((x % 4) * 16 + Math.floor(y / 16));\n if (i + 2 === b.length) return first + second + base64UrlDigit((y % 16) * 4);\n const z = b[i + 2] as number;\n return (\n first +\n second +\n base64UrlDigit((y % 16) * 4 + Math.floor(z / 64)) +\n base64UrlDigit(z % 64) +\n encodeBase64UrlFrom(b, i + 3)\n );\n}\n\n/**\n * Every character of `s` from index `i` on is lowercase hex.\n *\n * @ensures result ⟺ ∀ k ∈ [i, |s|). isLowerHexChar(s[k])\n */\nexport function lowerHexFrom(s: string, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases s.length - i\n //@ ensures \\result <==> forall(k: nat, (i <= k && k < s.length) ==> isLowerHexChar(s.charCodeAt(k)))\n return i >= s.length || (isLowerHexChar(s.charCodeAt(i)) && lowerHexFrom(s, i + 1));\n}\n\n/**\n * Every character of `s` from index `i` on is hex, either case.\n *\n * @ensures result ⟺ ∀ k ∈ [i, |s|). isHexChar(s[k])\n */\nexport function hexFrom(s: string, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases s.length - i\n //@ ensures \\result <==> forall(k: nat, (i <= k && k < s.length) ==> isHexChar(s.charCodeAt(k)))\n return i >= s.length || (isHexChar(s.charCodeAt(i)) && hexFrom(s, i + 1));\n}\n\n/**\n * Every character of `s` from index `i` on is an ASCII digit (`\\d` without the `u` flag).\n *\n * @ensures result ⟺ ∀ k ∈ [i, |s|). isDigit(s[k])\n */\nexport function digitsFrom(s: string, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases s.length - i\n //@ ensures \\result <==> forall(k: nat, (i <= k && k < s.length) ==> isDigit(s.charCodeAt(k)))\n return i >= s.length || (isDigit(s.charCodeAt(i)) && digitsFrom(s, i + 1));\n}\n\n/**\n * Every character of `s` from index `i` on is a word character (`\\w`: `[A-Za-z0-9_]`).\n *\n * @ensures result ⟺ ∀ k ∈ [i, |s|). isWordChar(s[k])\n */\nexport function wordCharsFrom(s: string, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases s.length - i\n //@ ensures \\result <==> forall(k: nat, (i <= k && k < s.length) ==> isWordChar(s.charCodeAt(k)))\n return i >= s.length || (isWordChar(s.charCodeAt(i)) && wordCharsFrom(s, i + 1));\n}\n\n/**\n * Every character of `s` from index `i` on is a snake_case character (`[a-z0-9_]`).\n *\n * @ensures result ⟺ ∀ k ∈ [i, |s|). isSnakeChar(s[k])\n */\nexport function snakeCharsFrom(s: string, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases s.length - i\n //@ ensures \\result <==> forall(k: nat, (i <= k && k < s.length) ==> isSnakeChar(s.charCodeAt(k)))\n return i >= s.length || (isSnakeChar(s.charCodeAt(i)) && snakeCharsFrom(s, i + 1));\n}\n\n/**\n * Every character of `s` from index `i` on is an ASCII letter or digit (`[A-Za-z0-9]`).\n *\n * @ensures result ⟺ ∀ k ∈ [i, |s|). isAlnumChar(s[k])\n */\nexport function alnumFrom(s: string, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases s.length - i\n //@ ensures \\result <==> forall(k: nat, (i <= k && k < s.length) ==> isAlnumChar(s.charCodeAt(k)))\n return i >= s.length || (isAlnumChar(s.charCodeAt(i)) && alnumFrom(s, i + 1));\n}\n\n/**\n * Every character of `s` from index `i` on is an ASCII letter, digit or `-` (`[A-Za-z0-9-]`):\n * a domain label's tail, a service name's tail.\n *\n * @ensures result ⟺ ∀ k ∈ [i, |s|). isAlnumChar(s[k]) ∨ s[k] = \"-\" — the model's AlnumDashFrom\n */\nexport function alnumDashFrom(s: string, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases s.length - i\n //@ ensures \\result <==> forall(k: nat, (i <= k && k < s.length) ==> (isAlnumChar(s.charCodeAt(k)) || s.charCodeAt(k) === 45))\n if (i >= s.length) return true;\n const c = s.charCodeAt(i);\n return (isAlnumChar(c) || c === 45) && alnumDashFrom(s, i + 1);\n}\n\n/**\n * The last index before `end` holding `code`, or -1 (JS `s.lastIndexOf(c)` at end = |s|).\n *\n * @ensures result = -1 ∨ (0 ≤ result < end ∧ s[result] = code ∧ no `code` in (result, end))\n */\nexport function lastIndexOfCode(s: string, code: number, end: number): number {\n //@ verify\n //@ type end nat\n //@ requires 0 <= end && end <= s.length\n //@ decreases end\n //@ ensures \\result === -1 || (0 <= \\result && \\result < end)\n //@ ensures \\result >= 0 ==> s.charCodeAt(\\result) === code\n //@ ensures \\result >= 0 ==> forall(k: nat, (\\result < k && k < end) ==> s.charCodeAt(k) !== code)\n //@ ensures \\result === -1 ==> forall(k: nat, k < end ==> s.charCodeAt(k) !== code)\n if (end === 0) return -1;\n if (s.charCodeAt(end - 1) === code) return end - 1;\n return lastIndexOfCode(s, code, end - 1);\n}\n\n/**\n * No character of `s` from index `i` on is `code`.\n *\n * @ensures result ⟺ ∀ k ∈ [i, |s|). s[k] ≠ code\n */\nexport function lacksCode(s: string, code: number, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires 0 <= i && i <= s.length\n //@ decreases s.length - i\n //@ ensures \\result <==> forall(k: nat, (i <= k && k < s.length) ==> s.charCodeAt(k) !== code)\n // `===`, not `>=`: past the end a read is NaN !== code, so `>=` vs `>` would be equivalent\n return i === s.length || (s.charCodeAt(i) !== code && lacksCode(s, code, i + 1));\n}\n\n/**\n * The end of the run of `code` starting at `i`: the first index ≥ i that isn't `code`\n * (skip spaces: `codeRunEnd(header, 32, i)`).\n *\n * @requires i ≤ |s|\n * @ensures i ≤ result ≤ |s| ∧ ∀ k ∈ [i, result). s[k] = code ∧ (result < |s| ⟹ s[result] ≠ code)\n * — exactly the run's end\n */\nexport function codeRunEnd(s: string, code: number, i: number): number {\n //@ verify\n //@ type i nat\n //@ requires 0 <= i && i <= s.length\n //@ requires code >= 0\n //@ decreases s.length - i\n //@ ensures i <= \\result && \\result <= s.length\n //@ ensures forall(k: nat, (i <= k && k < \\result) ==> s.charCodeAt(k) === code)\n //@ ensures \\result < s.length ==> s.charCodeAt(\\result) !== code\n return codeAt(s, i) === code ? codeRunEnd(s, code, i + 1) : i;\n}\n\n/**\n * The first index ≥ `from` holding `code`, or -1 (`indexOf` for one code unit, so \"\\n\" is\n * passed as 10, never as a literal).\n *\n * @ensures result = -1 ∨ (from ≤ result < |s| ∧ s[result] = code), with no `code` in\n * [from, result) — the first one; result = -1 ⟹ none from `from` on\n */\nexport function indexOfCode(s: string, code: number, from: number): number {\n //@ verify\n //@ type from nat\n //@ requires code >= 0 && from >= 0\n //@ decreases s.length - from\n //@ ensures \\result === -1 || (from <= \\result && \\result < s.length)\n //@ ensures \\result >= 0 ==> s.charCodeAt(\\result) === code\n //@ ensures \\result >= 0 ==> forall(k: nat, (from <= k && k < \\result) ==> s.charCodeAt(k) !== code)\n //@ ensures \\result === -1 ==> forall(k: nat, (from <= k && k < s.length) ==> s.charCodeAt(k) !== code)\n const c = codeAt(s, from);\n if (c === code) return from;\n if (c === -1) return -1;\n return indexOfCode(s, code, from + 1);\n}\n\n/**\n * The first index ≥ `i` holding `a` or `b`, or |s| (a run either code ends: a path segment's\n * run to \"/\" or \"?\"). One scan: the lesser of two `indexOfCode` ends is the same index, but\n * its two sides are equal only when both are |s|, a boundary no test can observe.\n *\n * @requires i ≤ |s|\n * @ensures i ≤ result ≤ |s| ∧ ∀ k ∈ [i, result). s[k] ∉ {a, b} ∧ (result < |s| ⟹ s[result] ∈ {a, b})\n * — exactly the first such index\n */\nexport function eitherCodeEnd(s: string, a: number, b: number, i: number): number {\n //@ verify\n //@ type i nat\n //@ requires 0 <= i && i <= s.length\n //@ requires a >= 0 && b >= 0\n //@ decreases s.length - i\n //@ ensures i <= \\result && \\result <= s.length\n //@ ensures forall(k: nat, (i <= k && k < \\result) ==> (s.charCodeAt(k) !== a && s.charCodeAt(k) !== b))\n //@ ensures \\result < s.length ==> (s.charCodeAt(\\result) === a || s.charCodeAt(\\result) === b)\n const c = codeAt(s, i);\n if (c === -1 || c === a || c === b) return i;\n return eitherCodeEnd(s, a, b, i + 1);\n}\n\n/**\n * `s` without one trailing `code` (\"\\r\" before a \"\\n\", say); unchanged otherwise.\n *\n * @ensures s ends with code ⟹ result = s[0, |s| − 1)\n * @ensures otherwise result = s\n */\nexport function dropLast(s: string, code: number): string {\n //@ verify\n //@ requires code >= 0\n //@ ensures (s.length > 0 && s.charCodeAt(s.length - 1) === code) ==> \\result === s.slice(0, s.length - 1)\n //@ ensures !(s.length > 0 && s.charCodeAt(s.length - 1) === code) ==> \\result === s\n return codeAt(s, s.length - 1) === code ? s.slice(0, s.length - 1) : s;\n}\n\n/**\n * `s` without its leading `code` units (every one of them).\n *\n * @ensures exactly the model's LStripCode\n */\nexport function lStripCode(s: string, code: number): string {\n //@ verify\n //@ requires code >= 0\n //@ decreases s.length\n //@ ensures \\result.length <= s.length\n if (codeAt(s, 0) === code) return lStripCode(s.slice(1), code);\n return s;\n}\n\n/**\n * `s` without its trailing `code` units (every one of them).\n *\n * @ensures exactly the model's RStripCode\n */\nexport function rStripCode(s: string, code: number): string {\n //@ verify\n //@ requires code >= 0\n //@ decreases s.length\n //@ ensures \\result.length <= s.length\n if (codeAt(s, s.length - 1) === code) {\n return rStripCode(s.slice(0, s.length - 1), code);\n }\n return s;\n}\n\n/**\n * `s` without its leading and trailing `code` units, the middle kept (`/a/b/` with `/` →\n * `a/b`).\n *\n * @ensures exactly the model's StripCode — neither end is `code` (StripCodeTrims)\n */\nexport function stripCode(s: string, code: number): string {\n //@ verify\n //@ requires code >= 0\n //@ ensures \\result === rStripCode(lStripCode(s, code), code)\n return rStripCode(lStripCode(s, code), code);\n}\n\n/**\n * JS `s.startsWith(x, pos)`, which lsc mistranslates (it drops `pos`). Like JS, a `pos`\n * past the end counts as the end, so the empty string matches anywhere.\n *\n * @ensures pos ≤ |s| ⟹ (result ⟺ pos + |x| ≤ |s| ∧ s[pos, pos + |x|) = x) — so a true\n * result also gives the bounds for slicing after the match\n * @ensures pos > |s| ⟹ (result ⟺ x = \"\")\n */\nexport function startsWithAt(s: string, x: string, pos: number): boolean {\n //@ verify\n //@ type pos nat\n //@ requires pos >= 0\n //@ ensures pos <= s.length ==> (\\result <==> (pos + x.length <= s.length && s.slice(pos, pos + x.length) === x))\n //@ ensures pos > s.length ==> (\\result <==> x.length === 0)\n // Math.min, not a ternary: at pos = |s| both branches agree, so `<` vs `<=` is equivalent\n const at = Math.min(pos, s.length);\n // The slice is clamped to the end, so a too-long x differs by length: no separate fit test\n // (JS slice clamps too, which made one unobservable)\n return s.slice(at, Math.min(at + x.length, s.length)) === x;\n}\n\n/**\n * `s` with A-Z lowercased and every other character kept: what lsc's proofs take\n * `toLowerCase()` to be. JS's `toLowerCase()` also maps non-ASCII letters, some onto ASCII\n * (the Kelvin sign K, U+212A, becomes \"k\") or onto two characters (İ, U+0130), so a proved\n * function that lowercases MUST use this, or the runtime does what the proof says it\n * doesn't.\n *\n * @ensures |result| = |s| ∧ ∀ k < |s|. result[k] = (s[k] ∈ A-Z ? s[k] + 32 : s[k]) — A-Z\n * lowered, every other code unit unchanged; true in JS (a contract `=== s.toLowerCase()` held\n * only in lsc's ASCII model of the built-in, Fable review 2026-11-06). Helper-free, since an\n * importer is given only the helpers its own body calls; exactly the model's LowerAscii in\n * the link\n */\nexport function lowerAscii(s: string): string {\n //@ verify\n //@ decreases s.length\n //@ ensures \\result.length === s.length\n //@ ensures forall(k: nat, k < s.length ==> \\result.charCodeAt(k) === ((s.charCodeAt(k) >= 65 && s.charCodeAt(k) <= 90) ? s.charCodeAt(k) + 32 : s.charCodeAt(k)))\n if (s.length === 0) return \"\";\n const c = s.charCodeAt(0);\n const head =\n c >= 65 && c <= 90 ? \"abcdefghijklmnopqrstuvwxyz\".slice(c - 65, c - 64) : s.slice(0, 1);\n return head + lowerAscii(s.slice(1));\n}\n\n/**\n * `lowerAscii` as a loop: a constant stack depth (and linear time) on long text, where the\n * recursive spec takes a frame and a slice per character.\n *\n * @ensures result = lowerAscii(s)\n */\nexport function lowerAsciiText(s: string): string {\n //@ verify\n //@ ensures \\result === lowerAscii(s)\n let out = \"\";\n let i = 0;\n while (i !== s.length) {\n //@ invariant 0 <= i && i <= s.length\n //@ invariant out + lowerAscii(s.slice(i)) === lowerAscii(s)\n //@ decreases s.length - i\n const c = s.charCodeAt(i);\n out =\n out +\n (c >= 65 && c <= 90 ? \"abcdefghijklmnopqrstuvwxyz\".slice(c - 65, c - 64) : s.slice(i, i + 1));\n i = i + 1;\n }\n return out;\n}\n\n/**\n * Whether `x` from index `k` matches `s` from `pos + k`, code by code, ASCII-lowered. A code\n * past the end of `s` is -1 (`codeAt`), which no code of `x` is, so there is no bound to\n * check (a `pos + |x| ≤ |s|` guard before a slice never changed the answer at runtime, where\n * `slice` clamps: an equivalent mutant, 2026-11-02).\n *\n * @ensures exactly the model's CaselessFrom\n */\nexport function caselessFrom(s: string, x: string, pos: number, k: number): boolean {\n //@ verify\n //@ type pos nat\n //@ type k nat\n //@ requires pos >= 0 && k >= 0 && k <= x.length\n //@ decreases x.length - k\n //@ ensures k === x.length ==> \\result\n if (k === x.length) return true;\n return lowerCode(codeAt(s, pos + k)) === x.charCodeAt(k) && caselessFrom(s, x, pos, k + 1);\n}\n\n/**\n * Whether the text of `s` at `pos`, ASCII-lowered, is `x` (`x` given lowercase): a case-free\n * `startsWith`; callers carry no length guard of their own. Only `pos ≤ |s|` is checked: it\n * decides an empty `x` past the end, the one case the code-by-code test can't see.\n *\n * @ensures result ⟺ pos + |x| ≤ |s| ∧ ∀ j < |x|. lowered(s[pos + j]) = x[j] — the text at pos,\n * ASCII-lowered (A-Z + 32), is x; helper-free; exactly the model's StartsWithCaseless in the link\n */\nexport function startsWithCaseless(s: string, x: string, pos: number): boolean {\n //@ verify\n //@ type pos nat\n //@ requires pos >= 0\n //@ ensures \\result <==> (pos + x.length <= s.length && forall(j: nat, j < x.length ==> ((s.charCodeAt(pos + j) >= 65 && s.charCodeAt(pos + j) <= 90) ? s.charCodeAt(pos + j) + 32 : s.charCodeAt(pos + j)) === x.charCodeAt(j)))\n return pos <= s.length && caselessFrom(s, x, pos, 0);\n}\n\n/**\n * Whether `s` ends with `suffix` (JS one-argument `endsWith`, which lsc reads as its\n * two-argument form; moved from the augur-api generator 2026-11-01).\n *\n * @ensures result ⟺ |s| ≥ |suffix| ∧ s[|s| − |suffix|, |s|) = suffix — exactly the model's EndsWith\n */\nexport function endsWith(s: string, suffix: string): boolean {\n //@ verify\n //@ ensures \\result <==> (s.length >= suffix.length && s.slice(s.length - suffix.length) === suffix)\n // No length test: from max(|s| − |suffix|, 0), a too-long suffix compares unequal by length\n return s.slice(Math.max(s.length - suffix.length, 0)) === suffix;\n}\n\n/**\n * The order of `a` and `b` by UTF-16 code unit from index `i`, as a recursive spec: -1, 0\n * or 1 (JS `<` on strings; a proper prefix comes first). Locale-free, unlike `localeCompare`.\n *\n * @ensures result ∈ {-1, 0, 1} — exactly the model's CompareFrom\n */\nexport function compareFrom(a: string, b: string, i: number): number {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases a.length - i\n //@ ensures \\result === -1 || \\result === 0 || \\result === 1\n if (i >= a.length) return i >= b.length ? 0 : -1;\n if (i >= b.length) return 1;\n const x = a.charCodeAt(i);\n const y = b.charCodeAt(i);\n if (x < y) return -1;\n if (x > y) return 1;\n return compareFrom(a, b, i + 1);\n}\n\n/**\n * The order of two strings by UTF-16 code unit, for `sort` (what the default `sort` uses on\n * strings), so sorted output never depends on locale.\n *\n * @ensures result = compareFrom(a, b, 0)\n */\nexport function compareCodeUnits(a: string, b: string): number {\n //@ verify\n //@ ensures \\result === compareFrom(a, b, 0)\n return compareFrom(a, b, 0);\n}\n\n/**\n * JS `s.slice(0, n)`: at most `n` characters, clamped to the length. lsc reads a bare\n * `slice(0, n)` without JS's clamp, so use this instead.\n *\n * @ensures result = s[0, min(n, |s|))\n */\nexport function prefix(s: string, n: number): string {\n //@ verify\n //@ type n nat\n //@ requires n >= 0\n //@ ensures n < s.length ==> \\result === s.slice(0, n)\n //@ ensures n >= s.length ==> \\result === s\n // Math.min, not a ternary: JS slice already clamps, so a ternary's branch is unobservable\n return s.slice(0, Math.min(n, s.length));\n}\n\n/**\n * The end of the JS-whitespace (`\\s`) run starting at `i`: the first index ≥ i that is not\n * whitespace, or |s|.\n *\n * @ensures i ≤ result ≤ |s| ∧ s[i, result) all whitespace ∧ (result = |s| ∨ ¬space(s[result]))\n */\nexport function spaceRunEnd(s: string, i: number): number {\n //@ verify\n //@ type i nat\n //@ requires 0 <= i && i <= s.length\n //@ decreases s.length - i\n //@ ensures i <= \\result && \\result <= s.length\n //@ ensures forall(k: nat, (i <= k && k < \\result) ==> isJsSpace(s.charCodeAt(k)))\n //@ ensures \\result < s.length ==> !isJsSpace(s.charCodeAt(\\result))\n // codeAt is -1 at the end, which is no space: the bound needs no test of its own here\n if (!isJsSpace(codeAt(s, i))) return i;\n return spaceRunEnd(s, i + 1);\n}\n\n/**\n * The end of the ASCII-digit run starting at `i` (a regex's `\\d+`, from `i`): the first index\n * ≥ i that is not 0-9, or |s|.\n *\n * @ensures i ≤ result ≤ |s| ∧ s[i, result) all digits ∧ (result = |s| ∨ ¬digit(s[result])) —\n * exactly the model's DigitRunEnd\n */\nexport function digitRunEnd(s: string, i: number): number {\n //@ verify\n //@ type i nat\n //@ requires 0 <= i && i <= s.length\n //@ decreases s.length - i\n //@ ensures i <= \\result && \\result <= s.length\n //@ ensures forall(k: nat, (i <= k && k < \\result) ==> (s.charCodeAt(k) >= 48 && s.charCodeAt(k) <= 57))\n //@ ensures \\result < s.length ==> !(s.charCodeAt(\\result) >= 48 && s.charCodeAt(\\result) <= 57)\n // codeAt is -1 at the end, which is no digit\n if (!isDigit(codeAt(s, i))) return i;\n return digitRunEnd(s, i + 1);\n}\n\n/**\n * A natural number written canonically (`^(0|[1-9][0-9]*)$`): the keys JS objects move\n * first, ahead of their source order.\n *\n * @ensures exactly the model's CanonicalNat, stated over the codes themselves so an importer\n * can use it (a contract naming `digitsFrom` can't be resolved there, models/LSC-GAPS.md)\n */\nexport function isCanonicalNat(s: string): boolean {\n //@ verify\n //@ ensures \\result <==> (s === \"0\" || (s.length > 0 && s.charCodeAt(0) >= 49 && s.charCodeAt(0) <= 57 && forall(k: nat, (1 <= k && k < s.length) ==> (s.charCodeAt(k) >= 48 && s.charCodeAt(k) <= 57))))\n if (s === \"0\") return true;\n const first = codeAt(s, 0);\n return first >= 49 && first <= 57 && digitsFrom(s, 1);\n}\n\n/**\n * The value of a run of decimal digits, most significant first (\"\" is 0).\n *\n * @requires ∀ k ∈ [0, |s|). isDigit(s[k])\n * @ensures exactly the model's DigitsValue (proved in links/utils-strings.dfy)\n */\nexport function digitsValue(s: string): number {\n //@ verify\n //@ requires forall(k: nat, k < s.length ==> (s.charCodeAt(k) >= 48 && s.charCodeAt(k) <= 57))\n //@ decreases s.length\n //@ ensures \\result >= 0\n if (s.length === 0) return 0;\n return digitsValue(s.slice(0, s.length - 1)) * 10 + (s.charCodeAt(s.length - 1) - 48);\n}\n\n/**\n * What a number from the environment means (owner 2026-11-04: a bad one fails at start):\n * \"default\" when unset (\"\": the wrapper reads a missing variable as \"\"), \"value\" when it is\n * a canonical natural number at least `min` (its value is `digitsValue(raw)`), and \"bad\"\n * otherwise: a sign, a space, a fraction, an exponent, hex, text, or below `min`.\n *\n * @ensures exactly the model's EnvNumberOf (proved in links/utils-strings.dfy)\n */\nexport function envNumberKind(raw: string, min: number): string {\n //@ verify\n //@ type min nat\n //@ ensures raw === \"\" ==> \\result === \"default\"\n //@ ensures (raw !== \"\" && !isCanonicalNat(raw)) ==> \\result === \"bad\"\n if (raw === \"\") return \"default\";\n if (!isCanonicalNat(raw)) return \"bad\";\n return digitsValue(raw) < min ? \"bad\" : \"value\";\n}\n\n/**\n * A canonical natural number's value, 0 for any other text (a count written as JSON).\n *\n * @ensures exactly the model's CanonicalNatValue (in the link: `digitsValue` needs the digits\n * isCanonicalNat's contract gives, which a contract can't call for); 0 for other text\n */\nexport function canonicalNatValue(s: string): number {\n //@ verify\n //@ ensures \\result >= 0\n //@ ensures !isCanonicalNat(s) ==> \\result === 0\n if (!isCanonicalNat(s)) return 0;\n return digitsValue(s);\n}\n\n/**\n * The first index ≥ `i` holding a line terminator, or |s|: where a regex `.+` started at `i`\n * stops.\n *\n * @ensures i ≤ result ≤ |s| ∧ no terminator in s[i, result) ∧ (result = |s| ∨ terminator at result)\n */\nexport function lineEnd(s: string, i: number): number {\n //@ verify\n //@ type i nat\n //@ requires 0 <= i && i <= s.length\n //@ decreases s.length - i\n //@ ensures i <= \\result && \\result <= s.length\n //@ ensures forall(k: nat, (i <= k && k < \\result) ==> !isLineTerminator(s.charCodeAt(k)))\n //@ ensures \\result < s.length ==> isLineTerminator(s.charCodeAt(\\result))\n if (i === s.length || isLineTerminator(s.charCodeAt(i))) return i;\n return lineEnd(s, i + 1);\n}\n\n/**\n * The largest p with lo ≤ p < hi whose character is not a line terminator, or -1: where a\n * backtracking regex `.` can start.\n *\n * @ensures result = -1 ∨ (lo ≤ result < hi ∧ ¬terminator(s[result]) ∧ only terminators in (result, hi))\n */\nexport function lastLineCharBelow(s: string, hi: number, lo: number): number {\n //@ verify\n //@ type hi nat\n //@ type lo nat\n //@ requires 0 <= lo && 0 <= hi && hi <= s.length\n //@ decreases hi\n //@ ensures \\result === -1 || (lo <= \\result && \\result < hi)\n //@ ensures \\result >= 0 ==> !isLineTerminator(s.charCodeAt(\\result))\n //@ ensures \\result >= 0 ==> forall(k: nat, (\\result < k && k < hi) ==> isLineTerminator(s.charCodeAt(k)))\n //@ ensures \\result === -1 ==> forall(k: nat, (lo <= k && k < hi) ==> isLineTerminator(s.charCodeAt(k)))\n if (hi <= lo) return -1;\n if (!isLineTerminator(s.charCodeAt(hi - 1))) return hi - 1;\n return lastLineCharBelow(s, hi - 1, lo);\n}\n\n/**\n * Nothing but JS whitespace (`s.trim() === \"\"`).\n *\n * @ensures exactly the model's Blank\n */\nexport function isBlank(s: string): boolean {\n //@ verify\n //@ ensures \\result <==> s.trim() === \"\"\n return s.trim() === \"\";\n}\n\n/**\n * The first index ≥ `i` where `x` occurs in `s`, or -1 (JS `s.indexOf(x, i)` for i ≤ |s|).\n *\n * @ensures result = -1 ∨ (i ≤ result ∧ s[result, result + |x|) = x) — and the first such\n * index: links/utils-strings.dfy proves it is exactly the model's IndexOfFrom (lsc can't\n * give the \"no earlier match\" quantifier a trigger)\n */\nexport function indexOfFrom(s: string, x: string, i: number): number {\n //@ verify\n //@ type i nat\n //@ requires i >= 0 && i <= s.length\n //@ decreases s.length - i\n //@ ensures \\result === -1 || (i <= \\result && \\result + x.length <= s.length)\n //@ ensures \\result >= 0 ==> s.slice(\\result, \\result + x.length) === x\n // A match here, else the end, else the next index: stopping at `i === |s|` (not \"x no\n // longer fits\") leaves no bound whose slip a later, harmless step would hide\n if (startsWithAt(s, x, i)) return i;\n if (i === s.length) return -1;\n return indexOfFrom(s, x, i + 1);\n}\n\n/**\n * `indexOfFrom` as a loop: the same answer at a constant stack depth, for text long enough that\n * one frame per character would overflow the stack (a rendered page, 50–60 KB with its inlined\n * stylesheet). Specs and links use `indexOfFrom`; runtime scans over long text use this.\n *\n * @ensures result = indexOfFrom(s, x, i)\n */\nexport function findFrom(s: string, x: string, i: number): number {\n //@ verify\n //@ type i nat\n //@ requires i >= 0 && i <= s.length\n //@ ensures \\result === indexOfFrom(s, x, i)\n let k = i;\n while (k !== s.length) {\n //@ invariant i <= k && k <= s.length\n //@ invariant indexOfFrom(s, x, k) === indexOfFrom(s, x, i)\n //@ decreases s.length - k\n if (startsWithAt(s, x, k)) return k;\n k = k + 1;\n }\n return startsWithAt(s, x, k) ? k : -1;\n}\n\n/**\n * `indexOfCode` as a loop: the same answer at a constant stack depth (see `findFrom`).\n *\n * @ensures result = indexOfCode(s, code, from)\n */\nexport function findCodeFrom(s: string, code: number, from: number): number {\n //@ verify\n //@ type from nat\n //@ requires code >= 0 && from >= 0\n //@ ensures \\result === indexOfCode(s, code, from)\n let k = from;\n while (k < s.length) {\n //@ invariant from <= k\n //@ invariant indexOfCode(s, code, k) === indexOfCode(s, code, from)\n //@ decreases s.length - k\n if (s.charCodeAt(k) === code) return k;\n k = k + 1;\n }\n return -1;\n}\n\n/**\n * How many times `x` occurs in `s` from `i` on, left to right without overlaps (repeated JS\n * `s.indexOf(x, at + x.length)`).\n *\n * @requires |x| > 0\n * @ensures result ≥ 0 — exactly the model's CountFrom (links/utils-strings.dfy)\n */\nexport function countFrom(s: string, x: string, i: number): number {\n //@ verify\n //@ type i nat\n //@ requires x.length > 0 && i >= 0 && i <= s.length\n //@ decreases s.length - i\n //@ ensures \\result >= 0\n const at = indexOfFrom(s, x, i);\n if (at === -1) return 0;\n return 1 + countFrom(s, x, at + x.length);\n}\n\n/**\n * `countFrom(s, x, 0)` as a loop: a constant stack depth on long text (see `findFrom`).\n *\n * @requires |x| > 0\n * @ensures result = countFrom(s, x, 0)\n */\nexport function countOf(s: string, x: string): number {\n //@ verify\n //@ requires x.length > 0\n //@ ensures \\result === countFrom(s, x, 0)\n let n = 0;\n let i = 0;\n let at = findFrom(s, x, 0);\n while (at !== -1) {\n //@ invariant 0 <= i && i <= s.length\n //@ invariant at === indexOfFrom(s, x, i)\n //@ invariant n + countFrom(s, x, i) === countFrom(s, x, 0)\n //@ decreases s.length - i\n n = n + 1;\n i = at + x.length;\n at = findFrom(s, x, i);\n }\n return n;\n}\n\n/**\n * The text before the first `code`, or all of it when there is none (JS `s.split(c)[0]`).\n *\n * @ensures result = s[0, first code) when s holds one, else s — the model's UntilCode\n */\nexport function untilCode(s: string, code: number): string {\n //@ verify\n //@ requires code >= 0\n //@ ensures \\result === (indexOfCode(s, code, 0) >= 0 && indexOfCode(s, code, 0) <= s.length ? s.slice(0, indexOfCode(s, code, 0)) : s)\n const k = indexOfCode(s, code, 0);\n return k >= 0 ? s.slice(0, k) : s;\n}\n\n/**\n * `s` from `i` on split at every `code` (JS `s.split(c)` at i = 0, for a one-character\n * separator): always at least one part.\n *\n * @ensures |result| ≥ 1 — exactly the model's SplitCodeFrom (links/utils-strings.dfy)\n */\nexport function splitCodeFrom(s: string, code: number, i: number): string[] {\n //@ verify\n //@ type i nat\n //@ requires code >= 0 && i >= 0 && i <= s.length\n //@ decreases s.length - i\n //@ ensures \\result.length > 0\n const k = indexOfCode(s, code, i);\n if (k === -1) return [s.slice(i)];\n return [s.slice(i, k), ...splitCodeFrom(s, code, k + 1)];\n}\n\n/**\n * Every run of JS whitespace in `s` from `i` on as one space (JS `s.replace(/\\s+/g, \" \")` at\n * i = 0).\n *\n * @ensures |result| ≤ |s| − i — exactly the model's CollapseSpacesFrom (links/utils-strings.dfy)\n */\nexport function collapseSpacesFrom(s: string, i: number): string {\n //@ verify\n //@ type i nat\n //@ requires 0 <= i && i <= s.length\n //@ decreases s.length - i\n //@ ensures \\result.length <= s.length - i\n if (i === s.length) return \"\";\n if (isJsSpace(s.charCodeAt(i))) return \" \" + collapseSpacesFrom(s, spaceRunEnd(s, i));\n return s.slice(i, i + 1) + collapseSpacesFrom(s, i + 1);\n}\n\n/**\n * `collapseSpacesFrom(s, 0)` at a constant stack depth (a page's text is tens of KB).\n *\n * @ensures result = collapseSpacesFrom(s, 0)\n */\nexport function collapseSpaces(s: string): string {\n //@ verify\n //@ ensures \\result === collapseSpacesFrom(s, 0)\n let out = \"\";\n let i = 0;\n while (i < s.length) {\n //@ invariant 0 <= i && i <= s.length\n //@ invariant out + collapseSpacesFrom(s, i) === collapseSpacesFrom(s, 0)\n //@ decreases s.length - i\n if (isJsSpace(s.charCodeAt(i))) {\n out = out + \" \";\n // the run's end as a loop too: spaceRunEnd takes a frame per space (Fable review 2026-11-06)\n let e = i + 1;\n while (e !== s.length && isJsSpace(s.charCodeAt(e))) {\n //@ invariant i < e && e <= s.length\n //@ invariant forall(k: nat, (i <= k && k < e) ==> isJsSpace(s.charCodeAt(k)))\n //@ decreases s.length - e\n e = e + 1;\n }\n i = e;\n } else {\n out = out + s.slice(i, i + 1);\n i = i + 1;\n }\n }\n return out;\n}\n\n/**\n * `n` spaces.\n *\n * @ensures result = \" \" repeated n times — the model's Spaces\n */\nexport function spaces(n: number): string {\n //@ verify\n //@ type n nat\n //@ requires n >= 0\n //@ decreases n\n //@ ensures \\result.length === n\n //@ ensures forall(k: nat, k < n ==> \\result.charCodeAt(k) === 32)\n // `<= 0`, not `=== 0`: the same for a nat, and a fraction a caller slips in (outside the\n // contract) ends instead of recursing forever (Fable review 2026-11-06: spaces(0.5))\n if (n <= 0) return \"\";\n return \" \" + spaces(n - 1);\n}\n\n/**\n * A line feed, `\"\\n\"`, for proved text that spans lines: lsc emits a `\"\\n\"` escape raw, which\n * Dafny cannot parse (models/LSC-GAPS.md), so the contract pins it by code unit and the body is\n * a template literal holding the line break itself.\n *\n * @ensures result = \"\\n\" — one code unit, 10\n */\nexport function lineFeed(): string {\n //@ verify\n //@ ensures \\result.length === 1 && \\result.charCodeAt(0) === 10\n return `\n`;\n}\n\n/**\n * `s` padded on the left with spaces to `width` (JS `s.padStart(width)`).\n *\n * @ensures result = Spaces(width − |s|) + s when width > |s|, else s\n */\nexport function padLeft(s: string, width: number): string {\n //@ verify\n //@ ensures width <= s.length ==> \\result === s\n // width > |s| ⟹ spaces(width − |s|) + s: a hand ensures in strings.dfy, so importers'\n // axioms name no helper (models/LSC-GAPS.md). Math.max, not a ternary: at width = |s|\n // both branches agree (spaces(0) is \"\"), so `>` vs `>=` would be equivalent.\n return spaces(Math.max(width - s.length, 0)) + s;\n}\n\n/**\n * `s` padded on the right with spaces to `width` (JS `s.padEnd(width)`).\n *\n * @ensures result = s + Spaces(width − |s|) when width > |s|, else s\n */\nexport function padRight(s: string, width: number): string {\n //@ verify\n //@ ensures width <= s.length ==> \\result === s\n // width > |s| ⟹ s + spaces(width − |s|): a hand ensures in strings.dfy, as padLeft's.\n return s + spaces(Math.max(width - s.length, 0));\n}\n\n/**\n * An optional part of a key: `tag` then `text`, or nothing when `text` is empty, so an absent\n * part never leaves a bare tag.\n *\n * @ensures result = \"\" when text = \"\", else tag + text — exactly the model's KeyPart\n */\nexport function keyPart(tag: string, text: string): string {\n //@ verify\n //@ ensures \\result === (text === \"\" ? \"\" : tag + text)\n return text === \"\" ? \"\" : tag + text;\n}\n\n/**\n * Two texts joined by one space: the one rule behind a loader key's `<column> <direction>`, a\n * sync line's `METHOD /path` and a field's description then note.\n *\n * @ensures result = a + \" \" + b — exactly the model's SpaceJoined\n */\nexport function spaceJoined(a: string, b: string): string {\n //@ verify\n //@ ensures \\result === a + \" \" + b\n return a + \" \" + b;\n}\n\n/**\n * The first code point of `w` as text: a surrogate pair whole, else the first code unit, or \"\"\n * (Fable review 2026-11-06: an astral initial was a lone surrogate).\n *\n * @ensures exactly the model's FirstChar\n */\nexport function firstCodePoint(w: string): string {\n //@ verify\n //@ ensures \\result === ((w.length >= 2 && w.charCodeAt(0) >= 55296 && w.charCodeAt(0) <= 56319 && w.charCodeAt(1) >= 56320 && w.charCodeAt(1) <= 57343) ? w.slice(0, 2) : (w.length > 0 ? w.slice(0, 1) : \"\"))\n if (w.length >= 2) {\n const hi = w.charCodeAt(0);\n const lo = w.charCodeAt(1);\n if (hi >= 55296 && hi <= 56319 && lo >= 56320 && lo <= 57343) return w.slice(0, 2);\n }\n return w.length > 0 ? w.slice(0, 1) : \"\";\n}\n\n/**\n * An ASCII letter (A-Z, a-z) somewhere in `w` from `i` on (moved from readability.ts\n * 2026-11-06, so `readableWords`' contract can name it and importers resolve it).\n *\n * @ensures result ⟺ ∃ k ∈ [i, |w|). w[k] ∈ A-Z ∪ a-z — at i = 0 exactly the model's HasAsciiLetter\n */\nexport function hasAsciiLetterFrom(w: string, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases w.length - i\n //@ ensures \\result <==> exists(k: nat, i <= k && k < w.length && ((w.charCodeAt(k) >= 65 && w.charCodeAt(k) <= 90) || (w.charCodeAt(k) >= 97 && w.charCodeAt(k) <= 122)))\n if (i >= w.length) return false;\n const c = w.charCodeAt(i);\n if ((c >= 65 && c <= 90) || (c >= 97 && c <= 122)) return true;\n return hasAsciiLetterFrom(w, i + 1);\n}\n\n/**\n * `s` with a-z uppercased and every other character kept, the mirror of `lowerAscii`: JS's\n * `toUpperCase()` also maps non-ASCII letters (ß becomes \"SS\").\n *\n * @ensures |result| = |s| ∧ ∀ k < |s|. result[k] = (s[k] ∈ a-z ? s[k] − 32 : s[k]) — a-z\n * raised, every other code unit unchanged; exactly the model's UpperAscii in the link\n */\nexport function upperAscii(s: string): string {\n //@ verify\n //@ decreases s.length\n //@ ensures \\result.length === s.length\n //@ ensures forall(k: nat, k < s.length ==> \\result.charCodeAt(k) === ((s.charCodeAt(k) >= 97 && s.charCodeAt(k) <= 122) ? s.charCodeAt(k) - 32 : s.charCodeAt(k)))\n if (s.length === 0) return \"\";\n const c = s.charCodeAt(0);\n const head =\n c >= 97 && c <= 122 ? \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\".slice(c - 97, c - 96) : s.slice(0, 1);\n return head + upperAscii(s.slice(1));\n}\n","//@ backend dafny\nimport { decimal } from \"@atlas/foundation/decimal\";\nimport { digitsFrom, digitsValue, indexOfCode } from \"./strings\";\n\n/* Fixed-point decimals (atlas#78, 2026-11-08): a quantity with `d` decimal places held exactly\n as an integer count of 10^-d units, read from the text a person types and written back as\n `toFixed(d)` writes it, so no `parseFloat`, `Math.ceil` or `toFixed` decides one. Verified with\n LemmaScript → Dafny (`pnpm proofs`; model: models/Fixed.dfy; link:\n models/links/utils-fixed.dfy). */\n\n/**\n * 10^n.\n *\n * @ensures exactly the model's Pow10 (in the link; importers see result ≥ 1)\n */\nexport function pow10(n: number): number {\n //@ verify\n //@ type n nat\n //@ requires n >= 0\n //@ decreases n\n //@ ensures \\result >= 1\n return n === 0 ? 1 : 10 * pow10(n - 1);\n}\n\n/**\n * n zeros.\n *\n * @ensures |result| = n, every character \"0\"\n */\nexport function zeros(n: number): string {\n //@ verify\n //@ type n nat\n //@ requires n >= 0\n //@ decreases n\n //@ ensures \\result.length === n\n //@ ensures forall(k: nat, k < \\result.length ==> \\result.charCodeAt(k) === 48)\n return n === 0 ? \"\" : \"0\" + zeros(n - 1);\n}\n\n/**\n * The digits of `n`, left-padded with zeros to `width` (never cut).\n *\n * @ensures exactly the model's PaddedDigits (in the link)\n */\nexport function paddedDigits(n: number, width: number): string {\n //@ verify\n //@ type n nat\n //@ type width nat\n //@ requires n >= 0 && width >= 0\n //@ ensures \\result.length >= width\n const s = decimal(n);\n return s.length >= width ? s : zeros(width - s.length) + s;\n}\n\n/**\n * A non-negative count of 10^-d units as `toFixed(d)` writes the value: the whole part, then,\n * for d > 0, a \".\" and exactly d digits (tests pin it to `toFixed`).\n *\n * @ensures exactly the model's FixedText (in the link)\n */\nexport function fixedText(units: number, d: number): string {\n //@ verify\n //@ type units nat\n //@ type d nat\n //@ requires units >= 0 && d >= 0\n //@ ensures \\result.length > 0\n const p = pow10(d);\n const whole = decimal(Math.floor(units / p));\n return d === 0 ? whole : whole + \".\" + paddedDigits(units % p, d);\n}\n\n/**\n * Any integer count of 10^-d units: a minus for a negative one, then `fixedText` of its size.\n *\n * @ensures exactly the model's SignedFixedText (in the link)\n */\nexport function signedFixedText(units: number, d: number): string {\n //@ verify\n //@ type units int\n //@ type d nat\n //@ requires d >= 0\n //@ ensures \\result.length > 0\n return units < 0 ? \"-\" + fixedText(-units, d) : fixedText(units, d);\n}\n\n/**\n * Where the text's \".\" is, or its length when it has none.\n *\n * @ensures exactly the model's DotIndex (in the link)\n */\nexport function dotIndex(s: string): number {\n //@ verify\n //@ ensures \\result === (indexOfCode(s, 46, 0) < 0 ? s.length : indexOfCode(s, 46, 0))\n //@ ensures 0 <= \\result && \\result <= s.length\n const k = indexOfCode(s, 46, 0);\n return k < 0 ? s.length : k;\n}\n\n/**\n * The text before its \".\" (all of it when it has none).\n *\n * @ensures exactly the model's WholePart (in the link)\n */\nexport function wholePart(s: string): string {\n //@ verify\n //@ ensures \\result === s.slice(0, indexOfCode(s, 46, 0) < 0 ? s.length : indexOfCode(s, 46, 0))\n return s.slice(0, dotIndex(s));\n}\n\n/**\n * The text after its first \".\" (\"\" when it has none).\n *\n * @ensures exactly the model's FracPart (in the link)\n */\nexport function fracPart(s: string): string {\n //@ verify\n //@ ensures \\result === (indexOfCode(s, 46, 0) < 0 ? \"\" : s.slice(indexOfCode(s, 46, 0) + 1))\n const k = dotIndex(s);\n return k < s.length ? s.slice(k + 1) : \"\";\n}\n\n/**\n * Fixed-point text: digits, at most one \".\", and at least one digit (\"5\", \"5.\", \".5\", \"5.25\";\n * not \"\", \".\", \"-5\", \"5.2.1\").\n *\n * @ensures exactly the model's IsFixedText (in the link)\n */\nexport function isFixedText(s: string): boolean {\n //@ verify\n //@ ensures \\result ==> s.length > 0\n const whole = wholePart(s);\n const frac = fracPart(s);\n return digitsFrom(whole, 0) && digitsFrom(frac, 0) && whole.length + frac.length > 0;\n}\n\n/**\n * How many places the text writes after its \".\".\n *\n * @ensures exactly the model's Places (in the link)\n */\nexport function places(s: string): number {\n //@ verify\n //@ ensures \\result === (indexOfCode(s, 46, 0) < 0 ? 0 : s.length - indexOfCode(s, 46, 0) - 1)\n //@ ensures 0 <= \\result && \\result <= s.length\n return fracPart(s).length;\n}\n\n/**\n * Fixed-point text's exact value in 10^-places(s) units, or −1 for any other text.\n *\n * @ensures exactly the model's Units on fixed-point text, −1 off it (in the link)\n */\nexport function fixedUnits(s: string): number {\n //@ verify\n //@ ensures \\result >= -1\n const whole = wholePart(s);\n const frac = fracPart(s);\n if (!digitsFrom(whole, 0) || !digitsFrom(frac, 0) || whole.length + frac.length === 0) return -1;\n return digitsValue(whole) * pow10(frac.length) + digitsValue(frac);\n}\n\n/**\n * A non-negative count of 10^-from units as 10^-to units: exact when gaining places, else\n * rounded half up.\n *\n * @ensures exactly the model's Rescale (in the link)\n */\nexport function rescale(units: number, from: number, to: number): number {\n //@ verify\n //@ type units nat\n //@ type from nat\n //@ type to nat\n //@ requires units >= 0 && from >= 0 && to >= 0\n //@ ensures \\result >= 0\n if (to >= from) return units * pow10(to - from);\n return Math.floor((units + 5 * pow10(from - to - 1)) / pow10(from - to));\n}\n\n/**\n * Any integer count: a negative one rescales its size.\n *\n * @ensures exactly the model's SignedRescale (in the link)\n */\nexport function signedRescale(units: number, from: number, to: number): number {\n //@ verify\n //@ type units int\n //@ type from nat\n //@ type to nat\n //@ requires from >= 0 && to >= 0\n //@ ensures units >= 0 ==> \\result >= 0\n return units < 0 ? -rescale(-units, from, to) : rescale(units, from, to);\n}\n\n/**\n * The larger of two place counts.\n *\n * @ensures result = max(a, b)\n */\nexport function maxPlaces(a: number, b: number): number {\n //@ verify\n //@ type a nat\n //@ type b nat\n //@ requires a >= 0 && b >= 0\n //@ ensures \\result === (a >= b ? a : b)\n return a >= b ? a : b;\n}\n","//@ backend dafny\nimport { includesFrom } from \"./includes\";\nimport { compareFrom, endsWith } from \"./strings\";\n\n/* Pure list helpers, verified with LemmaScript → Dafny (model: models/Utils.dfy; link:\n models/links/utils-lists.dfy). */\n\n/**\n * The items from index `i` joined by `sep` (JS `xs.join(sep)` at i = 0).\n *\n * @ensures exactly the model's JoinFrom\n */\nexport function joinFrom(xs: string[], sep: string, i: number): string {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases xs.length - i\n //@ ensures i >= xs.length ==> \\result === \"\"\n //@ ensures i + 1 === xs.length ==> \\result === xs[i]\n //@ ensures i + 1 < xs.length ==> (\\result.length >= xs[i].length + sep.length && \\result.slice(0, xs[i].length + sep.length) === xs[i] + sep)\n if (i >= xs.length) return \"\";\n return joinRange(xs, sep, i, xs.length);\n}\n\n/**\n * The items from `lo` up to `hi` (at least one) joined by `sep`, by halves: a list of n items\n * recurses log n deep, so a generated file of many thousand lines fits the stack.\n *\n * @ensures = the model's JoinFrom(xs[..hi], sep, lo) (links/utils-lists.dfy, by JoinFromSplits)\n */\nfunction joinRange(xs: string[], sep: string, lo: number, hi: number): string {\n //@ verify\n //@ type lo nat\n //@ type hi nat\n //@ requires lo < hi && hi <= xs.length\n //@ decreases hi - lo\n //@ ensures hi === lo + 1 ==> \\result === xs[lo]\n if (hi - lo === 1) return xs[lo] as string;\n const mid = lo + Math.floor((hi - lo) / 2);\n return joinRange(xs, sep, lo, mid) + sep + joinRange(xs, sep, mid, hi);\n}\n\n/**\n * The first index ≥ `i` holding `x`, or -1 (JS `xs.indexOf(x, i)`): how a table of parallel\n * lists is read.\n *\n * @ensures result = -1 ∨ (i ≤ result < |xs| ∧ xs[result] = x) — and the first such index: the\n * model's ItemIndexFrom (links/utils-lists.dfy)\n */\nexport function itemIndexFrom(xs: string[], x: string, i: number): number {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases xs.length - i\n //@ ensures \\result === -1 || (i <= \\result && \\result < xs.length && xs[\\result] === x)\n //@ ensures \\result === -1 ==> forall(k: nat, (i <= k && k < xs.length) ==> xs[k] !== x)\n //@ ensures \\result >= 0 ==> forall(k: nat, (i <= k && k < \\result) ==> xs[k] !== x)\n if (i >= xs.length) return -1;\n return (xs[i] as string) === x ? i : itemIndexFrom(xs, x, i + 1);\n}\n\n/**\n * The sum of `xs[i..]`, 0 past the end (JS `xs.slice(i).reduce((a, b) => a + b, 0)`).\n *\n * @ensures result = Σ xs[i..] — exactly the model's SumFrom\n */\nexport function sumFrom(xs: number[], i: number): number {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases xs.length - i\n //@ ensures \\result === (i >= xs.length ? 0 : xs[i] + sumFrom(xs, i + 1))\n if (i >= xs.length) return 0;\n return (xs[i] as number) + sumFrom(xs, i + 1);\n}\n\n/**\n * Whether `x` is one of `xs` from index `i` on (JS `xs.includes(x)` at i = 0).\n *\n * @ensures result ⟺ ∃ k. i ≤ k < |xs| ∧ xs[k] = x — exactly the model's HasItemFrom\n */\nexport function hasItemFrom(xs: string[], x: string, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0 && i <= xs.length\n //@ decreases xs.length - i\n //@ ensures \\result <==> exists(k: nat, i <= k && k < xs.length && xs[k] === x)\n // `===`, not `>=`: past the end xs[i] is undefined, never x, so `>=` vs `>` is equivalent\n if (i === xs.length) return false;\n if ((xs[i] as string) === x) return true;\n return hasItemFrom(xs, x, i + 1);\n}\n\n/**\n * Whether the number `x` is one of `xs` from index `i` on (JS `xs.includes(x)` at i = 0, for\n * numbers).\n *\n * @ensures result ⟺ ∃ k. i ≤ k < |xs| ∧ xs[k] = x — exactly the model's HasNumberFrom\n */\nexport function hasNumberFrom(xs: number[], x: number, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0 && i <= xs.length\n //@ decreases xs.length - i\n //@ ensures \\result <==> exists(k: nat, i <= k && k < xs.length && xs[k] === x)\n if (i === xs.length) return false;\n if ((xs[i] as number) === x) return true;\n return hasNumberFrom(xs, x, i + 1);\n}\n\n/**\n * Whether `s` ends with one of `xs` from index `i` on (a regex `(a|b|…)$` at i = 0).\n *\n * @ensures exactly the model's EndsWithAnyFrom\n */\nexport function endsWithAnyFrom(s: string, xs: string[], i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0 && i <= xs.length\n //@ decreases xs.length - i\n //@ ensures \\result <==> exists(k: nat, i <= k && k < xs.length && endsWith(s, xs[k]))\n if (i === xs.length) return false;\n if (endsWith(s, xs[i] as string)) return true;\n return endsWithAnyFrom(s, xs, i + 1);\n}\n\n/**\n * Whether one of `xs` from index `i` on occurs in `s` (a regex `(a|b|…)` at i = 0).\n *\n * @ensures exactly the model's IncludesAnyFrom\n */\nexport function includesAnyFrom(s: string, xs: string[], i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0 && i <= xs.length\n //@ decreases xs.length - i\n //@ ensures \\result <==> exists(k: nat, i <= k && k < xs.length && includesFrom(s, xs[k], 0))\n if (i === xs.length) return false;\n if (includesFrom(s, xs[i] as string, 0)) return true;\n return includesAnyFrom(s, xs, i + 1);\n}\n\n/**\n * The items of `xs` from index `i` that `allowed` doesn't hold, in order (JS\n * `xs.filter((x) => !allowed.includes(x))` at i = 0).\n *\n * @ensures exactly the model's NotInFrom (NotInFromIsTheUnknown: the unknown ones, all of them)\n */\nexport function notInFrom(xs: string[], allowed: string[], i: number): string[] {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases xs.length - i\n //@ ensures i >= xs.length ==> \\result.length === 0\n if (i >= xs.length) return [];\n const rest = notInFrom(xs, allowed, i + 1);\n return hasItemFrom(allowed, xs[i] as string, 0) ? rest : [xs[i] as string, ...rest];\n}\n\n/**\n * Whether `x` is one of the first `k` items of `xs` (JS `xs.slice(0, k).includes(x)`).\n *\n * @ensures result ⟺ ∃ j < k. xs[j] = x — exactly the model's HasItemBelow\n */\nexport function hasItemBelow(xs: string[], x: string, k: number): boolean {\n //@ verify\n //@ type k nat\n //@ requires k >= 0 && k <= xs.length\n //@ decreases k\n //@ ensures \\result <==> exists(j: nat, j < k && xs[j] === x)\n if (k === 0) return false;\n if ((xs[k - 1] as string) === x) return true;\n return hasItemBelow(xs, x, k - 1);\n}\n\n/**\n * The items of `xs` from index `i` that repeat an earlier item, in order (at i = 0, every\n * duplicate after its first; none ⟺ the items are unique).\n *\n * @ensures exactly the model's RepeatsFrom (RepeatsFromEmpty: none exactly when unique)\n */\nexport function repeatsFrom(xs: string[], i: number): string[] {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases xs.length - i\n //@ ensures i >= xs.length ==> \\result.length === 0\n if (i >= xs.length) return [];\n const rest = repeatsFrom(xs, i + 1);\n return hasItemBelow(xs, xs[i] as string, i) ? [xs[i] as string, ...rest] : rest;\n}\n\n/**\n * The items of `xs` from index `i` that no earlier item repeats, in order (at i = 0, JS\n * `[...new Set(xs)]`: each item once, first occurrence kept).\n *\n * @ensures exactly the model's FirstsFrom\n */\nexport function firstsFrom(xs: string[], i: number): string[] {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases xs.length - i\n //@ ensures i >= xs.length ==> \\result.length === 0\n //@ ensures forall(j: nat, forall(k: nat, (j < k && k < \\result.length) ==> \\result[j] !== \\result[k]))\n if (i >= xs.length) return [];\n const rest = firstsFrom(xs, i + 1);\n return hasItemBelow(xs, xs[i] as string, i) ? rest : [xs[i] as string, ...rest];\n}\n\n/**\n * At most the first `n` items (JS `xs.slice(0, n)` for n ≥ 0).\n *\n * @ensures exactly the model's TakeAtMost\n */\nexport function takeAtMost(xs: string[], n: number): string[] {\n //@ verify\n //@ type n nat\n //@ requires n >= 0\n //@ ensures \\result === (n < xs.length ? xs.slice(0, n) : xs)\n return n < xs.length ? xs.slice(0, n) : xs;\n}\n\n/**\n * Whether two number lists are equal from index `i` on (the whole lists at i = 0).\n *\n * @ensures result ⟺ |a| = |b| ∧ ∀ k ∈ [i, |a|). a[k] = b[k] — exactly the model's\n * SameNumbersFrom\n */\nexport function sameNumbersFrom(a: number[], b: number[], i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0 && i <= a.length\n //@ decreases a.length - i\n //@ ensures \\result <==> (a.length === b.length && forall(k: nat, i <= k && k < a.length ==> a[k] === b[k]))\n if (a.length !== b.length) return false;\n if (i === a.length) return true;\n return (a[i] as number) === (b[i] as number) && sameNumbersFrom(a, b, i + 1);\n}\n\n/**\n * Whether two text lists are equal from index `i` on (the whole lists at i = 0, as their\n * `JSON.stringify` texts compare).\n *\n * @ensures result ⟺ |a| = |b| ∧ ∀ k ∈ [i, |a|). a[k] = b[k] — exactly the model's\n * SameTextsFrom\n */\nexport function sameTextsFrom(a: string[], b: string[], i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0 && i <= a.length\n //@ decreases a.length - i\n //@ ensures \\result <==> (a.length === b.length && forall(k: nat, i <= k && k < a.length ==> a[k] === b[k]))\n if (a.length !== b.length) return false;\n if (i === a.length) return true;\n return (a[i] as string) === (b[i] as string) && sameTextsFrom(a, b, i + 1);\n}\n\n/**\n * The texts in code-unit order (JS's default `sort`, stable: of two equal texts the earlier\n * stays first).\n *\n * @ensures exactly the model's SortedTexts (in the link); the same length\n */\nexport function sortedTexts(xs: string[]): string[] {\n //@ verify\n //@ ensures \\result.length === xs.length\n if (xs.length === 0) return [];\n return insertText(xs[0] as string, sortedTexts(xs.slice(1)));\n}\n\n/**\n * `x` placed into sorted texts before the first it doesn't sort after.\n *\n * @ensures exactly the model's InsertText (in the link); one longer\n */\nexport function insertText(x: string, sorted: string[]): string[] {\n //@ verify\n //@ ensures \\result.length === sorted.length + 1\n if (sorted.length === 0) return [x];\n const first = sorted[0] as string;\n if (compareFrom(x, first, 0) <= 0) return [x, ...sorted];\n return [first, ...insertText(x, sorted.slice(1))];\n}\n","//@ backend dafny\nimport { codeAt } from \"@atlas/utils/strings\";\n\n/* The payment BIN scan (`/[?&]Bin=[^&]/`), verified with LemmaScript → Dafny (model:\n models/AugurWeb.dfy \"payment BIN\"; link: models/links/augur-web-bin-scan.dfy). Its own file\n so `binFromString` in rules.ts names these by import, which an importer can resolve\n (a contract naming a same-file function can't be, models/LSC-GAPS.md). */\n\n/**\n * A \"?\" or \"&\" at `k`, \"Bin=\" right after it, and a value character (not \"&\") after that: what\n * `[?&]Bin=[^&]` matches there.\n *\n * @ensures result ⟺ s[k] ∈ {?, &} ∧ s[k+1 ..] starts with \"Bin=\" ∧ s[k+5] is a char ≠ \"&\"\n */\nexport function binMarkAt(s: string, k: number): boolean {\n //@ verify\n //@ type k nat\n //@ requires k >= 0\n //@ ensures \\result === (k + 6 <= s.length && (s.charCodeAt(k) === 63 || s.charCodeAt(k) === 38) && s.slice(k + 1, k + 5) === \"Bin=\" && s.charCodeAt(k + 5) !== 38)\n if (k + 6 > s.length) return false;\n const c = codeAt(s, k);\n return (c === 63 || c === 38) && s.slice(k + 1, k + 5) === \"Bin=\" && codeAt(s, k + 5) !== 38;\n}\n\n/**\n * The first \"?Bin=\" or \"&Bin=\" from index `i` with a value after it, or −1 (the regex skips a\n * mark with an empty value and keeps searching).\n *\n * @ensures result is the first k ≥ i with k + 6 ≤ |s| where `binMarkAt(s, k)`, else −1 —\n * exactly the model's BinAt; binMarkAt is spelled out so importers can read the contract\n */\nexport function binAt(s: string, i: number): number {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases s.length - i\n //@ ensures \\result === -1 || (i <= \\result && \\result + 6 <= s.length)\n //@ ensures \\result >= 0 ==> ((s.charCodeAt(\\result) === 63 || s.charCodeAt(\\result) === 38) && s.slice(\\result + 1, \\result + 5) === \"Bin=\" && s.charCodeAt(\\result + 5) !== 38)\n //@ ensures \\result >= 0 ==> forall(k: nat, (i <= k && k < \\result) ==> !((s.charCodeAt(k) === 63 || s.charCodeAt(k) === 38) && s.slice(k + 1, k + 5) === \"Bin=\" && s.charCodeAt(k + 5) !== 38))\n //@ ensures \\result === -1 ==> forall(k: nat, (i <= k && k + 6 <= s.length) ==> !((s.charCodeAt(k) === 63 || s.charCodeAt(k) === 38) && s.slice(k + 1, k + 5) === \"Bin=\" && s.charCodeAt(k + 5) !== 38))\n if (i + 6 > s.length) return -1;\n if (binMarkAt(s, i)) return i;\n return binAt(s, i + 1);\n}\n\n/**\n * The text after a \"?Bin=\" mark at `k`, or \"\" when no mark fits there (`binAt` never gives\n * such a k; the guard keeps the slice in range for the proof).\n *\n * @ensures result = s[k + 5 ..] when 0 ≤ k ∧ k + 5 ≤ |s|, else \"\"\n */\nexport function afterBinMark(s: string, k: number): string {\n //@ verify\n //@ ensures (k < 0 || k + 5 > s.length) ==> \\result === \"\"\n //@ ensures !(k < 0 || k + 5 > s.length) ==> \\result === s.slice(k + 5)\n if (k < 0 || k + 5 > s.length) return \"\";\n return s.slice(k + 5);\n}\n","//@ backend dafny\nimport { codeAt, indexOfFrom } from \"./strings\";\n\n/* Whole-string rewrites, verified with LemmaScript → Dafny (model: models/Utils.dfy \"text\n replacement\"; link: models/links/utils-replace.dfy). Each has a recursive spec (for\n proofs and tests) and a loop proved to build exactly it (for callers: its depth doesn't\n grow with the input). The `\\result === spec(s)` clause of each loop is a hand `ensures`\n in replace.dfy, as utils `escapeHtml`'s is: lsc would put it in every importer's axiom,\n where this file's own spec isn't emitted. */\n\n// --- One substitution rule (model `Utils.Substituted`, 2026-11-03): each code unit found in a\n// table is replaced by its text, every other kept. replaceCode, pointerToken, escapeHtml and\n// the generator's look-alike fold are this rule, each with its own table.\n\n/**\n * The first index ≥ `i` holding `c` in `codes`, or -1.\n *\n * @ensures result = -1 ∨ (i ≤ result < |codes| ∧ codes[result] = c) — the model's CodeIndex\n */\nexport function codeIndex(codes: number[], c: number, i: number): number {\n //@ verify\n //@ type i nat\n //@ requires i <= codes.length\n //@ decreases codes.length - i\n //@ ensures \\result === -1 || (i <= \\result && \\result < codes.length && codes[\\result] === c)\n // `===`, not `>=`: past the end codes[i] is undefined, never c, so `>=` vs `>` is equivalent\n if (i === codes.length) return -1;\n return (codes[i] as number) === c ? i : codeIndex(codes, c, i + 1);\n}\n\n/** `s` with each code unit in `codes` replaced by its text, recursively: the spec `substitute` builds. */\nexport function substitutedText(s: string, codes: number[], texts: string[]): string {\n //@ verify\n //@ decreases s.length\n //@ ensures s.length === 0 ==> \\result === \"\"\n //@ ensures codes.length === 0 ==> \\result === s\n if (s.length === 0) return \"\";\n const k = codeIndex(codes, s.charCodeAt(0), 0);\n const head = k >= 0 && k < texts.length ? (texts[k] as string) : s.slice(0, 1);\n return head + substitutedText(s.slice(1), codes, texts);\n}\n\n/**\n * `s` with each code unit in `codes` replaced by the text at its first index there, every\n * other kept. A loop, so its depth doesn't grow with the input.\n *\n * @ensures result = substitutedText(s, codes, texts) — the model's Substituted (a hand\n * ensures in replace.dfy: importers can't resolve substitutedText)\n * @ensures |s| = 0 ⟹ result = \"\" ∧ |codes| = 0 ⟹ result = s — no table, no change\n */\nexport function substitute(s: string, codes: number[], texts: string[]): string {\n //@ verify\n //@ ensures s.length === 0 ==> \\result === \"\"\n //@ ensures codes.length === 0 ==> \\result === s\n let out = \"\";\n // `!==`, not `<`: one step past the end reads NaN and adds \"\", so `<=` would be equivalent\n for (let i = 0; i !== s.length; i++) {\n //@ invariant 0 <= i && i <= s.length\n //@ invariant out + substitutedText(s.slice(i), codes, texts) === substitutedText(s, codes, texts)\n //@ decreases s.length - i\n const k = codeIndex(codes, s.charCodeAt(i), 0);\n out = k >= 0 && k < texts.length ? out + (texts[k] as string) : out + s.slice(i, i + 1);\n }\n return out;\n}\n\n/**\n * `s` with every `from` code unit replaced by `to`, recursively: the one-entry table.\n *\n * @ensures no `from` survives when `to` holds none — exactly the model's ReplaceCode in the link\n */\nexport function replacedCode(s: string, from: number, to: string): string {\n //@ verify\n //@ ensures forall(j: nat, j < to.length ==> to.charCodeAt(j) !== from) ==> forall(k: nat, k < \\result.length ==> \\result.charCodeAt(k) !== from)\n return substitutedText(s, [from], [to]);\n}\n\n/**\n * `s` with every `from` code unit replaced by `to` (`replaceAll` of one character).\n *\n * @ensures no `from` survives when `to` holds none (ReplaceCodeRemoves) — = replacedCode(s,\n * from, to), the model's ReplaceCode, as a hand ensures in replace.dfy\n */\nexport function replaceCode(s: string, from: number, to: string): string {\n //@ verify\n //@ ensures forall(j: nat, j < to.length ==> to.charCodeAt(j) !== from) ==> forall(k: nat, k < \\result.length ==> \\result.charCodeAt(k) !== from)\n return substitute(s, [from], [to]);\n}\n\n/**\n * `s` with its first `find` replaced by `put`; no match leaves it: JS `s.replace(find, put)` for\n * a string `find`, when `put` holds no `$` (JS reads `$&`, `$1` there as a pattern).\n *\n * @ensures exactly the model's ReplaceFirst\n */\nexport function replaceFirst(s: string, find: string, put: string): string {\n //@ verify\n //@ ensures \\result === (indexOfFrom(s, find, 0) < 0 ? s : s.slice(0, indexOfFrom(s, find, 0)) + put + s.slice(indexOfFrom(s, find, 0) + find.length))\n const i = indexOfFrom(s, find, 0);\n if (i < 0) return s;\n return s.slice(0, i) + put + s.slice(i + find.length);\n}\n\n/**\n * `s` with every `find` replaced by `put`, left to right without overlaps: JS\n * `s.split(find).join(put)` for a non-empty `find` (tests pin the two). Recursive, with no\n * loop twin: its callers fill short templates (the SDK's registry paths), so the depth stays\n * small; a long-input caller would need one.\n *\n * @requires |find| > 0 — split(\"\") splits into characters, a different rule\n * @ensures text shorter than `find` is unchanged, and so is any text when `put` = `find` —\n * exactly the model's ReplaceText in the link\n */\nexport function replaceText(s: string, find: string, put: string): string {\n //@ verify\n //@ requires find.length > 0\n //@ decreases s.length\n //@ ensures s.length < find.length ==> \\result === s\n //@ ensures put === find ==> \\result === s\n if (s.length < find.length) return s;\n if (s.slice(0, find.length) === find) {\n return put + replaceText(s.slice(find.length), find, put);\n }\n return s.slice(0, 1) + replaceText(s.slice(1), find, put);\n}\n\n/**\n * A JSON Pointer token of `s`, recursively: `~` → `~0`, `/` → `~1`.\n *\n * @ensures never holds \"/\" — exactly the model's PointerToken in the link\n */\nexport function pointerTokenOf(s: string): string {\n //@ verify\n //@ ensures forall(k: nat, k < \\result.length ==> \\result.charCodeAt(k) !== 47)\n return substitutedText(s, [126, 47], [\"~0\", \"~1\"]);\n}\n\n/**\n * `s` as a JSON Pointer reference token (RFC 6901 §3): `~` as `~0`, `/` as `~1`.\n *\n * @ensures never holds \"/\" (PointerTokenHasNoSlash) — = pointerTokenOf(s), the model's\n * PointerToken, as a hand ensures in replace.dfy\n */\nexport function pointerToken(s: string): string {\n //@ verify\n //@ ensures forall(k: nat, k < \\result.length ==> \\result.charCodeAt(k) !== 47)\n return substitute(s, [126, 47], [\"~0\", \"~1\"]);\n}\n\n/**\n * The escape of `s`, recursively: each `&` `<` `>` `\"` `'` as its entity.\n *\n * @ensures no `<` `>` `\"` `'` in result — exactly the model's EscapeHtml in the link\n */\nexport function escapedText(s: string): string {\n //@ verify\n //@ ensures forall(k: nat, k < \\result.length ==> (\\result.charCodeAt(k) !== 60 && \\result.charCodeAt(k) !== 62 && \\result.charCodeAt(k) !== 34 && \\result.charCodeAt(k) !== 39))\n return substitutedText(s, [38, 60, 62, 34, 39], [\"&amp;\", \"&lt;\", \"&gt;\", \"&quot;\", \"&#39;\"]);\n}\n\n/**\n * Escape text for an HTML body or a double-quoted attribute.\n *\n * @ensures result = escapedText(value) — exactly the model's EscapeHtml (a hand ensures in\n * replace.dfy, since importers can't see escapedText)\n * @ensures ∀ k < |result|. result[k] ∉ {\"<\", \">\", \"\\\"\", \"'\"} — nothing in the output can\n * open a tag or close the attribute; \"&\" appears only as the start of an entity\n */\nexport function escapeHtml(value: string): string {\n //@ verify\n //@ ensures forall(k: nat, k < \\result.length ==> (\\result.charCodeAt(k) !== 60 && \\result.charCodeAt(k) !== 62 && \\result.charCodeAt(k) !== 34 && \\result.charCodeAt(k) !== 39))\n return substitute(value, [38, 60, 62, 34, 39], [\"&amp;\", \"&lt;\", \"&gt;\", \"&quot;\", \"&#39;\"]);\n}\n\n/**\n * The code units a JSON string escapes: `\"`, `\\`, then the control characters with short\n * escapes (\\b \\t \\n \\f \\r), then the other code units below 0x20.\n *\n * @ensures exactly the model's JsonCodes\n */\nexport function jsonCodes(): number[] {\n //@ verify\n //@ ensures \\result === [34, 92, 8, 9, 10, 12, 13, 0, 1, 2, 3, 4, 5, 6, 7, 11, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]\n return [\n 34, 92, 8, 9, 10, 12, 13, 0, 1, 2, 3, 4, 5, 6, 7, 11, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,\n 24, 25, 26, 27, 28, 29, 30, 31,\n ];\n}\n\n/**\n * Each `jsonCodes` entry's escape (ECMA-262 QuoteJSONString; lowercase hex, as JSON.stringify).\n *\n * @ensures exactly the model's JsonTexts\n */\nexport function jsonTexts(): string[] {\n //@ verify\n //@ ensures \\result === [\"\\\\\\\"\", \"\\\\\\\\\", \"\\\\b\", \"\\\\t\", \"\\\\n\", \"\\\\f\", \"\\\\r\", \"\\\\u0000\", \"\\\\u0001\", \"\\\\u0002\", \"\\\\u0003\", \"\\\\u0004\", \"\\\\u0005\", \"\\\\u0006\", \"\\\\u0007\", \"\\\\u000b\", \"\\\\u000e\", \"\\\\u000f\", \"\\\\u0010\", \"\\\\u0011\", \"\\\\u0012\", \"\\\\u0013\", \"\\\\u0014\", \"\\\\u0015\", \"\\\\u0016\", \"\\\\u0017\", \"\\\\u0018\", \"\\\\u0019\", \"\\\\u001a\", \"\\\\u001b\", \"\\\\u001c\", \"\\\\u001d\", \"\\\\u001e\", \"\\\\u001f\"]\n return [\n '\\\\\"',\n \"\\\\\\\\\",\n \"\\\\b\",\n \"\\\\t\",\n \"\\\\n\",\n \"\\\\f\",\n \"\\\\r\",\n \"\\\\u0000\",\n \"\\\\u0001\",\n \"\\\\u0002\",\n \"\\\\u0003\",\n \"\\\\u0004\",\n \"\\\\u0005\",\n \"\\\\u0006\",\n \"\\\\u0007\",\n \"\\\\u000b\",\n \"\\\\u000e\",\n \"\\\\u000f\",\n \"\\\\u0010\",\n \"\\\\u0011\",\n \"\\\\u0012\",\n \"\\\\u0013\",\n \"\\\\u0014\",\n \"\\\\u0015\",\n \"\\\\u0016\",\n \"\\\\u0017\",\n \"\\\\u0018\",\n \"\\\\u0019\",\n \"\\\\u001a\",\n \"\\\\u001b\",\n \"\\\\u001c\",\n \"\\\\u001d\",\n \"\\\\u001e\",\n \"\\\\u001f\",\n ];\n}\n\n/**\n * A string as JSON text, as `JSON.stringify(s)` writes it for well-formed text: quoted, `\"`\n * and `\\` backslashed, control characters escaped. A lone surrogate is kept as it is\n * (JSON.stringify writes `\\udxxx`); callers pass well-formed text. Built by the `substitute`\n * loop, so its depth doesn't grow with the text (an FAQ answer can be long; Fable review\n * 2026-11-06); importers still see a function (its contract as an axiom), so their premise\n * `jsonString(s) == Utils.JsonString(s)` stands.\n *\n * @ensures result = `\"` + substitutedText(s, jsonCodes(), jsonTexts()) + `\"` — a hand ensures in\n * replace.dfy; the model's JsonString (proved in links/utils-replace.dfy)\n * @ensures result starts and ends with `\"`\n */\nexport function jsonString(s: string): string {\n //@ verify\n //@ ensures \\result.length >= 2 && \\result.charCodeAt(0) === 34 && \\result.charCodeAt(\\result.length - 1) === 34\n return '\"' + substitute(s, jsonCodes(), jsonTexts()) + '\"';\n}\n\n/**\n * `s` with every `*` `/` pair written `* /`, recursively: the spec `commentSafe` builds.\n *\n * @ensures no \"*\" \"/\" pair in result — exactly the model's CommentSafe in the link\n */\nexport function commentSafeOf(s: string): string {\n //@ verify\n //@ decreases s.length\n //@ ensures forall(k: nat, k + 1 < \\result.length ==> !(\\result.charCodeAt(k) === 42 && \\result.charCodeAt(k + 1) === 47))\n if (s.length === 0) return \"\";\n if (codeAt(s, 0) === 42 && codeAt(s, 1) === 47) {\n return \"* /\" + commentSafeOf(s.slice(2));\n }\n return s.slice(0, 1) + commentSafeOf(s.slice(1));\n}\n\n/**\n * Text safe inside a `/* … *\\/` comment: every `*` `/` pair written `* /`.\n *\n * @ensures no \"*\" \"/\" pair in result: nothing can close the comment — = commentSafeOf(s), the\n * model's CommentSafe, as a hand ensures in replace.dfy\n */\nexport function commentSafe(s: string): string {\n //@ verify\n //@ ensures forall(k: nat, k + 1 < \\result.length ==> !(\\result.charCodeAt(k) === 42 && \\result.charCodeAt(k + 1) === 47))\n let out = \"\";\n let i = 0;\n while (i !== s.length) {\n //@ invariant 0 <= i && i <= s.length\n //@ invariant out + commentSafeOf(s.slice(i)) === commentSafeOf(s)\n //@ decreases s.length - i\n if (codeAt(s, i) === 42 && codeAt(s, i + 1) === 47) {\n out = out + \"* /\";\n i = i + 2;\n } else {\n out = out + s.slice(i, i + 1);\n i = i + 1;\n }\n }\n return out;\n}\n","//@ backend dafny\nimport { decimal } from \"@atlas/foundation/decimal\";\nimport { includesFrom } from \"./includes\";\nimport { joinFrom } from \"./lists\";\nimport { jsonString } from \"./replace\";\nimport { canonicalNatValue, compareFrom, isCanonicalNat, lineFeed, spaces } from \"./strings\";\n\n/* A JSON value as a node, verified with LemmaScript → Dafny (model: models/Utils.dfy \"JSON\n values\"; link: models/links/utils-json-node.dfy), so every rule over parsed JSON is proved:\n equality, presence, and the text JSON.stringify, `JSON.stringify(v, null, 2)` and canonical\n JSON write. Only reading a JS value into a node (`@atlas/json-values` `jsonNode`) stays\n outside, where `typeof` is (2026-11-06; scratch PDSA: lsc's `decreases` takes no tuple, so\n the recursions over items and fields take the rest of the list, `xs.slice(1)`). */\n\n/**\n * A JSON value: its kind (\"undefined\", \"null\", \"boolean\", \"number\", \"string\", \"array\",\n * \"object\"), a leaf's JSON text (\"\" for undefined, an array or an object), a string leaf's own\n * value (`str`, the JS string; \"\" for every other kind), an array's items and an object's fields\n * in insertion order.\n */\nexport interface JsonNode {\n kind: string;\n text: string;\n str: string;\n items: JsonNode[];\n fields: JsonField[];\n}\n\n/** An object's field: its key, the key's JSON text (`JSON.stringify(key)`), its value. */\nexport interface JsonField {\n key: string;\n keyText: string;\n value: JsonNode;\n}\n\n/**\n * The undefined node: a missing field's value.\n *\n * @ensures kind \"undefined\", no text, items or fields\n */\nexport function undefinedNode(): JsonNode {\n //@ verify\n //@ ensures \\result.kind === \"undefined\" && \\result.text === \"\" && \\result.str === \"\" && \\result.items.length === 0 && \\result.fields.length === 0\n return { kind: \"undefined\", text: \"\", str: \"\", items: [], fields: [] };\n}\n\n/**\n * The value of the first field from `i` on named `key`, or the undefined node (JS `obj[key]`:\n * an object's keys are unique).\n *\n * @ensures exactly the model's FieldValue (in the link)\n */\nexport function fieldValue(fs: JsonField[], key: string, i: number): JsonNode {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases fs.length - i\n //@ ensures i >= fs.length ==> \\result.kind === \"undefined\"\n if (i >= fs.length) return undefinedNode();\n const f = fs[i] as JsonField;\n return f.key === key ? f.value : fieldValue(fs, key, i + 1);\n}\n\n/**\n * Whether a field from `i` on is named `key` (JS `key in obj`).\n *\n * @ensures exactly the model's HasField (in the link)\n */\nexport function hasField(fs: JsonField[], key: string, i: number): boolean {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases fs.length - i\n //@ ensures i >= fs.length ==> !\\result\n if (i >= fs.length) return false;\n return (fs[i] as JsonField).key === key || hasField(fs, key, i + 1);\n}\n\n/**\n * Two values are equal: lists item by item in order, objects with the same keys and equal\n * values whatever their order, any other pair by kind and text (JS `===` on JSON leaves).\n *\n * @ensures exactly the model's NodeEqual (in the link): an array never equals a non-array\n */\nexport function nodeEqual(a: JsonNode, b: JsonNode): boolean {\n //@ verify\n //@ ensures (a.kind === \"array\") !== (b.kind === \"array\") ==> !\\result\n if (a.kind === \"array\" || b.kind === \"array\") {\n return (\n a.kind === \"array\" &&\n b.kind === \"array\" &&\n a.items.length === b.items.length &&\n itemsEqual(a.items, b.items)\n );\n }\n if (a.kind === \"object\" && b.kind === \"object\") {\n return a.fields.length === b.fields.length && fieldsIn(a.fields, b.fields);\n }\n return a.kind === b.kind && a.text === b.text;\n}\n\n/** Item by item (the model's ItemsEqual). */\nfunction itemsEqual(xs: JsonNode[], ys: JsonNode[]): boolean {\n //@ verify\n //@ requires xs.length === ys.length\n //@ ensures xs.length === 0 ==> \\result\n if (xs.length === 0) return true;\n return nodeEqual(xs[0] as JsonNode, ys[0] as JsonNode) && itemsEqual(xs.slice(1), ys.slice(1));\n}\n\n/** Every field of `fs` is one of `gs`'s keys, with an equal value (the model's FieldsIn). */\nfunction fieldsIn(fs: JsonField[], gs: JsonField[]): boolean {\n //@ verify\n //@ ensures fs.length === 0 ==> \\result\n if (fs.length === 0) return true;\n const f = fs[0] as JsonField;\n return (\n hasField(gs, f.key, 0) &&\n nodeEqual(f.value, fieldValue(gs, f.key, 0)) &&\n fieldsIn(fs.slice(1), gs)\n );\n}\n\n/**\n * A value counts as given: not undefined, null, false, 0, \"\", [] or {} (an object with any key\n * counts, whatever its value).\n *\n * @ensures exactly the model's NodePresent\n */\nexport function nodePresent(v: JsonNode): boolean {\n //@ verify\n //@ ensures \\result === (v.kind === \"array\" ? v.items.length > 0 : (v.kind === \"object\" ? v.fields.length > 0 : (v.kind === \"boolean\" ? v.text === \"true\" : (v.kind === \"number\" ? v.text !== \"0\" : (v.kind === \"string\" ? v.text !== '\"\"' : false)))))\n if (v.kind === \"array\") return v.items.length > 0;\n if (v.kind === \"object\") return v.fields.length > 0;\n if (v.kind === \"boolean\") return v.text === \"true\";\n if (v.kind === \"number\") return v.text !== \"0\";\n if (v.kind === \"string\") return v.text !== '\"\"';\n return false;\n}\n\n// --- Building nodes (2026-11-06, for rules that write JSON: the generator's type nodes). JS\n// object semantics exactly: assignment keeps an existing key's place.\n\n/**\n * A string leaf: its JSON text and its own value.\n *\n * @ensures exactly the model's StringNode\n */\nexport function stringNode(s: string): JsonNode {\n //@ verify\n //@ ensures \\result.kind === \"string\" && \\result.text === jsonString(s) && \\result.str === s && \\result.items.length === 0 && \\result.fields.length === 0\n return { kind: \"string\", text: jsonString(s), str: s, items: [], fields: [] };\n}\n\n/**\n * A boolean leaf.\n *\n * @ensures exactly the model's BoolNode\n */\nexport function boolNode(b: boolean): JsonNode {\n //@ verify\n //@ ensures \\result.kind === \"boolean\" && \\result.text === (b ? \"true\" : \"false\") && \\result.str === \"\" && \\result.items.length === 0 && \\result.fields.length === 0\n return { kind: \"boolean\", text: b ? \"true\" : \"false\", str: \"\", items: [], fields: [] };\n}\n\n/**\n * An array of these items.\n *\n * @ensures exactly the model's ArrayNode\n */\nexport function arrayNode(items: JsonNode[]): JsonNode {\n //@ verify\n //@ ensures \\result.kind === \"array\" && \\result.text === \"\" && \\result.str === \"\" && \\result.items === items && \\result.fields.length === 0\n return { kind: \"array\", text: \"\", str: \"\", items, fields: [] };\n}\n\n/**\n * An object of these fields.\n *\n * @ensures exactly the model's ObjectNode\n */\nexport function objectNode(fields: JsonField[]): JsonNode {\n //@ verify\n //@ ensures \\result.kind === \"object\" && \\result.text === \"\" && \\result.str === \"\" && \\result.items.length === 0 && \\result.fields === fields\n return { kind: \"object\", text: \"\", str: \"\", items: [], fields };\n}\n\n/**\n * A field: the key, its JSON text, the value.\n *\n * @ensures exactly the model's FieldOf\n */\nexport function fieldOf(key: string, value: JsonNode): JsonField {\n //@ verify\n //@ ensures \\result.key === key && \\result.keyText === jsonString(key) && \\result.value === value\n return { key, keyText: jsonString(key), value };\n}\n\n/**\n * JSON null.\n *\n * @ensures exactly the model's NullNode\n */\nexport function nullNode(): JsonNode {\n //@ verify\n //@ ensures \\result.kind === \"null\" && \\result.text === \"null\" && \\result.str === \"\" && \\result.items.length === 0 && \\result.fields.length === 0\n return { kind: \"null\", text: \"null\", str: \"\", items: [], fields: [] };\n}\n\n/**\n * A whole number: JSON.stringify writes a natural number in decimal.\n *\n * @ensures exactly the model's NatNode\n */\nexport function natNode(n: number): JsonNode {\n //@ verify\n //@ type n nat\n //@ requires n >= 0\n //@ ensures \\result.kind === \"number\" && \\result.text === decimal(n) && \\result.str === \"\" && \\result.items.length === 0 && \\result.fields.length === 0\n return { kind: \"number\", text: decimal(n), str: \"\", items: [], fields: [] };\n}\n\n/**\n * JS `obj[f.key] = f.value` on fields: an existing key keeps its place and takes the value; a\n * new key goes last.\n *\n * @ensures exactly the model's SetField (in the link); never shorter, at most one longer\n */\nexport function setField(fs: JsonField[], f: JsonField): JsonField[] {\n //@ verify\n //@ ensures \\result.length >= fs.length && \\result.length <= fs.length + 1\n if (fs.length === 0) return [f];\n const first = fs[0] as JsonField;\n if (first.key === f.key) {\n return [{ key: first.key, keyText: first.keyText, value: f.value }, ...fs.slice(1)];\n }\n return [first, ...setField(fs.slice(1), f)];\n}\n\n/**\n * JS `{...a, ...b}` on fields: each of `gs` assigned onto `fs`, in order.\n *\n * @ensures exactly the model's SpreadFields (in the link); no field of `fs` is lost\n */\nexport function spreadFields(fs: JsonField[], gs: JsonField[]): JsonField[] {\n //@ verify\n //@ decreases gs.length\n //@ ensures \\result.length >= fs.length\n if (gs.length === 0) return fs;\n return spreadFields(setField(fs, gs[0] as JsonField), gs.slice(1));\n}\n\nfunction insertByField(x: JsonNode, sorted: JsonNode[], key: string): JsonNode[] {\n //@ verify\n //@ ensures \\result.length === sorted.length + 1\n if (sorted.length === 0) return [x];\n const first = sorted[0] as JsonNode;\n const before =\n compareFrom(fieldValue(x.fields, key, 0).str, fieldValue(first.fields, key, 0).str, 0) <= 0;\n if (before) return [x, ...sorted];\n return [first, ...insertByField(x, sorted.slice(1), key)];\n}\n\n/**\n * Nodes in code-unit order of one text field, equal keys keeping their order (a stable\n * insertion sort): `xs.sort((a, b) => compareCodeUnits(a[key], b[key]))`.\n *\n * @ensures exactly the model's SortedByField (in the link); a reordering of `xs`\n */\nexport function sortedByField(xs: JsonNode[], key: string): JsonNode[] {\n //@ verify\n //@ ensures \\result.length === xs.length\n if (xs.length === 0) return [];\n return insertByField(xs[0] as JsonNode, sortedByField(xs.slice(1), key), key);\n}\n\n/**\n * JS truthiness of a JSON value (`Boolean(v)`, `if (v)`): false, 0, \"\", null and an absent\n * value are falsy; everything else, an empty list or object included, is truthy.\n *\n * @ensures exactly the model's NodeTruthy\n */\nexport function nodeTruthy(v: JsonNode): boolean {\n //@ verify\n //@ ensures \\result === (v.kind === \"boolean\" ? v.text === \"true\" : (v.kind === \"number\" ? v.text !== \"0\" : (v.kind === \"string\" ? v.str !== \"\" : (v.kind === \"array\" || v.kind === \"object\"))))\n if (v.kind === \"boolean\") return v.text === \"true\";\n if (v.kind === \"number\") return v.text !== \"0\";\n if (v.kind === \"string\") return v.str !== \"\";\n return v.kind === \"array\" || v.kind === \"object\";\n}\n\n/**\n * A JS array index: a canonical natural number below 2^32 − 1, the keys JS lists first.\n *\n * @ensures exactly the model's ArrayIndexKey\n */\nexport function isArrayIndexKey(k: string): boolean {\n //@ verify\n //@ ensures \\result === (isCanonicalNat(k) && canonicalNatValue(k) <= 4294967294)\n return isCanonicalNat(k) && canonicalNatValue(k) <= 4294967294;\n}\n\nfunction insertIndexField(fs: JsonField[], f: JsonField): JsonField[] {\n //@ verify\n //@ ensures \\result.length === fs.length + 1\n if (fs.length === 0) return [f];\n const first = fs[0] as JsonField;\n if (!isArrayIndexKey(first.key) || canonicalNatValue(first.key) > canonicalNatValue(f.key)) {\n return [f, ...fs];\n }\n return [first, ...insertIndexField(fs.slice(1), f)];\n}\n\n/**\n * JS `obj[f.key] = f.value` in the key order JS keeps (OrdinaryOwnPropertyKeys): an existing\n * key keeps its place and takes the value; a new array-index key goes among the indexes, in\n * numeric order; any other new key goes last. `{b: 1}` then `\"2\"` reads back `{\"2\": …, b: 1}`.\n *\n * @ensures exactly the model's JsSetField (in the link); never shorter, at most one longer\n */\nexport function jsSetField(fs: JsonField[], f: JsonField): JsonField[] {\n //@ verify\n //@ ensures \\result.length >= fs.length && \\result.length <= fs.length + 1\n if (!hasField(fs, f.key, 0) && isArrayIndexKey(f.key)) return insertIndexField(fs, f);\n return setField(fs, f);\n}\n\n/**\n * `jsonText` on a node: a string as itself, anything else its JSON, undefined \"null\".\n *\n * @ensures exactly the model's NodeText (in the link: a contract naming proseText would leave\n * it unresolved in an importer); a string is itself\n */\nexport function nodeText(v: JsonNode): string {\n //@ verify\n //@ ensures v.kind === \"string\" ==> \\result === v.str\n return v.kind === \"string\" ? v.str : proseText(v);\n}\n\n/**\n * `Number.isInteger` of the number JSON.stringify wrote `t` for (a finite double, shortest\n * form): \"e+\" is at least 1e21, past 2^53, where every double is whole; \"e-\" is a non-zero\n * magnitude below 1e-6; otherwise whole exactly when there is no \".\".\n *\n * @ensures exactly the model's WholeNumberText\n */\nexport function wholeNumberText(t: string): boolean {\n //@ verify\n //@ ensures \\result === (includesFrom(t, \"e+\", 0) || (!includesFrom(t, \"e-\", 0) && !includesFrom(t, \".\", 0)))\n return includesFrom(t, \"e+\", 0) || (!includesFrom(t, \"e-\", 0) && !includesFrom(t, \".\", 0));\n}\n\n/**\n * A node's size: itself, its items' and its fields' values. At least its depth, so a recursion\n * spending one unit of fuel per level never runs out on fuel a fixed multiple of this.\n *\n * @ensures exactly the model's NodeSize (in the link); at least 1\n */\nexport function nodeSize(v: JsonNode): number {\n //@ verify\n //@ ensures \\result >= 1\n return 1 + itemsSize(v.items) + fieldsSize(v.fields);\n}\n\nfunction itemsSize(xs: JsonNode[]): number {\n //@ verify\n //@ ensures \\result >= 0\n if (xs.length === 0) return 0;\n return nodeSize(xs[0] as JsonNode) + itemsSize(xs.slice(1));\n}\n\nfunction fieldsSize(fs: JsonField[]): number {\n //@ verify\n //@ ensures \\result >= 0\n if (fs.length === 0) return 0;\n return nodeSize((fs[0] as JsonField).value) + fieldsSize(fs.slice(1));\n}\n\n// --- Text\n\n/**\n * `JSON.stringify(v)` for a v that isn't undefined: an undefined item is written null, an\n * undefined field left out, a leaf its own text.\n *\n * @ensures exactly the model's CompactText (in the link); a leaf is its own text\n */\nexport function compactText(v: JsonNode): string {\n //@ verify\n //@ ensures v.kind !== \"array\" && v.kind !== \"object\" ==> \\result === v.text\n if (v.kind === \"array\") return \"[\" + joinFrom(compactItems(v.items), \",\", 0) + \"]\";\n if (v.kind === \"object\") return \"{\" + joinFrom(compactFields(v.fields), \",\", 0) + \"}\";\n return v.text;\n}\n\nfunction compactItems(xs: JsonNode[]): string[] {\n //@ verify\n //@ ensures \\result.length === xs.length\n if (xs.length === 0) return [];\n const x = xs[0] as JsonNode;\n return [x.kind === \"undefined\" ? \"null\" : compactText(x), ...compactItems(xs.slice(1))];\n}\n\nfunction compactFields(fs: JsonField[]): string[] {\n //@ verify\n //@ ensures \\result.length <= fs.length\n if (fs.length === 0) return [];\n const f = fs[0] as JsonField;\n if (f.value.kind === \"undefined\") return compactFields(fs.slice(1));\n return [f.keyText + \":\" + compactText(f.value), ...compactFields(fs.slice(1))];\n}\n\n/**\n * `JSON.stringify(v, null, 2)` at nesting depth `d`, for a v that isn't undefined: each item and\n * field on its own line, indented two spaces a level; an empty array or object (fields all\n * undefined included) as \"[]\" / \"{}\".\n *\n * @ensures exactly the model's FileText (in the link); a leaf is its own text\n */\nexport function fileText(v: JsonNode, d: number): string {\n //@ verify\n //@ type d nat\n //@ requires d >= 0\n //@ ensures v.kind !== \"array\" && v.kind !== \"object\" ==> \\result === v.text\n if (v.kind === \"array\") {\n if (v.items.length === 0) return \"[]\";\n const items = fileItems(v.items, d + 1);\n return (\n \"[\" + lineFeed() + joinFrom(items, \",\" + lineFeed(), 0) + lineFeed() + spaces(2 * d) + \"]\"\n );\n }\n if (v.kind === \"object\") {\n const members = fileFields(v.fields, d + 1);\n if (members.length === 0) return \"{}\";\n return (\n \"{\" + lineFeed() + joinFrom(members, \",\" + lineFeed(), 0) + lineFeed() + spaces(2 * d) + \"}\"\n );\n }\n return v.text;\n}\n\nfunction fileItems(xs: JsonNode[], d: number): string[] {\n //@ verify\n //@ type d nat\n //@ requires d >= 0\n //@ ensures \\result.length === xs.length\n if (xs.length === 0) return [];\n const x = xs[0] as JsonNode;\n return [\n spaces(2 * d) + (x.kind === \"undefined\" ? \"null\" : fileText(x, d)),\n ...fileItems(xs.slice(1), d),\n ];\n}\n\nfunction fileFields(fs: JsonField[], d: number): string[] {\n //@ verify\n //@ type d nat\n //@ requires d >= 0\n //@ ensures \\result.length <= fs.length\n if (fs.length === 0) return [];\n const f = fs[0] as JsonField;\n if (f.value.kind === \"undefined\") return fileFields(fs.slice(1), d);\n return [spaces(2 * d) + f.keyText + \": \" + fileText(f.value, d), ...fileFields(fs.slice(1), d)];\n}\n\n/**\n * A JSON file's text (`jsonFileText`): `JSON.stringify(v, null, 2)` and one final newline; JS\n * writes \"undefined\" for undefined.\n *\n * @ensures exactly the model's JsonFile (in the link)\n */\nexport function jsonFile(v: JsonNode): string {\n //@ verify\n //@ ensures \\result === (v.kind === \"undefined\" ? \"undefined\" : fileText(v, 0)) + lineFeed()\n return (v.kind === \"undefined\" ? \"undefined\" : fileText(v, 0)) + lineFeed();\n}\n\n/**\n * A non-string value as prose (`jsonText` once the string case is read off): its JSON text, an\n * undefined value as \"null\".\n *\n * @ensures exactly the model's ProseText\n */\nexport function proseText(v: JsonNode): string {\n //@ verify\n //@ ensures \\result === (v.kind === \"undefined\" ? \"null\" : compactText(v))\n return v.kind === \"undefined\" ? \"null\" : compactText(v);\n}\n\n/**\n * Canonical JSON: keys sorted by UTF-16 code unit, undefined fields left out, no whitespace; an\n * undefined item written \"\" (as `canonicalJson` does: `join` writes undefined as \"\", so\n * [undefined, 1] is \"[,1]\"; a model finding, 2026-11-06).\n *\n * @ensures exactly the model's CanonicalText (in the link); a leaf is its own text\n */\nexport function canonicalText(v: JsonNode): string {\n //@ verify\n //@ ensures v.kind !== \"array\" && v.kind !== \"object\" && v.kind !== \"undefined\" ==> \\result === v.text\n //@ ensures v.kind === \"undefined\" ==> \\result === \"\"\n if (v.kind === \"array\") return \"[\" + joinFrom(canonicalItems(v.items), \",\", 0) + \"]\";\n if (v.kind === \"object\") {\n return \"{\" + joinFrom(memberTexts(sortedMembers(canonicalMembers(v.fields))), \",\", 0) + \"}\";\n }\n if (v.kind === \"undefined\") return \"\";\n return v.text;\n}\n\nfunction canonicalItems(xs: JsonNode[]): string[] {\n //@ verify\n //@ ensures \\result.length === xs.length\n if (xs.length === 0) return [];\n return [canonicalText(xs[0] as JsonNode), ...canonicalItems(xs.slice(1))];\n}\n\n/** A written member: its field's key (for sorting) and its text. */\ninterface KeyedText {\n key: string;\n text: string;\n}\n\nfunction canonicalMembers(fs: JsonField[]): KeyedText[] {\n //@ verify\n //@ ensures \\result.length <= fs.length\n if (fs.length === 0) return [];\n const f = fs[0] as JsonField;\n if (f.value.kind === \"undefined\") return canonicalMembers(fs.slice(1));\n return [\n { key: f.key, text: f.keyText + \":\" + canonicalText(f.value) },\n ...canonicalMembers(fs.slice(1)),\n ];\n}\n\nfunction sortedMembers(ms: KeyedText[]): KeyedText[] {\n //@ verify\n //@ ensures \\result.length === ms.length\n if (ms.length === 0) return [];\n return insertMember(ms[0] as KeyedText, sortedMembers(ms.slice(1)));\n}\n\nfunction insertMember(m: KeyedText, sorted: KeyedText[]): KeyedText[] {\n //@ verify\n //@ ensures \\result.length === sorted.length + 1\n if (sorted.length === 0) return [m];\n const first = sorted[0] as KeyedText;\n if (compareFrom(m.key, first.key, 0) <= 0) return [m, ...sorted];\n return [first, ...insertMember(m, sorted.slice(1))];\n}\n\nfunction memberTexts(ms: KeyedText[]): string[] {\n //@ verify\n //@ ensures \\result.length === ms.length\n if (ms.length === 0) return [];\n return [(ms[0] as KeyedText).text, ...memberTexts(ms.slice(1))];\n}\n","//@ backend dafny\nimport { codeAt, indexOfCode, lineFeed, untilCode } from \"@atlas/utils/strings\";\nimport { joinFrom } from \"@atlas/utils/lists\";\nimport { intText } from \"@atlas/foundation/decimal\";\nimport { afterBinMark, binAt } from \"./bin-scan\";\nimport { fieldValue, type JsonField, type JsonNode } from \"@atlas/utils/json-node\";\nimport { replaceCode } from \"@atlas/utils/replace\";\nimport { fixedUnits, maxPlaces, places, pow10, signedRescale } from \"@atlas/utils/fixed\";\n\n/* The components' decisions, as pure functions verified with LemmaScript → Dafny (model:\n models/AugurWeb.dfy; link: models/links/augur-web-rules.dfy). JSX, the DOM, timers and\n `Math.random` stay in the components; quantities are integer counts of 10^-decimals units\n (utils `fixed`), so no `parseFloat` or `toFixed` decides one (atlas#78). */\n\n/** One entry of a pagination bar: a page number or an ellipsis. */\nexport type PageEntry = { kind: \"page\"; n: number } | { kind: \"ellipsis\" };\n\n/**\n * Pages i + 1 … total, in order.\n *\n * @ensures exactly the model's Range1\n */\nexport function pageRangeFrom(total: number, i: number): PageEntry[] {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases total - i\n //@ ensures i >= total ==> \\result.length === 0\n //@ ensures i < total ==> \\result.length === total - i\n if (i >= total) return [];\n return [{ kind: \"page\", n: i + 1 }, ...pageRangeFrom(total, i + 1)];\n}\n\n/**\n * getPageNumbers: none for no pages; all of them up to 5; else the first, the last, and a window\n * around the (clamped) current page with ellipses.\n *\n * @ensures exactly the model's PageNumbers\n */\nexport function pageNumbersOf(current: number, total: number): PageEntry[] {\n //@ verify\n //@ ensures total <= 0 ==> \\result.length === 0\n //@ ensures total === 1 ==> \\result.length === 1\n if (total <= 0) return [];\n if (total === 1) return [{ kind: \"page\", n: 1 }];\n const page = clampPage(current, total);\n if (total <= 5) return pageRangeFrom(total, 0);\n const first: PageEntry = { kind: \"page\", n: 1 };\n const last: PageEntry = { kind: \"page\", n: total };\n const gap: PageEntry = { kind: \"ellipsis\" };\n if (page <= 3) {\n return [\n first,\n { kind: \"page\", n: 2 },\n { kind: \"page\", n: 3 },\n { kind: \"page\", n: 4 },\n gap,\n last,\n ];\n }\n if (page >= total - 2) {\n return [\n first,\n gap,\n { kind: \"page\", n: total - 3 },\n { kind: \"page\", n: total - 2 },\n { kind: \"page\", n: total - 1 },\n last,\n ];\n }\n return [\n first,\n gap,\n { kind: \"page\", n: page - 1 },\n { kind: \"page\", n: page },\n { kind: \"page\", n: page + 1 },\n gap,\n last,\n ];\n}\n\n/**\n * The current page clamped to [1, total] (`Math.max(1, Math.min(current, total))`).\n *\n * @ensures result = 1 below 1, total above it, else current\n */\nexport function clampPage(current: number, total: number): number {\n //@ verify\n //@ ensures current > total && total >= 1 ==> \\result === total\n //@ ensures current > total && total < 1 ==> \\result === 1\n //@ ensures current <= total && current < 1 ==> \\result === 1\n //@ ensures current <= total && current >= 1 ==> \\result === current\n const capped = current > total ? total : current;\n return capped < 1 ? 1 : capped;\n}\n\n/**\n * ListPagination's first page: at or before page 1 (on the unclamped page).\n *\n * @ensures exactly the model's IsFirstPage\n */\nexport function isFirstPage(page: number): boolean {\n //@ verify\n //@ ensures \\result === (page <= 1)\n return page <= 1;\n}\n\n/**\n * ListPagination's last page: at or past the total (on the unclamped page).\n *\n * @ensures exactly the model's IsLastPage\n */\nexport function isLastPage(page: number, totalPages: number): boolean {\n //@ verify\n //@ ensures \\result === (page >= totalPages)\n return page >= totalPages;\n}\n\n/**\n * The Prev link's target: page 1 on the first page, else the one before.\n *\n * @ensures exactly the model's PrevTarget\n */\nexport function prevTarget(page: number): number {\n //@ verify\n //@ ensures page <= 1 ==> \\result === 1\n //@ ensures page > 1 ==> \\result === page - 1\n return isFirstPage(page) ? 1 : page - 1;\n}\n\n/**\n * The Next link's target: the last page on it, else the one after.\n *\n * @ensures exactly the model's NextTarget\n */\nexport function nextTarget(page: number, totalPages: number): number {\n //@ verify\n //@ ensures page >= totalPages ==> \\result === totalPages\n //@ ensures page < totalPages ==> \\result === page + 1\n return isLastPage(page, totalPages) ? totalPages : page + 1;\n}\n\n/** One item of ListPagination's bar: an ellipsis or a page link, with its React key. */\ninterface BarItem {\n key: string;\n n: number;\n active: boolean;\n ellipsis: number[];\n page: number[];\n}\n\n/**\n * The bar's items from entry `i`: an ellipsis keyed by its position, or a page link keyed by its\n * number and active on the current page.\n *\n * @ensures exactly the model's BarItemsFrom (in the link)\n */\nfunction barItemsFrom(es: PageEntry[], current: number, i: number): BarItem[] {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases es.length - i\n //@ ensures i >= es.length ==> \\result.length === 0\n if (i >= es.length) return [];\n const e = es[i] as PageEntry;\n const item: BarItem =\n e.kind === \"ellipsis\"\n ? { key: \"ellipsis-\" + intText(i), n: 0, active: false, ellipsis: [0], page: [] }\n : { key: intText(e.n), n: e.n, active: e.n === current, ellipsis: [], page: [e.n] };\n return [item, ...barItemsFrom(es, current, i + 1)];\n}\n\n/** ListPagination's bar. */\nexport interface PaginationBar {\n shown: number[];\n prevTarget: number;\n prevOff: boolean;\n nextTarget: number;\n nextOff: boolean;\n items: BarItem[];\n}\n\n/**\n * ListPagination's bar: shown only with more than one page; Prev and Next with their targets,\n * off at the ends; one item per page number entry.\n *\n * @ensures exactly the model's PaginationBarOf (in the link)\n */\nexport function paginationBarOf(page: number, totalPages: number): PaginationBar {\n //@ verify\n //@ ensures totalPages <= 1 ==> \\result.shown.length === 0\n return {\n shown: slotsWhen(totalPages > 1),\n prevTarget: prevTarget(page),\n prevOff: isFirstPage(page),\n nextTarget: nextTarget(page, totalPages),\n nextOff: isLastPage(page, totalPages),\n items: barItemsFrom(pageNumbersOf(page, totalPages), page, 0),\n };\n}\n\n/**\n * An end of the bar that is off is dimmed and unclickable.\n *\n * @ensures exactly the model's DisabledClass\n */\nexport function disabledClass(off: boolean): string | undefined {\n //@ verify\n //@ ensures off ==> \\result === \"pointer-events-none opacity-50\"\n //@ ensures !off ==> \\result === undefined\n return off ? \"pointer-events-none opacity-50\" : undefined;\n}\n\n/**\n * An end of the bar that is off is announced disabled; one that is on says nothing.\n *\n * @ensures exactly the model's AriaDisabled\n */\nexport function ariaDisabled(off: boolean): boolean | undefined {\n //@ verify\n //@ ensures off ==> \\result === true\n //@ ensures !off ==> \\result === undefined\n return off ? true : undefined;\n}\n\n/** What a click on a bar link does. */\nexport type ClickAction = \"block\" | \"handle\" | \"follow\";\n\n/**\n * A click on a bar link: an end that is off is blocked; with a page callback the link is\n * handled in place; otherwise the browser follows the href.\n *\n * @ensures exactly the model's ClickActionOf\n */\nexport function clickActionOf(off: boolean, hasCallback: boolean): ClickAction {\n //@ verify\n //@ ensures off ==> \\result === \"block\"\n //@ ensures !off && hasCallback ==> \\result === \"handle\"\n //@ ensures !off && !hasCallback ==> \\result === \"follow\"\n if (off) return \"block\";\n return hasCallback ? \"handle\" : \"follow\";\n}\n\n/**\n * commit's clamp: at least min, then at most max when there is one (max wins if max < min).\n *\n * @ensures exactly the model's Clamp\n */\nexport function clampQuantity(value: number, min: number, max: number | undefined): number {\n //@ verify\n //@ ensures max === undefined ==> \\result === (value < min ? min : value)\n //@ ensures max !== undefined && (value < min ? min : value) > max ==> \\result === max\n //@ ensures max !== undefined && (value < min ? min : value) <= max ==> \\result === (value < min ? min : value)\n const atLeast = value < min ? min : value;\n if (max === undefined) return atLeast;\n return atLeast > max ? max : atLeast;\n}\n\n/**\n * The decrease stepper: one step down, not below min (`Math.max(value - step, min)`).\n *\n * @ensures exactly the model's StepDown\n */\nexport function stepDown(value: number, step: number, min: number): number {\n //@ verify\n //@ ensures value - step > min ==> \\result === value - step\n //@ ensures value - step <= min ==> \\result === min\n return value - step > min ? value - step : min;\n}\n\n/**\n * The increase stepper: one step up, not above max when there is one.\n *\n * @ensures exactly the model's StepUp\n */\nexport function stepUp(value: number, step: number, max: number | undefined): number {\n //@ verify\n //@ ensures max === undefined ==> \\result === value + step\n //@ ensures max !== undefined && value + step > max ==> \\result === max\n //@ ensures max !== undefined && value + step <= max ==> \\result === value + step\n if (max === undefined) return value + step;\n return value + step > max ? max : value + step;\n}\n\n/**\n * The decrease button is off when the input is, or the value is already at the minimum.\n *\n * @ensures exactly the model's MinDisabled\n */\nexport function decreaseDisabled(disabled: boolean, value: number, min: number): boolean {\n //@ verify\n //@ ensures \\result === (disabled || value <= min)\n return disabled || value <= min;\n}\n\n/**\n * The increase button is off when the input is, or the value is already at the maximum.\n *\n * @ensures exactly the model's MaxDisabled\n */\nexport function increaseDisabled(\n disabled: boolean,\n value: number,\n max: number | undefined,\n): boolean {\n //@ verify\n //@ ensures max === undefined ==> \\result === disabled\n //@ ensures max !== undefined ==> \\result === (disabled || value >= max)\n if (max === undefined) return disabled;\n return disabled || value >= max;\n}\n\n/**\n * The quantity input's digit test, JS's `key >= \"0\" && key <= \"9\"` (string order): a first code\n * unit 0–8 with anything after it, or exactly \"9\".\n *\n * @ensures exactly the model's KeyAllowed digit clause\n */\nexport function digitKey(key: string): boolean {\n //@ verify\n //@ ensures \\result === ((codeAt(key, 0) >= 48 && codeAt(key, 0) <= 56) || key === \"9\")\n const c = codeAt(key, 0);\n return (c >= 48 && c <= 56) || key === \"9\";\n}\n\n/**\n * The editing and navigation keys the input always lets through.\n *\n * @ensures result ⟺ key ∈ {Backspace, Delete, ArrowLeft, ArrowRight, Tab, Enter}\n */\nexport function editingKey(key: string): boolean {\n //@ verify\n //@ ensures \\result === (key === \"Backspace\" || key === \"Delete\" || key === \"ArrowLeft\" || key === \"ArrowRight\" || key === \"Tab\" || key === \"Enter\")\n return (\n key === \"Backspace\" ||\n key === \"Delete\" ||\n key === \"ArrowLeft\" ||\n key === \"ArrowRight\" ||\n key === \"Tab\" ||\n key === \"Enter\"\n );\n}\n\n/**\n * A keypress is allowed: an editing key, one \".\" when decimals are allowed, or a digit.\n *\n * @ensures exactly the model's KeyAllowed\n */\nexport function keyAllowed(key: string, decimals: number, hasDot: boolean): boolean {\n //@ verify\n //@ ensures \\result === (key === \"Backspace\" || key === \"Delete\" || key === \"ArrowLeft\" || key === \"ArrowRight\" || key === \"Tab\" || key === \"Enter\" || (decimals > 0 && key === \".\" && !hasDot) || (codeAt(key, 0) >= 48 && codeAt(key, 0) <= 56) || key === \"9\")\n return editingKey(key) || (decimals > 0 && key === \".\" && !hasDot) || digitKey(key);\n}\n\n/** What a keypress in the quantity input does. */\nexport type KeyAction = \"commit\" | \"allow\" | \"block\";\n\n/**\n * Enter commits, an allowed key types (the text has a \".\" already when it holds one), anything\n * else is blocked.\n *\n * @ensures exactly the model's KeyActionOf\n */\nexport function keyActionOf(key: string, decimals: number, text: string): KeyAction {\n //@ verify\n //@ ensures key === \"Enter\" ==> \\result === \"commit\"\n //@ ensures key !== \"Enter\" && (key === \"Backspace\" || key === \"Delete\" || key === \"ArrowLeft\" || key === \"ArrowRight\" || key === \"Tab\" || (decimals > 0 && key === \".\" && indexOfCode(text, 46, 0) < 0) || (codeAt(key, 0) >= 48 && codeAt(key, 0) <= 56) || key === \"9\") ==> \\result === \"allow\"\n //@ ensures key !== \"Enter\" && !(key === \"Backspace\" || key === \"Delete\" || key === \"ArrowLeft\" || key === \"ArrowRight\" || key === \"Tab\" || (decimals > 0 && key === \".\" && indexOfCode(text, 46, 0) < 0) || (codeAt(key, 0) >= 48 && codeAt(key, 0) <= 56) || key === \"9\") ==> \\result === \"block\"\n if (key === \"Enter\") return \"commit\";\n return keyAllowed(key, decimals, indexOfCode(text, 46, 0) >= 0) ? \"allow\" : \"block\";\n}\n\n/**\n * `v` rounded up to the next multiple of `s` (the model's CeilDiv(v, s) · s).\n *\n * @ensures result ≥ v, a multiple of s; exactly the model's RoundToStep for s > 1 (in the link)\n */\nexport function roundUpTo(v: number, s: number): number {\n //@ verify\n //@ type v nat\n //@ requires v >= 0 && s >= 1\n //@ ensures \\result >= 0\n const q = Math.floor(v / s);\n return (v % s === 0 ? q : q + 1) * s;\n}\n\n/**\n * Typed text worth `u` units of 10^-k, at p = max(k, d) places, rounded up to the step (`step`\n * in 10^-d units); a step of one whole unit or less leaves it.\n *\n * @ensures exactly the model's CommitUnits before the clamp (in the link)\n */\nexport function roundedUnits(u: number, k: number, step: number, d: number): number {\n //@ verify\n //@ type u nat\n //@ type k nat\n //@ type d nat\n //@ requires u >= 0 && k >= 0 && d >= 0\n //@ ensures \\result >= 0\n const p = maxPlaces(k, d);\n const v = u * pow10(p - k);\n const s = step * pow10(p - d);\n return s <= pow10(p) ? v : roundUpTo(v, s);\n}\n\n/**\n * commit over the typed text, every quantity in 10^-d units: text that isn't fixed-point is\n * `min`; else rounded up to the step, clamped to [min, max], then rounded half up to d places.\n *\n * @ensures exactly the model's CommitUnits (in the link)\n */\nexport function commitUnits(\n text: string,\n min: number,\n max: number | undefined,\n step: number,\n d: number,\n): number {\n //@ verify\n //@ type d nat\n //@ requires d >= 0\n //@ ensures fixedUnits(text) < 0 ==> \\result === min\n const u = fixedUnits(text);\n if (u < 0) return min;\n const k = places(text);\n const p = maxPlaces(k, d);\n const up = pow10(p - d);\n const clamped = clampQuantity(\n roundedUnits(u, k, step, d),\n min * up,\n max === undefined ? undefined : max * up,\n );\n return signedRescale(clamped, p, d);\n}\n\n/**\n * A slot list markup maps over in place of `show && <…>`: one slot when shown, none otherwise\n * (QuantityInput's steppers, FormField's required marker), so the markup holds no `&&`.\n *\n * @ensures exactly the model's SlotsWhen\n */\nexport function slotsWhen(show: boolean): number[] {\n //@ verify\n //@ ensures show ==> \\result.length === 1 && \\result[0] === 0\n //@ ensures !show ==> \\result.length === 0\n return show ? [0] : [];\n}\n\n/**\n * A text slot: the text, when there is some (FormField's error message).\n *\n * @ensures exactly the model's TextSlots\n */\nexport function textSlots(text: string | undefined): string[] {\n //@ verify\n //@ ensures text === undefined ==> \\result.length === 0\n //@ ensures text !== undefined && text === \"\" ==> \\result.length === 0\n //@ ensures text !== undefined && text !== \"\" ==> \\result.length === 1 && \\result[0] === text\n return text !== undefined && text !== \"\" ? [text] : [];\n}\n\n/** The detail panel's slots. */\nexport interface PanelSlots {\n header: number[];\n title: string[];\n editButton: number[];\n actions: number[];\n edit: number[];\n read: number[];\n}\n\n/**\n * The detail panel's slots: the header row shows with a title or while reading; the Edit button\n * while reading; Save and Cancel, and each field's input, while editing; each field's value while\n * reading.\n *\n * @ensures exactly the model's PanelSlotsOf\n */\nexport function panelSlotsOf(title: string | undefined, editing: boolean): PanelSlots {\n //@ verify\n //@ ensures editing ==> \\result.editButton.length === 0 && \\result.actions.length === 1\n const t = textSlots(title);\n return {\n header: slotsWhen(t.length > 0 || !editing),\n title: t,\n editButton: slotsWhen(!editing),\n actions: slotsWhen(editing),\n edit: slotsWhen(editing),\n read: slotsWhen(!editing),\n };\n}\n\n/** Which input a field edits with. */\nexport interface FieldSlots {\n readonly: number[];\n select: number[];\n textarea: number[];\n input: number[];\n}\n\n/**\n * Which input a field edits with: shown read-only, a select, a textarea, else a text input.\n *\n * @ensures exactly the model's FieldSlotsOf\n */\nexport function fieldSlotsOf(kind: string): FieldSlots {\n //@ verify\n //@ ensures kind === \"select\" ==> \\result.select.length === 1 && \\result.input.length === 0\n const other = kind !== \"readonly\" && kind !== \"select\" && kind !== \"textarea\";\n return {\n readonly: slotsWhen(kind === \"readonly\"),\n select: slotsWhen(kind === \"select\"),\n textarea: slotsWhen(kind === \"textarea\"),\n input: slotsWhen(other),\n };\n}\n\n/** ResourceTabs' view: whether it shows, and the tab it opens on. */\nexport interface TabsView {\n shown: number[];\n defaultTab: string;\n}\n\n/**\n * ResourceTabs shows with at least one tab, opening on the given tab, else the first.\n *\n * @ensures exactly the model's TabsViewOf\n */\nexport function tabsViewOf(defaultTab: string | undefined, values: string[]): TabsView {\n //@ verify\n //@ ensures values.length === 0 ==> \\result.shown.length === 0\n //@ ensures defaultTab !== undefined ==> \\result.defaultTab === defaultTab\n const first = values.length > 0 ? (values[0] as string) : \"\";\n return {\n shown: slotsWhen(values.length > 0),\n defaultTab: defaultTab !== undefined ? defaultTab : first,\n };\n}\n\n/**\n * A form control's id: its own id, else its name, else \"\".\n *\n * @ensures exactly the model's FieldId\n */\nexport function fieldId(id: string | undefined, name: string | undefined): string {\n //@ verify\n //@ ensures id !== undefined ==> \\result === id\n //@ ensures id === undefined && name !== undefined ==> \\result === name\n //@ ensures id === undefined && name === undefined ==> \\result === \"\"\n if (id !== undefined) return id;\n return name !== undefined ? name : \"\";\n}\n\n/** The on-screen keyboard a quantity input asks for. */\nexport type InputMode = \"decimal\" | \"numeric\";\n\n/**\n * The on-screen keyboard: decimal when the field has places, else numeric.\n *\n * @ensures exactly the model's InputModeOf\n */\nexport function inputModeOf(decimals: number): InputMode {\n //@ verify\n //@ ensures decimals > 0 ==> \\result === \"decimal\"\n //@ ensures decimals <= 0 ==> \\result === \"numeric\"\n return decimals > 0 ? \"decimal\" : \"numeric\";\n}\n\n/**\n * handleChange from index `i`: digits only, plus the first \".\" when decimals are allowed.\n *\n * @ensures exactly the model's CleanQtyFrom\n */\nexport function cleanQtyFrom(raw: string, allowDot: boolean, seenDot: boolean, i: number): string {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases raw.length - i\n //@ ensures i >= raw.length ==> \\result === \"\"\n if (i >= raw.length) return \"\";\n const c = codeAt(raw, i);\n if (c >= 48 && c <= 57) return raw.slice(i, i + 1) + cleanQtyFrom(raw, allowDot, seenDot, i + 1);\n if (allowDot && c === 46 && !seenDot) return \".\" + cleanQtyFrom(raw, allowDot, true, i + 1);\n return cleanQtyFrom(raw, allowDot, seenDot, i + 1);\n}\n\n/**\n * The spinner's size classes: sm 12px, md 16px (the default), lg 24px.\n *\n * @ensures exactly the model's SpinnerSizeClass\n */\nexport function spinnerSizeClass(size: string): string {\n //@ verify\n //@ ensures size === \"sm\" ==> \\result === \"h-3 w-3\"\n //@ ensures size === \"lg\" ==> \\result === \"h-6 w-6\"\n //@ ensures size !== \"sm\" && size !== \"lg\" ==> \\result === \"h-4 w-4\"\n if (size === \"sm\") return \"h-3 w-3\";\n return size === \"lg\" ? \"h-6 w-6\" : \"h-4 w-4\";\n}\n\n/**\n * A separator is one pixel high across, or one pixel wide down (the default is across).\n *\n * @ensures exactly the model's SeparatorClass\n */\nexport function separatorClass(orientation: string): string {\n //@ verify\n //@ ensures orientation === \"vertical\" ==> \\result === \"h-full w-[1px]\"\n //@ ensures orientation !== \"vertical\" ==> \\result === \"h-[1px] w-full\"\n return orientation === \"vertical\" ? \"h-full w-[1px]\" : \"h-[1px] w-full\";\n}\n\n/**\n * A pagination link's aria-current: \"page\" on the current page, none elsewhere.\n *\n * @ensures exactly the model's PaginationAriaCurrent\n */\nexport function paginationAriaCurrent(isActive: boolean): string | undefined {\n //@ verify\n //@ ensures isActive ==> \\result === \"page\"\n //@ ensures !isActive ==> \\result === undefined\n return isActive ? \"page\" : undefined;\n}\n\n/**\n * A pagination link's button variant: outlined on the current page, a ghost elsewhere.\n *\n * @ensures exactly the model's PaginationVariant\n */\nexport function paginationVariant(isActive: boolean): \"outline\" | \"ghost\" {\n //@ verify\n //@ ensures isActive ==> \\result === \"outline\"\n //@ ensures !isActive ==> \\result === \"ghost\"\n return isActive ? \"outline\" : \"ghost\";\n}\n\n/**\n * A menu item, label or sub-trigger set in to line up with checkable items.\n *\n * @ensures exactly the model's InsetClass\n */\nexport function insetClass(inset: boolean): string {\n //@ verify\n //@ ensures inset ==> \\result === \"pl-8\"\n //@ ensures !inset ==> \\result === \"\"\n return inset ? \"pl-8\" : \"\";\n}\n\n/**\n * A select's popper-positioned content shifts off its trigger (popper is the default).\n *\n * @ensures exactly the model's SelectContentClass\n */\nexport function selectContentClass(position: string): string {\n //@ verify\n //@ ensures position === \"popper\" ==> \\result === \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\"\n //@ ensures position !== \"popper\" ==> \\result === \"\"\n return position === \"popper\"\n ? \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\"\n : \"\";\n}\n\n/**\n * A select's popper-positioned viewport takes its trigger's size.\n *\n * @ensures exactly the model's SelectViewportClass\n */\nexport function selectViewportClass(position: string): string {\n //@ verify\n //@ ensures position === \"popper\" ==> \\result === \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n //@ ensures position !== \"popper\" ==> \\result === \"\"\n return position === \"popper\"\n ? \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n : \"\";\n}\n\n/** What a stock label shows. */\nexport type StockView = \"loading\" | \"out-of-stock\" | \"low-stock\" | \"in-stock\";\n\n/**\n * Loading first; out of stock overrides low stock.\n *\n * @ensures exactly the model's StockViewOf\n */\nexport function stockViewOf(loading: boolean, isInStock: boolean, isLowStock: boolean): StockView {\n //@ verify\n //@ ensures loading ==> \\result === \"loading\"\n //@ ensures !loading && !isInStock ==> \\result === \"out-of-stock\"\n //@ ensures !loading && isInStock && isLowStock ==> \\result === \"low-stock\"\n //@ ensures !loading && isInStock && !isLowStock ==> \\result === \"in-stock\"\n if (loading) return \"loading\";\n if (!isInStock) return \"out-of-stock\";\n return isLowStock ? \"low-stock\" : \"in-stock\";\n}\n\n/**\n * The quantity shows only when given and the item is in stock (low or not).\n *\n * @ensures exactly the model's ShowStockQuantity\n */\nexport function showStockQuantity(view: StockView, hasQuantity: boolean): boolean {\n //@ verify\n //@ ensures \\result === (hasQuantity && view !== \"out-of-stock\" && view !== \"loading\")\n return hasQuantity && view !== \"out-of-stock\" && view !== \"loading\";\n}\n\n/** The stock label's slots. */\nexport interface StockSlots {\n skeleton: number[];\n outOfStock: number[];\n lowStock: number[];\n inStock: number[];\n quantity: number[];\n}\n\n/**\n * The stock label's slots: one per view (exactly one holds a slot), plus the quantity\n * parenthetical where `showStockQuantity` shows it.\n *\n * @ensures exactly the model's StockSlotsOf\n */\nexport function stockSlotsOf(\n loading: boolean,\n isInStock: boolean,\n isLowStock: boolean,\n hasQuantity: boolean,\n): StockSlots {\n //@ verify\n //@ ensures loading ==> \\result.skeleton.length === 1 && \\result.quantity.length === 0\n const view = stockViewOf(loading, isInStock, isLowStock);\n return {\n skeleton: slotsWhen(view === \"loading\"),\n outOfStock: slotsWhen(view === \"out-of-stock\"),\n lowStock: slotsWhen(view === \"low-stock\"),\n inStock: slotsWhen(view === \"in-stock\"),\n quantity: slotsWhen(showStockQuantity(view, hasQuantity)),\n };\n}\n\n/** What a price shows. */\nexport type PriceView = \"skeleton\" | \"zero-label\" | \"amount\";\n\n/**\n * Loading first; a 0 value with no original price shows the zero label (\"Call for Price\").\n *\n * @ensures exactly the model's PriceViewOf\n */\nexport function priceViewOf(\n loading: boolean,\n valueIsZero: boolean,\n hasOriginal: boolean,\n): PriceView {\n //@ verify\n //@ ensures loading ==> \\result === \"skeleton\"\n //@ ensures !loading && valueIsZero && !hasOriginal ==> \\result === \"zero-label\"\n //@ ensures !loading && !(valueIsZero && !hasOriginal) ==> \\result === \"amount\"\n if (loading) return \"skeleton\";\n if (valueIsZero && !hasOriginal) return \"zero-label\";\n return \"amount\";\n}\n\n/**\n * A CSV field needs quotes iff it holds a comma, a quote or a line feed (a carriage return alone\n * does not).\n *\n * @ensures exactly the model's NeedsQuotes\n */\nexport function needsQuotes(s: string): boolean {\n //@ verify\n //@ ensures \\result === (indexOfCode(s, 44, 0) >= 0 || indexOfCode(s, 34, 0) >= 0 || indexOfCode(s, 10, 0) >= 0)\n return indexOfCode(s, 44, 0) >= 0 || indexOfCode(s, 34, 0) >= 0 || indexOfCode(s, 10, 0) >= 0;\n}\n\n/**\n * A CSV field: quoted, with quotes doubled, when it needs quotes; else as is.\n *\n * @ensures exactly the model's CsvField\n */\nexport function csvField(s: string): string {\n //@ verify\n //@ ensures (indexOfCode(s, 44, 0) >= 0 || indexOfCode(s, 34, 0) >= 0 || indexOfCode(s, 10, 0) >= 0) ==> \\result === \"\\\"\" + replaceCode(s, 34, \"\\\"\\\"\") + \"\\\"\"\n //@ ensures !(indexOfCode(s, 44, 0) >= 0 || indexOfCode(s, 34, 0) >= 0 || indexOfCode(s, 10, 0) >= 0) ==> \\result === s\n if (!needsQuotes(s)) return s;\n return '\"' + replaceCode(s, 34, '\"\"') + '\"';\n}\n\n/**\n * The CSV export's lines from index `i`: each row's (already `csvField`-escaped) fields joined\n * by \",\".\n *\n * @ensures exactly the model's CsvLines (in the link)\n */\nexport function csvLines(rows: string[][], i: number): string[] {\n //@ verify\n //@ type i nat\n //@ requires i >= 0\n //@ decreases rows.length - i\n //@ ensures i >= rows.length ==> \\result.length === 0\n if (i >= rows.length) return [];\n return [joinFrom(rows[i] as string[], \",\", 0), ...csvLines(rows, i + 1)];\n}\n\n/**\n * The CSV export's text: the header line (the headers joined as they are), then the rows,\n * joined by line feeds.\n *\n * @ensures exactly the model's CsvText (in the link)\n */\nexport function csvText(headers: string[], rows: string[][]): string {\n //@ verify\n //@ ensures rows.length === 0 ==> \\result === joinFrom(headers, \",\", 0)\n return joinFrom([joinFrom(headers, \",\", 0), ...csvLines(rows, 0)], lineFeed(), 0);\n}\n\n/** A column's sort direction. */\nexport type SortDir = \"asc\" | \"desc\" | \"unsorted\";\n\n/**\n * aria-sort: only on a sortable column, naming its direction. A string, not a literal union:\n * lsc names a `\"none\"` literal `None`, which clashes with Option's (models/LSC-GAPS.md).\n *\n * @ensures exactly the model's AriaSort\n */\nexport function ariaSortOf(sortable: boolean, dir: SortDir): string | undefined {\n //@ verify\n //@ ensures !sortable ==> \\result === undefined\n //@ ensures sortable && dir === \"asc\" ==> \\result === \"ascending\"\n //@ ensures sortable && dir === \"desc\" ==> \\result === \"descending\"\n //@ ensures sortable && dir === \"unsorted\" ==> \\result === \"none\"\n if (!sortable) return undefined;\n if (dir === \"asc\") return \"ascending\";\n if (dir === \"desc\") return \"descending\";\n return \"none\";\n}\n\n/**\n * A column's cell alignment class from its meta's `align` (\"left\" when it has none); any other\n * value has none.\n *\n * @ensures exactly the model's AlignClass\n */\nexport function alignClass(align: string): string {\n //@ verify\n //@ ensures align === \"left\" ==> \\result === \"text-left\"\n //@ ensures align === \"center\" ==> \\result === \"text-center\"\n //@ ensures align === \"right\" ==> \\result === \"text-right\"\n //@ ensures align !== \"left\" && align !== \"center\" && align !== \"right\" ==> \\result === \"\"\n if (align === \"left\") return \"text-left\";\n if (align === \"center\") return \"text-center\";\n return align === \"right\" ? \"text-right\" : \"\";\n}\n\n/**\n * The toolbar shows when there is anything to put in it: search, a toolbar of the caller's, the\n * column toggle, the CSV export, or a selection.\n *\n * @ensures exactly the model's ToolbarShown\n */\nexport function toolbarShown(\n searchable: boolean,\n hasToolbar: boolean,\n columnToggle: boolean,\n exportCsv: boolean,\n selectedCount: number,\n): boolean {\n //@ verify\n //@ ensures \\result === (searchable || hasToolbar || columnToggle || exportCsv || selectedCount > 0)\n return searchable || hasToolbar || columnToggle || exportCsv || selectedCount > 0;\n}\n\n/**\n * A column's cell alignment class from its optional `align` (left when it has none).\n *\n * @ensures exactly the model's AlignClassOf\n */\nexport function alignClassOf(align: string | undefined): string {\n //@ verify\n //@ ensures align === undefined ==> \\result === \"text-left\"\n return alignClass(align === undefined ? \"left\" : align);\n}\n\n/**\n * A column sorts only when it says so (`sortable ?? false`).\n *\n * @ensures exactly the model's EnableSorting\n */\nexport function enableSorting(sortable: boolean | undefined): boolean {\n //@ verify\n //@ ensures sortable === undefined ==> \\result === false\n //@ ensures sortable !== undefined ==> \\result === sortable\n return sortable === undefined ? false : sortable;\n}\n\n/**\n * A column hides unless it says it can't (`hideable !== false`).\n *\n * @ensures exactly the model's EnableHiding\n */\nexport function enableHiding(hideable: boolean | undefined): boolean {\n //@ verify\n //@ ensures hideable === undefined ==> \\result === true\n //@ ensures hideable !== undefined ==> \\result === hideable\n return hideable === undefined ? true : hideable;\n}\n\n/** What the table body shows. */\nexport type BodyView = \"loading\" | \"empty\" | \"rows\";\n\n/**\n * Loading first, then the empty message for no rows.\n *\n * @ensures exactly the model's BodyViewOf\n */\nexport function bodyViewOf(loading: boolean, rowCount: number): BodyView {\n //@ verify\n //@ ensures loading ==> \\result === \"loading\"\n //@ ensures !loading && rowCount === 0 ==> \\result === \"empty\"\n //@ ensures !loading && rowCount !== 0 ==> \\result === \"rows\"\n if (loading) return \"loading\";\n return rowCount === 0 ? \"empty\" : \"rows\";\n}\n\n/** The table body's slots, one list per view. */\nexport interface ViewSlots {\n loading: number[];\n empty: number[];\n rows: number[];\n}\n\n/**\n * The table body's slots: exactly one view's list holds a slot, so the markup maps over each in\n * place of an `if` per view.\n *\n * @ensures exactly the model's BodySlots\n */\nexport function bodySlots(loading: boolean, rowCount: number): ViewSlots {\n //@ verify\n //@ ensures loading ==> \\result.loading.length === 1 && \\result.empty.length === 0 && \\result.rows.length === 0\n const view = bodyViewOf(loading, rowCount);\n return {\n loading: slotsWhen(view === \"loading\"),\n empty: slotsWhen(view === \"empty\"),\n rows: slotsWhen(view === \"rows\"),\n };\n}\n\n/** The sort indicator's icon slots. */\nexport interface SortSlots {\n up: number[];\n down: number[];\n both: number[];\n}\n\n/**\n * The sort indicator's icon: up for ascending, down for descending, both ways otherwise.\n *\n * @ensures exactly the model's SortSlotsOf\n */\nexport function sortSlotsOf(dir: SortDir): SortSlots {\n //@ verify\n //@ ensures dir === \"asc\" ==> \\result.up.length === 1 && \\result.down.length === 0 && \\result.both.length === 0\n return {\n up: slotsWhen(dir === \"asc\"),\n down: slotsWhen(dir === \"desc\"),\n both: slotsWhen(dir === \"unsorted\"),\n };\n}\n\n/**\n * The bulk actions bar shows with a selection and actions to offer.\n *\n * @ensures exactly the model's BulkBarShown\n */\nexport function bulkBarShown(selectedCount: number, hasBulkActions: boolean): boolean {\n //@ verify\n //@ ensures \\result === (selectedCount > 0 && hasBulkActions)\n return selectedCount > 0 && hasBulkActions;\n}\n\n/** When GTM loads. */\nexport type LoadTiming = \"idle\" | \"next-tick\" | \"now\";\n\n/**\n * GTM loads at idle for lazyOnload when the browser has requestIdleCallback, on the next tick for\n * afterInteractive, else at once.\n *\n * @ensures exactly the model's GtmTiming\n */\nexport function gtmTiming(strategy: string, hasIdleCallback: boolean): LoadTiming {\n //@ verify\n //@ ensures strategy === \"lazyOnload\" && hasIdleCallback ==> \\result === \"idle\"\n //@ ensures !(strategy === \"lazyOnload\" && hasIdleCallback) && strategy === \"afterInteractive\" ==> \\result === \"next-tick\"\n //@ ensures !(strategy === \"lazyOnload\" && hasIdleCallback) && strategy !== \"afterInteractive\" ==> \\result === \"now\"\n if (strategy === \"lazyOnload\" && hasIdleCallback) return \"idle\";\n return strategy === \"afterInteractive\" ? \"next-tick\" : \"now\";\n}\n\n/**\n * A widget script is injected unless disabled or already there (the financing widget).\n *\n * @ensures exactly the model's InjectAllowed\n */\nexport function injectAllowed(disabled: boolean, injected: boolean): boolean {\n //@ verify\n //@ ensures \\result === (!disabled && !injected)\n return !disabled && !injected;\n}\n\n/**\n * Session replay is injected unless disabled or already there, and, under a rate below 1, only\n * when the draw is at or under it.\n *\n * @ensures exactly the model's ReplaySampled\n */\nexport function replaySampled(\n disabled: boolean,\n injected: boolean,\n rate: number,\n draw: number,\n): boolean {\n //@ verify\n //@ type rate real\n //@ type draw real\n //@ ensures \\result === (!disabled && !injected && !(rate < 1 && draw > rate))\n return !disabled && !injected && !(rate < 1 && draw > rate);\n}\n\n/**\n * extractBin from a message string (`/[?&]Bin=([^&]+)/`): the first mark's value up to the next\n * \"&\" (never empty), or undefined.\n *\n * @ensures exactly the model's BinFromString\n */\nexport function binFromString(s: string): string | undefined {\n //@ verify\n //@ ensures binAt(s, 0) < 0 ==> \\result === undefined\n //@ ensures binAt(s, 0) >= 0 ==> \\result === untilCode(afterBinMark(s, binAt(s, 0)), 38)\n const k = binAt(s, 0);\n if (k < 0) return undefined;\n return untilCode(afterBinMark(s, k), 38);\n}\n\n/** The Unified/Element iframe's configuration. */\nexport interface UnifiedFrame {\n uiEndpoint: string;\n transactionSetupId: string;\n}\n\n/** The payment modal's frame: the iframe's URL, or the loading text. */\nexport interface PaymentFrame {\n src: string[];\n loading: number[];\n}\n\n/**\n * The payment modal's frame: with the Unified config, its iframe URL\n * (`<uiEndpoint>?TransactionSetupID=<id>`); without it, the loading text.\n *\n * @ensures exactly the model's PaymentFrameOf\n */\nexport function paymentFrameOf(unified: UnifiedFrame | null): PaymentFrame {\n //@ verify\n //@ ensures unified === null ==> \\result.src.length === 0 && \\result.loading.length === 1\n if (unified === null) return { src: [], loading: [0] };\n return {\n src: [unified.uiEndpoint + \"?TransactionSetupID=\" + unified.transactionSetupId],\n loading: [],\n };\n}\n\n/**\n * The modal listens for the iframe's BIN message only while open and with someone to tell.\n *\n * @ensures exactly the model's BinListenerOn\n */\nexport function binListenerOn(open: boolean, hasCapture: boolean): boolean {\n //@ verify\n //@ ensures \\result === (open && hasCapture)\n return open && hasCapture;\n}\n\n/**\n * A node's string when it is a non-empty one.\n *\n * @ensures exactly the model's NonEmptyText\n */\nexport function nonEmptyText(v: JsonNode): string | undefined {\n //@ verify\n //@ ensures (v.kind === \"string\" && v.str !== \"\") ==> \\result === v.str\n //@ ensures !(v.kind === \"string\" && v.str !== \"\") ==> \\result === undefined\n return v.kind === \"string\" && v.str !== \"\" ? v.str : undefined;\n}\n\n/**\n * extractBin from an object's fields: the first non-empty string of Bin, bin, CardBIN and\n * cardBin, else a non-empty string data.Bin.\n *\n * @ensures exactly the model's BinOfFields (in the link)\n */\nexport function binOfFields(fs: JsonField[]): string | undefined {\n //@ verify\n //@ ensures fs.length === 0 ==> \\result === undefined\n const a = nonEmptyText(fieldValue(fs, \"Bin\", 0));\n if (a !== undefined) return a;\n const b = nonEmptyText(fieldValue(fs, \"bin\", 0));\n if (b !== undefined) return b;\n const c = nonEmptyText(fieldValue(fs, \"CardBIN\", 0));\n if (c !== undefined) return c;\n const e = nonEmptyText(fieldValue(fs, \"cardBin\", 0));\n if (e !== undefined) return e;\n const data = fieldValue(fs, \"data\", 0);\n return data.kind === \"object\" ? nonEmptyText(fieldValue(data.fields, \"Bin\", 0)) : undefined;\n}\n\n/**\n * extractBin over a postMessage payload read into a JSON node: a string's BIN mark, an object's\n * BIN field, else none.\n *\n * @ensures exactly the model's BinOfNode (in the link)\n */\nexport function binOfNode(v: JsonNode): string | undefined {\n //@ verify\n //@ ensures v.kind !== \"string\" && v.kind !== \"object\" ==> \\result === undefined\n if (v.kind === \"string\") return binFromString(v.str);\n if (v.kind === \"object\") return binOfFields(v.fields);\n return undefined;\n}\n"],"mappings":"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","names":[]}
@@ -3,6 +3,10 @@
3
3
 
4
4
  var _chunkMAENILZPcjs = require('./chunk-MAENILZP.cjs');
5
5
 
6
+
7
+
8
+ var _chunkNBXA42AWcjs = require('./chunk-NBXA42AW.cjs');
9
+
6
10
  // src/form-field.tsx
7
11
  var _react = require('react'); var React = _interopRequireWildcard(_react);
8
12
  var _web = require('@simpleapps-com/augur-utils/web');
@@ -12,10 +16,10 @@ var FormField = React.forwardRef(
12
16
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { ref, className: _web.cn.call(void 0, "space-y-2", className), children: [
13
17
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkMAENILZPcjs.Label, { htmlFor, children: [
14
18
  label,
15
- required && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-destructive", children: "*" })
19
+ _chunkNBXA42AWcjs.slotsWhen.call(void 0, required === true).map((slot) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-destructive", children: "*" }, slot))
16
20
  ] }),
17
21
  children,
18
- error && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-destructive text-sm", children: error })
22
+ _chunkNBXA42AWcjs.textSlots.call(void 0, error).map((message) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-destructive text-sm", children: message }, message))
19
23
  ] });
20
24
  }
21
25
  );
@@ -24,4 +28,4 @@ FormField.displayName = "FormField";
24
28
 
25
29
 
26
30
  exports.FormField = FormField;
27
- //# sourceMappingURL=chunk-BXMTQGKV.cjs.map
31
+ //# sourceMappingURL=chunk-OJS2B6U2.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/stuartjackson/Projects/simpleapps/atlas/repo/packages/augur-web/dist/chunk-OJS2B6U2.cjs","../src/form-field.tsx"],"names":[],"mappings":"AAAA,uWAAY;AACZ;AACE;AACF,wDAA6B;AAC7B;AACE;AACA;AACF,wDAA6B;AAC7B;AACA;ACPA,2EAAuB;AACvB,sDAAmB;AAkBX,+CAAA;AAJR,IAAM,UAAA,EAAkB,KAAA,CAAA,UAAA;AAAA,EACtB,CAAC,EAAE,KAAA,EAAO,OAAA,EAAS,QAAA,EAAU,KAAA,EAAO,SAAA,EAAW,SAAS,CAAA,EAAG,GAAA,EAAA,GAAQ;AACjE,IAAA,uBACE,8BAAA,KAAC,EAAA,EAAI,GAAA,EAAU,SAAA,EAAW,qBAAA,WAAG,EAAa,SAAS,CAAA,EACjD,QAAA,EAAA;AAAA,sBAAA,8BAAA,uBAAC,EAAA,EAAM,OAAA,EACJ,QAAA,EAAA;AAAA,QAAA,KAAA;AAAA,QACA,yCAAA,SAAU,IAAa,IAAI,CAAA,CAAE,GAAA,CAAI,CAAC,IAAA,EAAA,mBACjC,6BAAA,MAAC,EAAA,EAAgB,SAAA,EAAU,kBAAA,EAAmB,QAAA,EAAA,IAAA,CAAA,EAAnC,IAEX,CACD;AAAA,MAAA,EAAA,CACH,CAAA;AAAA,MACC,QAAA;AAAA,MACA,yCAAA,KAAe,CAAA,CAAE,GAAA,CAAI,CAAC,OAAA,EAAA,mBACrB,6BAAA,GAAC,EAAA,EAAgB,SAAA,EAAU,0BAAA,EACxB,QAAA,EAAA,QAAA,CAAA,EADK,OAER,CACD;AAAA,IAAA,EAAA,CACH,CAAA;AAAA,EAEJ;AACF,CAAA;AACA,SAAA,CAAU,YAAA,EAAc,WAAA;ADbxB;AACA;AACE;AACF,8BAAC","file":"/Users/stuartjackson/Projects/simpleapps/atlas/repo/packages/augur-web/dist/chunk-OJS2B6U2.cjs","sourcesContent":[null,"\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@simpleapps-com/augur-utils/web\";\nimport { Label } from \"./label\";\nimport { slotsWhen, textSlots } from \"./rules\";\n\nexport interface FormFieldProps {\n label: string;\n htmlFor: string;\n required?: boolean;\n error?: string;\n className?: string;\n children: React.ReactNode;\n}\n\n/** Base form layout: label + children + optional error message. */\nconst FormField = React.forwardRef<HTMLDivElement, FormFieldProps>(\n ({ label, htmlFor, required, error, className, children }, ref) => {\n return (\n <div ref={ref} className={cn(\"space-y-2\", className)}>\n <Label htmlFor={htmlFor}>\n {label}\n {slotsWhen(required === true).map((slot) => (\n <span key={slot} className=\"text-destructive\">\n *\n </span>\n ))}\n </Label>\n {children}\n {textSlots(error).map((message) => (\n <p key={message} className=\"text-destructive text-sm\">\n {message}\n </p>\n ))}\n </div>\n );\n },\n);\nFormField.displayName = \"FormField\";\n\nexport { FormField };\n"]}
@@ -1,18 +1,16 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});"use client";
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }"use client";
2
+
3
+
4
+ var _chunkNBXA42AWcjs = require('./chunk-NBXA42AW.cjs');
2
5
 
3
6
  // src/spinner.tsx
4
7
  var _web = require('@simpleapps-com/augur-utils/web');
5
8
  var _jsxruntime = require('react/jsx-runtime');
6
- var sizeClasses = {
7
- sm: "h-3 w-3",
8
- md: "h-4 w-4",
9
- lg: "h-6 w-6"
10
- };
11
- var Spinner = ({ className, size = "md" }) => {
9
+ var Spinner = ({ className, size }) => {
12
10
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
13
11
  "svg",
14
12
  {
15
- className: _web.cn.call(void 0, "animate-spin", sizeClasses[size], className),
13
+ className: _web.cn.call(void 0, "animate-spin", _chunkNBXA42AWcjs.spinnerSizeClass.call(void 0, _nullishCoalesce(size, () => ( "md"))), className),
16
14
  xmlns: "http://www.w3.org/2000/svg",
17
15
  fill: "none",
18
16
  viewBox: "0 0 24 24",
@@ -34,4 +32,4 @@ var Spinner = ({ className, size = "md" }) => {
34
32
 
35
33
 
36
34
  exports.Spinner = Spinner;
37
- //# sourceMappingURL=chunk-5VMEEKZ5.cjs.map
35
+ //# sourceMappingURL=chunk-SJGSBMMN.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/stuartjackson/Projects/simpleapps/atlas/repo/packages/augur-web/dist/chunk-SJGSBMMN.cjs","../src/spinner.tsx"],"names":[],"mappings":"AAAA,yLAAY;AACZ;AACE;AACF,wDAA6B;AAC7B;AACA;ACLA,sDAAmB;AAWf,+CAAA;AAFJ,IAAM,QAAA,EAAU,CAAC,EAAE,SAAA,EAAW,KAAK,CAAA,EAAA,GAAoB;AACrD,EAAA,uBACE,8BAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAW,qBAAA,cAAG,EAAgB,gDAAA,iBAAiB,IAAA,UAAQ,MAAI,CAAA,EAAG,SAAS,CAAA;AAAA,MACvE,KAAA,EAAM,4BAAA;AAAA,MACN,IAAA,EAAK,MAAA;AAAA,MACL,OAAA,EAAQ,WAAA;AAAA,MAER,QAAA,EAAA;AAAA,wBAAA,6BAAA,QAAC,EAAA,EAAO,SAAA,EAAU,YAAA,EAAa,EAAA,EAAG,IAAA,EAAK,EAAA,EAAG,IAAA,EAAK,CAAA,EAAE,IAAA,EAAK,MAAA,EAAO,cAAA,EAAe,WAAA,EAAY,IAAA,CAAI,CAAA;AAAA,wBAC5F,6BAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YACC,SAAA,EAAU,YAAA;AAAA,YACV,IAAA,EAAK,cAAA;AAAA,YACL,CAAA,EAAE;AAAA,UAAA;AAAA,QACJ;AAAA,MAAA;AAAA,IAAA;AAAA,EACF,CAAA;AAEJ,CAAA;ADKA;AACA;AACE;AACF,0BAAC","file":"/Users/stuartjackson/Projects/simpleapps/atlas/repo/packages/augur-web/dist/chunk-SJGSBMMN.cjs","sourcesContent":[null,"import { cn } from \"@simpleapps-com/augur-utils/web\";\nimport { spinnerSizeClass } from \"./rules\";\n\ninterface SpinnerProps {\n className?: string;\n size?: \"sm\" | \"md\" | \"lg\";\n}\n\n/** Animated SVG loading spinner. Sizes: `sm` (12px), `md` (16px, the default), `lg` (24px). */\nconst Spinner = ({ className, size }: SpinnerProps) => {\n return (\n <svg\n className={cn(\"animate-spin\", spinnerSizeClass(size ?? \"md\"), className)}\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n />\n </svg>\n );\n};\n\nexport { Spinner };\n"]}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  gtmTiming
4
- } from "./chunk-VI2IE6RC.js";
4
+ } from "./chunk-NSMOT7GD.js";
5
5
 
6
6
  // src/gtm.tsx
7
7
  import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
@@ -126,4 +126,4 @@ export {
126
126
  useAnalytics,
127
127
  AnalyticsNoscript
128
128
  };
129
- //# sourceMappingURL=chunk-PFEPE5UE.js.map
129
+ //# sourceMappingURL=chunk-TEDFINZP.js.map
@@ -1,7 +1,7 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});"use client";
2
2
 
3
3
 
4
- var _chunkEEJYDSXRcjs = require('./chunk-EEJYDSXR.cjs');
4
+ var _chunkNBXA42AWcjs = require('./chunk-NBXA42AW.cjs');
5
5
 
6
6
  // src/gtm.tsx
7
7
  var _react = require('react');
@@ -41,7 +41,7 @@ function AnalyticsProvider({
41
41
  const loadGtm = _react.useCallback.call(void 0, () => {
42
42
  if (injectedRef.current) return;
43
43
  injectedRef.current = true;
44
- const timing = _chunkEEJYDSXRcjs.gtmTiming.call(void 0, strategy, typeof requestIdleCallback === "function");
44
+ const timing = _chunkNBXA42AWcjs.gtmTiming.call(void 0, strategy, typeof requestIdleCallback === "function");
45
45
  if (timing === "idle") {
46
46
  requestIdleCallback(() => {
47
47
  injectGtmScript(gtmId, strategy);
@@ -126,4 +126,4 @@ function AnalyticsNoscript({ gtmId }) {
126
126
 
127
127
 
128
128
  exports.AnalyticsProvider = AnalyticsProvider; exports.useAnalytics = useAnalytics; exports.AnalyticsNoscript = AnalyticsNoscript;
129
- //# sourceMappingURL=chunk-YTBB63IE.cjs.map
129
+ //# sourceMappingURL=chunk-VCFKYOVH.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/stuartjackson/Projects/simpleapps/atlas/repo/packages/augur-web/dist/chunk-YTBB63IE.cjs","../src/gtm.tsx"],"names":[],"mappings":"AAAA,qFAAY;AACZ;AACE;AACF,wDAA6B;AAC7B;AACA;ACHA,8BAAkF;AAmNzE,+CAAA;AAlIT,IAAM,iBAAA,EAAmB,kCAAA,IAAgD,CAAA;AAazE,SAAS,YAAA,CAAA,EAA0C;AACjD,EAAA,GAAA,CAAI,OAAO,OAAA,IAAW,WAAA,EAAa,OAAO,CAAC,CAAA;AAC3C,EAAA,GAAA,CAAI,CAAC,MAAA,CAAO,SAAA,EAAW,MAAA,CAAO,UAAA,EAAY,CAAC,CAAA;AAC3C,EAAA,OAAO,MAAA,CAAO,SAAA;AAChB;AAEA,SAAS,eAAA,CAAgB,KAAA,EAAe,QAAA,EAAmC;AACzE,EAAA,GAAA,CAAI,OAAO,SAAA,IAAa,WAAA,EAAa,MAAA;AAGrC,EAAA,GAAA,CAAI,QAAA,CAAS,cAAA,CAAe,YAAY,CAAA,EAAG,MAAA;AAE3C,EAAA,MAAM,UAAA,EAAY,YAAA,CAAa,CAAA;AAC/B,EAAA,SAAA,CAAU,IAAA,CAAK,EAAE,WAAA,EAAA,iBAAa,IAAI,IAAA,CAAK,CAAA,CAAA,CAAE,OAAA,CAAQ,CAAA,EAAG,KAAA,EAAO,SAAS,CAAC,CAAA;AAErE,EAAA,MAAM,OAAA,EAAS,QAAA,CAAS,aAAA,CAAc,QAAQ,CAAA;AAC9C,EAAA,MAAA,CAAO,GAAA,EAAK,YAAA;AACZ,EAAA,MAAA,CAAO,IAAA,EAAM,CAAA,2CAAA,EAA8C,kBAAA,CAAmB,KAAK,CAAC,CAAA,CAAA;AAErD,EAAA;AACd,IAAA;AACA,IAAA;AACV,EAAA;AAEU,IAAA;AACjB,EAAA;AAEgC,EAAA;AAClC;AAMkC;AAChC,EAAA;AACW,EAAA;AACO,EAAA;AACV,EAAA;AACR,EAAA;AACyB;AACoC,EAAA;AACf,EAAA;AACd,EAAA;AAEE,EAAA;AACP,IAAA;AACH,IAAA;AAEsD,IAAA;AACrD,IAAA;AACK,MAAA;AACO,QAAA;AACf,QAAA;AACjB,MAAA;AACgC,IAAA;AAEhB,MAAA;AACgB,QAAA;AACf,QAAA;AACd,MAAA;AACC,IAAA;AAC0B,MAAA;AACf,MAAA;AAClB,IAAA;AACkB,EAAA;AAGqD,EAAA;AAC/D,IAAA;AACV,EAAA;AAEmB,EAAA;AACoC,IAAA;AAClB,MAAA;AACtB,MAAA;AACuB,QAAA;AAClC,MAAA;AACyB,MAAA;AAC3B,IAAA;AACM,IAAA;AACR,EAAA;AAE4B,EAAA;AAC0B,IAAA;AAEX,MAAA;AACE,MAAA;AAC9B,MAAA;AACiC,QAAA;AAC5C,MAAA;AACyB,MAAA;AAC3B,IAAA;AACM,IAAA;AACR,EAAA;AAEuC,EAAA;AACnB,IAAA;AACV,IAAA;AACE,EAAA;AAE4B,EAAA;AACnB,IAAA;AAChB,EAAA;AAES,EAAA;AACL,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACmF,IAAA;AACrF,EAAA;AAE0D,EAAA;AAC5D;AAMsD;AACT,EAAA;AAC7B,EAAA;AAC6D,IAAA;AAC3E,EAAA;AACO,EAAA;AACT;AAWqE;AAG/D,EAAA;AAAC,IAAA;AAAA,IAAA;AAC8E,MAAA;AACtE,MAAA;AACD,MAAA;AACyC,MAAA;AACzC,MAAA;AAAA,IAAA;AAEV,EAAA;AAEJ;AD/HuF;AACA;AACA;AACA;AACA;AACA","file":"/Users/stuartjackson/Projects/simpleapps/atlas/repo/packages/augur-web/dist/chunk-YTBB63IE.cjs","sourcesContent":[null,"\"use client\";\n\nimport { createContext, useCallback, useContext, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { gtmTiming } from \"./rules\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Script loading strategy. */\nexport type AnalyticsStrategy = \"default\" | \"afterInteractive\" | \"lazyOnload\";\n\n/** GA4 standard e-commerce event names. */\nexport type EcommerceEvent =\n | \"view_item\"\n | \"view_item_list\"\n | \"select_item\"\n | \"add_to_cart\"\n | \"remove_from_cart\"\n | \"view_cart\"\n | \"begin_checkout\"\n | \"add_shipping_info\"\n | \"add_payment_info\"\n | \"purchase\"\n | \"refund\";\n\n/** GA4 e-commerce item shape. */\nexport interface EcommerceItem {\n item_id: string;\n item_name?: string;\n item_brand?: string;\n item_category?: string;\n item_variant?: string;\n price?: number;\n quantity?: number;\n index?: number;\n [key: string]: unknown;\n}\n\n/** Parameters for e-commerce events. */\nexport interface EcommerceParams {\n currency?: string;\n value?: number;\n items?: EcommerceItem[];\n transaction_id?: string;\n shipping?: number;\n tax?: number;\n coupon?: string;\n [key: string]: unknown;\n}\n\nexport interface AnalyticsProviderProps {\n /** GTM container ID (e.g. \"GTM-XXXXX\"). */\n gtmId: string;\n /** Script loading strategy. Default: \"afterInteractive\". */\n strategy?: AnalyticsStrategy;\n /** Delay GTM loading until consent is granted. Default: false. */\n consentRequired?: boolean;\n /** Log dataLayer pushes to console. Default: false. */\n debug?: boolean;\n children: ReactNode;\n}\n\nexport interface AnalyticsContextValue {\n /** Push a custom event to the dataLayer. */\n trackEvent: (event: string, params?: Record<string, unknown>) => void;\n /** Push a GA4 e-commerce event to the dataLayer. */\n trackEcommerceEvent: (event: EcommerceEvent, params: EcommerceParams) => void;\n /** Grant consent — loads GTM if consentRequired was true. */\n grantConsent: () => void;\n /** Revoke consent. Does not unload GTM (scripts can't be unloaded). */\n revokeConsent: () => void;\n /** Whether consent has been granted. */\n hasConsent: boolean;\n /** Whether the GTM script has been injected. */\n isLoaded: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\nconst AnalyticsContext = createContext<AnalyticsContextValue | null>(null);\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\ndeclare global {\n interface Window {\n dataLayer?: Record<string, unknown>[];\n }\n}\n\n// Analytics calls can come from callbacks and timers after the page has gone.\nfunction getDataLayer(): Record<string, unknown>[] {\n if (typeof window === \"undefined\") return [];\n if (!window.dataLayer) window.dataLayer = [];\n return window.dataLayer;\n}\n\nfunction injectGtmScript(gtmId: string, strategy: AnalyticsStrategy): void {\n if (typeof document === \"undefined\") return;\n\n // Prevent double-injection\n if (document.getElementById(\"gtm-script\")) return;\n\n const dataLayer = getDataLayer();\n dataLayer.push({ \"gtm.start\": new Date().getTime(), event: \"gtm.js\" });\n\n const script = document.createElement(\"script\");\n script.id = \"gtm-script\";\n script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(gtmId)}`;\n\n if (strategy === \"lazyOnload\") {\n script.async = true;\n script.defer = true;\n } else {\n // \"default\" and \"afterInteractive\"\n script.async = true;\n }\n\n document.head.appendChild(script);\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport function AnalyticsProvider({\n gtmId,\n strategy = \"afterInteractive\",\n consentRequired = false,\n debug = false,\n children,\n}: AnalyticsProviderProps) {\n const [hasConsent, setHasConsent] = useState(!consentRequired);\n const [isLoaded, setIsLoaded] = useState(false);\n const injectedRef = useRef(false);\n\n const loadGtm = useCallback(() => {\n if (injectedRef.current) return;\n injectedRef.current = true;\n\n const timing = gtmTiming(strategy, typeof requestIdleCallback === \"function\");\n if (timing === \"idle\") {\n requestIdleCallback(() => {\n injectGtmScript(gtmId, strategy);\n setIsLoaded(true);\n });\n } else if (timing === \"next-tick\") {\n // Defer to after hydration\n setTimeout(() => {\n injectGtmScript(gtmId, strategy);\n setIsLoaded(true);\n }, 0);\n } else {\n injectGtmScript(gtmId, strategy);\n setIsLoaded(true);\n }\n }, [gtmId, strategy]);\n\n // Auto-load when consent is not required or already granted (never while server-rendering)\n if (hasConsent && !injectedRef.current && typeof window !== \"undefined\") {\n loadGtm();\n }\n\n const trackEvent = useCallback(\n (event: string, params?: Record<string, unknown>) => {\n const entry = { event, ...params };\n if (debug) {\n console.log(\"[Analytics]\", entry);\n }\n getDataLayer().push(entry);\n },\n [debug],\n );\n\n const trackEcommerceEvent = useCallback(\n (event: EcommerceEvent, params: EcommerceParams) => {\n // Clear previous ecommerce data per GA4 best practices\n getDataLayer().push({ ecommerce: null });\n const entry = { event, ecommerce: params };\n if (debug) {\n console.log(\"[Analytics ecommerce]\", entry);\n }\n getDataLayer().push(entry);\n },\n [debug],\n );\n\n const grantConsent = useCallback(() => {\n setHasConsent(true);\n loadGtm();\n }, [loadGtm]);\n\n const revokeConsent = useCallback(() => {\n setHasConsent(false);\n }, []);\n\n const value = useMemo<AnalyticsContextValue>(\n () => ({\n trackEvent,\n trackEcommerceEvent,\n grantConsent,\n revokeConsent,\n hasConsent,\n isLoaded,\n }),\n [trackEvent, trackEcommerceEvent, grantConsent, revokeConsent, hasConsent, isLoaded],\n );\n\n return <AnalyticsContext.Provider value={value}>{children}</AnalyticsContext.Provider>;\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useAnalytics(): AnalyticsContextValue {\n const context = useContext(AnalyticsContext);\n if (!context) {\n throw new Error(\"useAnalytics must be used within an <AnalyticsProvider>\");\n }\n return context;\n}\n\n// ---------------------------------------------------------------------------\n// Noscript\n// ---------------------------------------------------------------------------\n\nexport interface AnalyticsNoscriptProps {\n /** GTM container ID (must match AnalyticsProvider). */\n gtmId: string;\n}\n\nexport function AnalyticsNoscript({ gtmId }: AnalyticsNoscriptProps) {\n return (\n <noscript>\n <iframe\n src={`https://www.googletagmanager.com/ns.html?id=${encodeURIComponent(gtmId)}`}\n height=\"0\"\n width=\"0\"\n style={{ display: \"none\", visibility: \"hidden\" }}\n title=\"Google Tag Manager\"\n />\n </noscript>\n );\n}\n"]}
1
+ {"version":3,"sources":["/Users/stuartjackson/Projects/simpleapps/atlas/repo/packages/augur-web/dist/chunk-VCFKYOVH.cjs","../src/gtm.tsx"],"names":[],"mappings":"AAAA,qFAAY;AACZ;AACE;AACF,wDAA6B;AAC7B;AACA;ACHA,8BAAkF;AAmNzE,+CAAA;AAlIT,IAAM,iBAAA,EAAmB,kCAAA,IAAgD,CAAA;AAazE,SAAS,YAAA,CAAA,EAA0C;AACjD,EAAA,GAAA,CAAI,OAAO,OAAA,IAAW,WAAA,EAAa,OAAO,CAAC,CAAA;AAC3C,EAAA,GAAA,CAAI,CAAC,MAAA,CAAO,SAAA,EAAW,MAAA,CAAO,UAAA,EAAY,CAAC,CAAA;AAC3C,EAAA,OAAO,MAAA,CAAO,SAAA;AAChB;AAEA,SAAS,eAAA,CAAgB,KAAA,EAAe,QAAA,EAAmC;AACzE,EAAA,GAAA,CAAI,OAAO,SAAA,IAAa,WAAA,EAAa,MAAA;AAGrC,EAAA,GAAA,CAAI,QAAA,CAAS,cAAA,CAAe,YAAY,CAAA,EAAG,MAAA;AAE3C,EAAA,MAAM,UAAA,EAAY,YAAA,CAAa,CAAA;AAC/B,EAAA,SAAA,CAAU,IAAA,CAAK,EAAE,WAAA,EAAA,iBAAa,IAAI,IAAA,CAAK,CAAA,CAAA,CAAE,OAAA,CAAQ,CAAA,EAAG,KAAA,EAAO,SAAS,CAAC,CAAA;AAErE,EAAA,MAAM,OAAA,EAAS,QAAA,CAAS,aAAA,CAAc,QAAQ,CAAA;AAC9C,EAAA,MAAA,CAAO,GAAA,EAAK,YAAA;AACZ,EAAA,MAAA,CAAO,IAAA,EAAM,CAAA,2CAAA,EAA8C,kBAAA,CAAmB,KAAK,CAAC,CAAA,CAAA;AAErD,EAAA;AACd,IAAA;AACA,IAAA;AACV,EAAA;AAEU,IAAA;AACjB,EAAA;AAEgC,EAAA;AAClC;AAMkC;AAChC,EAAA;AACW,EAAA;AACO,EAAA;AACV,EAAA;AACR,EAAA;AACyB;AACoC,EAAA;AACf,EAAA;AACd,EAAA;AAEE,EAAA;AACP,IAAA;AACH,IAAA;AAEsD,IAAA;AACrD,IAAA;AACK,MAAA;AACO,QAAA;AACf,QAAA;AACjB,MAAA;AACgC,IAAA;AAEhB,MAAA;AACgB,QAAA;AACf,QAAA;AACd,MAAA;AACC,IAAA;AAC0B,MAAA;AACf,MAAA;AAClB,IAAA;AACkB,EAAA;AAGqD,EAAA;AAC/D,IAAA;AACV,EAAA;AAEmB,EAAA;AACoC,IAAA;AAClB,MAAA;AACtB,MAAA;AACuB,QAAA;AAClC,MAAA;AACyB,MAAA;AAC3B,IAAA;AACM,IAAA;AACR,EAAA;AAE4B,EAAA;AAC0B,IAAA;AAEX,MAAA;AACE,MAAA;AAC9B,MAAA;AACiC,QAAA;AAC5C,MAAA;AACyB,MAAA;AAC3B,IAAA;AACM,IAAA;AACR,EAAA;AAEuC,EAAA;AACnB,IAAA;AACV,IAAA;AACE,EAAA;AAE4B,EAAA;AACnB,IAAA;AAChB,EAAA;AAES,EAAA;AACL,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACmF,IAAA;AACrF,EAAA;AAE0D,EAAA;AAC5D;AAMsD;AACT,EAAA;AAC7B,EAAA;AAC6D,IAAA;AAC3E,EAAA;AACO,EAAA;AACT;AAWqE;AAG/D,EAAA;AAAC,IAAA;AAAA,IAAA;AAC8E,MAAA;AACtE,MAAA;AACD,MAAA;AACyC,MAAA;AACzC,MAAA;AAAA,IAAA;AAEV,EAAA;AAEJ;AD/HuF;AACA;AACA;AACA;AACA;AACA","file":"/Users/stuartjackson/Projects/simpleapps/atlas/repo/packages/augur-web/dist/chunk-VCFKYOVH.cjs","sourcesContent":[null,"\"use client\";\n\nimport { createContext, useCallback, useContext, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { gtmTiming } from \"./rules\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Script loading strategy. */\nexport type AnalyticsStrategy = \"default\" | \"afterInteractive\" | \"lazyOnload\";\n\n/** GA4 standard e-commerce event names. */\nexport type EcommerceEvent =\n | \"view_item\"\n | \"view_item_list\"\n | \"select_item\"\n | \"add_to_cart\"\n | \"remove_from_cart\"\n | \"view_cart\"\n | \"begin_checkout\"\n | \"add_shipping_info\"\n | \"add_payment_info\"\n | \"purchase\"\n | \"refund\";\n\n/** GA4 e-commerce item shape. */\nexport interface EcommerceItem {\n item_id: string;\n item_name?: string;\n item_brand?: string;\n item_category?: string;\n item_variant?: string;\n price?: number;\n quantity?: number;\n index?: number;\n [key: string]: unknown;\n}\n\n/** Parameters for e-commerce events. */\nexport interface EcommerceParams {\n currency?: string;\n value?: number;\n items?: EcommerceItem[];\n transaction_id?: string;\n shipping?: number;\n tax?: number;\n coupon?: string;\n [key: string]: unknown;\n}\n\nexport interface AnalyticsProviderProps {\n /** GTM container ID (e.g. \"GTM-XXXXX\"). */\n gtmId: string;\n /** Script loading strategy. Default: \"afterInteractive\". */\n strategy?: AnalyticsStrategy;\n /** Delay GTM loading until consent is granted. Default: false. */\n consentRequired?: boolean;\n /** Log dataLayer pushes to console. Default: false. */\n debug?: boolean;\n children: ReactNode;\n}\n\nexport interface AnalyticsContextValue {\n /** Push a custom event to the dataLayer. */\n trackEvent: (event: string, params?: Record<string, unknown>) => void;\n /** Push a GA4 e-commerce event to the dataLayer. */\n trackEcommerceEvent: (event: EcommerceEvent, params: EcommerceParams) => void;\n /** Grant consent — loads GTM if consentRequired was true. */\n grantConsent: () => void;\n /** Revoke consent. Does not unload GTM (scripts can't be unloaded). */\n revokeConsent: () => void;\n /** Whether consent has been granted. */\n hasConsent: boolean;\n /** Whether the GTM script has been injected. */\n isLoaded: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\nconst AnalyticsContext = createContext<AnalyticsContextValue | null>(null);\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\ndeclare global {\n interface Window {\n dataLayer?: Record<string, unknown>[];\n }\n}\n\n// Analytics calls can come from callbacks and timers after the page has gone.\nfunction getDataLayer(): Record<string, unknown>[] {\n if (typeof window === \"undefined\") return [];\n if (!window.dataLayer) window.dataLayer = [];\n return window.dataLayer;\n}\n\nfunction injectGtmScript(gtmId: string, strategy: AnalyticsStrategy): void {\n if (typeof document === \"undefined\") return;\n\n // Prevent double-injection\n if (document.getElementById(\"gtm-script\")) return;\n\n const dataLayer = getDataLayer();\n dataLayer.push({ \"gtm.start\": new Date().getTime(), event: \"gtm.js\" });\n\n const script = document.createElement(\"script\");\n script.id = \"gtm-script\";\n script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(gtmId)}`;\n\n if (strategy === \"lazyOnload\") {\n script.async = true;\n script.defer = true;\n } else {\n // \"default\" and \"afterInteractive\"\n script.async = true;\n }\n\n document.head.appendChild(script);\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport function AnalyticsProvider({\n gtmId,\n strategy = \"afterInteractive\",\n consentRequired = false,\n debug = false,\n children,\n}: AnalyticsProviderProps) {\n const [hasConsent, setHasConsent] = useState(!consentRequired);\n const [isLoaded, setIsLoaded] = useState(false);\n const injectedRef = useRef(false);\n\n const loadGtm = useCallback(() => {\n if (injectedRef.current) return;\n injectedRef.current = true;\n\n const timing = gtmTiming(strategy, typeof requestIdleCallback === \"function\");\n if (timing === \"idle\") {\n requestIdleCallback(() => {\n injectGtmScript(gtmId, strategy);\n setIsLoaded(true);\n });\n } else if (timing === \"next-tick\") {\n // Defer to after hydration\n setTimeout(() => {\n injectGtmScript(gtmId, strategy);\n setIsLoaded(true);\n }, 0);\n } else {\n injectGtmScript(gtmId, strategy);\n setIsLoaded(true);\n }\n }, [gtmId, strategy]);\n\n // Auto-load when consent is not required or already granted (never while server-rendering)\n if (hasConsent && !injectedRef.current && typeof window !== \"undefined\") {\n loadGtm();\n }\n\n const trackEvent = useCallback(\n (event: string, params?: Record<string, unknown>) => {\n const entry = { event, ...params };\n if (debug) {\n console.log(\"[Analytics]\", entry);\n }\n getDataLayer().push(entry);\n },\n [debug],\n );\n\n const trackEcommerceEvent = useCallback(\n (event: EcommerceEvent, params: EcommerceParams) => {\n // Clear previous ecommerce data per GA4 best practices\n getDataLayer().push({ ecommerce: null });\n const entry = { event, ecommerce: params };\n if (debug) {\n console.log(\"[Analytics ecommerce]\", entry);\n }\n getDataLayer().push(entry);\n },\n [debug],\n );\n\n const grantConsent = useCallback(() => {\n setHasConsent(true);\n loadGtm();\n }, [loadGtm]);\n\n const revokeConsent = useCallback(() => {\n setHasConsent(false);\n }, []);\n\n const value = useMemo<AnalyticsContextValue>(\n () => ({\n trackEvent,\n trackEcommerceEvent,\n grantConsent,\n revokeConsent,\n hasConsent,\n isLoaded,\n }),\n [trackEvent, trackEcommerceEvent, grantConsent, revokeConsent, hasConsent, isLoaded],\n );\n\n return <AnalyticsContext.Provider value={value}>{children}</AnalyticsContext.Provider>;\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useAnalytics(): AnalyticsContextValue {\n const context = useContext(AnalyticsContext);\n if (!context) {\n throw new Error(\"useAnalytics must be used within an <AnalyticsProvider>\");\n }\n return context;\n}\n\n// ---------------------------------------------------------------------------\n// Noscript\n// ---------------------------------------------------------------------------\n\nexport interface AnalyticsNoscriptProps {\n /** GTM container ID (must match AnalyticsProvider). */\n gtmId: string;\n}\n\nexport function AnalyticsNoscript({ gtmId }: AnalyticsNoscriptProps) {\n return (\n <noscript>\n <iframe\n src={`https://www.googletagmanager.com/ns.html?id=${encodeURIComponent(gtmId)}`}\n height=\"0\"\n width=\"0\"\n style={{ display: \"none\", visibility: \"hidden\" }}\n title=\"Google Tag Manager\"\n />\n </noscript>\n );\n}\n"]}