@alumbwe/anvil 1.0.20 → 1.0.21

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 (328) hide show
  1. package/package.json +6 -2
  2. package/src/agents/bundled-agents.generated.ts +4 -4
  3. package/src/chat.tsx +1703 -1698
  4. package/src/cli-args.ts +134 -134
  5. package/src/components/status-bar.tsx +331 -313
  6. package/src/hooks/helpers/send-message.ts +663 -654
  7. package/src/hooks/use-send-message.ts +806 -773
  8. package/src/index.tsx +473 -473
  9. package/src/state/chat-store.ts +553 -544
  10. package/src/utils/create-run-config.ts +121 -121
  11. package/src/utils/format-compact-number.ts +37 -0
  12. package/vendor/@anvil/common/package.json +27 -0
  13. package/vendor/@anvil/common/src/__tests__/agent-validation.test.ts +846 -0
  14. package/vendor/@anvil/common/src/__tests__/anvil-model-availability.test.ts +101 -0
  15. package/vendor/@anvil/common/src/__tests__/anvil-models.test.ts +1408 -0
  16. package/vendor/@anvil/common/src/__tests__/anvil-public-data-use-copy.test.ts +258 -0
  17. package/vendor/@anvil/common/src/__tests__/anvil-referral-tiers.test.ts +48 -0
  18. package/vendor/@anvil/common/src/__tests__/anvil-spend-ceilings.test.ts +282 -0
  19. package/vendor/@anvil/common/src/__tests__/anvil-trust.test.ts +526 -0
  20. package/vendor/@anvil/common/src/__tests__/deepseek-direct.test.ts +68 -0
  21. package/vendor/@anvil/common/src/__tests__/disposable-email.test.ts +132 -0
  22. package/vendor/@anvil/common/src/__tests__/dynamic-agent-template-schema.test.ts +422 -0
  23. package/vendor/@anvil/common/src/__tests__/env-ci.test.ts +167 -0
  24. package/vendor/@anvil/common/src/__tests__/env-process.test.ts +145 -0
  25. package/vendor/@anvil/common/src/__tests__/foreign-client-shipped-agents.test.ts +234 -0
  26. package/vendor/@anvil/common/src/__tests__/foreign-client-signals.test.ts +379 -0
  27. package/vendor/@anvil/common/src/__tests__/free-agents.test.ts +807 -0
  28. package/vendor/@anvil/common/src/__tests__/gravity-capi.test.ts +154 -0
  29. package/vendor/@anvil/common/src/__tests__/handlesteps-parsing.test.ts +246 -0
  30. package/vendor/@anvil/common/src/__tests__/kimi-k3-god-only.test.ts +77 -0
  31. package/vendor/@anvil/common/src/__tests__/model-config.test.ts +89 -0
  32. package/vendor/@anvil/common/src/__tests__/project-file-tree.test.ts +298 -0
  33. package/vendor/@anvil/common/src/__tests__/provisioned-model-tiers.test.ts +116 -0
  34. package/vendor/@anvil/common/src/__tests__/reasoning-effort.test.ts +167 -0
  35. package/vendor/@anvil/common/src/__tests__/reddit-capi.test.ts +132 -0
  36. package/vendor/@anvil/common/src/__tests__/response-ad-positions.test.ts +72 -0
  37. package/vendor/@anvil/common/src/__tests__/user-state.test.ts +30 -0
  38. package/vendor/@anvil/common/src/actions.ts +215 -0
  39. package/vendor/@anvil/common/src/analytics-core.ts +69 -0
  40. package/vendor/@anvil/common/src/analytics.ts +93 -0
  41. package/vendor/@anvil/common/src/api-keys/constants.ts +26 -0
  42. package/vendor/@anvil/common/src/browser-actions.ts +413 -0
  43. package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding-gate.test.ts +63 -0
  44. package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding.test.ts +246 -0
  45. package/vendor/@anvil/common/src/constants/__tests__/cf-worker-signals.test.ts +106 -0
  46. package/vendor/@anvil/common/src/constants/agents.ts +99 -0
  47. package/vendor/@anvil/common/src/constants/analytics-events.ts +435 -0
  48. package/vendor/@anvil/common/src/constants/anthropic.ts +73 -0
  49. package/vendor/@anvil/common/src/constants/anvil-data-use.ts +81 -0
  50. package/vendor/@anvil/common/src/constants/anvil-errors.ts +7 -0
  51. package/vendor/@anvil/common/src/constants/anvil-gemini-thinker.ts +21 -0
  52. package/vendor/@anvil/common/src/constants/anvil-model-ids.ts +11 -0
  53. package/vendor/@anvil/common/src/constants/anvil-models.ts +2130 -0
  54. package/vendor/@anvil/common/src/constants/anvil-onboarding-gate.ts +55 -0
  55. package/vendor/@anvil/common/src/constants/anvil-onboarding.ts +307 -0
  56. package/vendor/@anvil/common/src/constants/anvil-referral-tiers.ts +87 -0
  57. package/vendor/@anvil/common/src/constants/anvil-signup-block.ts +68 -0
  58. package/vendor/@anvil/common/src/constants/anvil-spend-ceilings.ts +450 -0
  59. package/vendor/@anvil/common/src/constants/anvil-trust.ts +976 -0
  60. package/vendor/@anvil/common/src/constants/auth.ts +18 -0
  61. package/vendor/@anvil/common/src/constants/byok.ts +2 -0
  62. package/vendor/@anvil/common/src/constants/cf-worker-signals.ts +132 -0
  63. package/vendor/@anvil/common/src/constants/composio.ts +34 -0
  64. package/vendor/@anvil/common/src/constants/deepseek-direct.ts +55 -0
  65. package/vendor/@anvil/common/src/constants/feedback.ts +13 -0
  66. package/vendor/@anvil/common/src/constants/foreign-client-signals.ts +272 -0
  67. package/vendor/@anvil/common/src/constants/free-agents.ts +777 -0
  68. package/vendor/@anvil/common/src/constants/gemini.ts +15 -0
  69. package/vendor/@anvil/common/src/constants/grant-priorities.ts +13 -0
  70. package/vendor/@anvil/common/src/constants/hosts.ts +6 -0
  71. package/vendor/@anvil/common/src/constants/images.ts +51 -0
  72. package/vendor/@anvil/common/src/constants/index.ts +7 -0
  73. package/vendor/@anvil/common/src/constants/knowledge.ts +35 -0
  74. package/vendor/@anvil/common/src/constants/limits.ts +23 -0
  75. package/vendor/@anvil/common/src/constants/model-config.ts +277 -0
  76. package/vendor/@anvil/common/src/constants/openrouter-attribution.ts +8 -0
  77. package/vendor/@anvil/common/src/constants/paths.ts +69 -0
  78. package/vendor/@anvil/common/src/constants/provider-routes.ts +317 -0
  79. package/vendor/@anvil/common/src/constants/reasoning-effort.ts +79 -0
  80. package/vendor/@anvil/common/src/constants/skills.ts +60 -0
  81. package/vendor/@anvil/common/src/constants/subscription-plans.ts +49 -0
  82. package/vendor/@anvil/common/src/constants/ui.ts +25 -0
  83. package/vendor/@anvil/common/src/env-ci.ts +36 -0
  84. package/vendor/@anvil/common/src/env-process.ts +95 -0
  85. package/vendor/@anvil/common/src/env-schema.ts +95 -0
  86. package/vendor/@anvil/common/src/env.ts +24 -0
  87. package/vendor/@anvil/common/src/gravity-capi.ts +207 -0
  88. package/vendor/@anvil/common/src/mcp/client.ts +233 -0
  89. package/vendor/@anvil/common/src/old-constants.ts +10 -0
  90. package/vendor/@anvil/common/src/project-file-tree.ts +355 -0
  91. package/vendor/@anvil/common/src/reddit-capi.ts +254 -0
  92. package/vendor/@anvil/common/src/schemas/feedback.ts +52 -0
  93. package/vendor/@anvil/common/src/schemas/logs.ts +66 -0
  94. package/vendor/@anvil/common/src/templates/agent-validation.ts +392 -0
  95. package/vendor/@anvil/common/src/templates/initial-agents-dir/LICENSE +202 -0
  96. package/vendor/@anvil/common/src/templates/initial-agents-dir/README.md +294 -0
  97. package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/01-basic-diff-reviewer.ts +17 -0
  98. package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/02-intermediate-git-committer.ts +78 -0
  99. package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/03-advanced-file-explorer.ts +73 -0
  100. package/vendor/@anvil/common/src/templates/initial-agents-dir/my-custom-agent.ts +40 -0
  101. package/vendor/@anvil/common/src/templates/initial-agents-dir/package.json +6 -0
  102. package/vendor/@anvil/common/src/templates/initial-agents-dir/skills/README.md +65 -0
  103. package/vendor/@anvil/common/src/templates/initial-agents-dir/skills/example-skill/SKILL.md +29 -0
  104. package/vendor/@anvil/common/src/templates/initial-agents-dir/types/agent-definition.ts +497 -0
  105. package/vendor/@anvil/common/src/templates/initial-agents-dir/types/tools.ts +444 -0
  106. package/vendor/@anvil/common/src/templates/initial-agents-dir/types/util-types.ts +178 -0
  107. package/vendor/@anvil/common/src/testing/TESTING_PATTERNS.md +351 -0
  108. package/vendor/@anvil/common/src/testing/anvil-offer-invariants.ts +169 -0
  109. package/vendor/@anvil/common/src/testing/errors.ts +33 -0
  110. package/vendor/@anvil/common/src/testing/fixtures/agent-runtime.ts +334 -0
  111. package/vendor/@anvil/common/src/testing/impl/agent-runtime.ts +6 -0
  112. package/vendor/@anvil/common/src/testing/index.ts +84 -0
  113. package/vendor/@anvil/common/src/testing/mock-modules.ts +53 -0
  114. package/vendor/@anvil/common/src/testing/mock-types.ts +123 -0
  115. package/vendor/@anvil/common/src/testing/mocks/analytics.ts +261 -0
  116. package/vendor/@anvil/common/src/testing/mocks/child-process.ts +93 -0
  117. package/vendor/@anvil/common/src/testing/mocks/crypto.ts +218 -0
  118. package/vendor/@anvil/common/src/testing/mocks/database.ts +337 -0
  119. package/vendor/@anvil/common/src/testing/mocks/fetch.ts +219 -0
  120. package/vendor/@anvil/common/src/testing/mocks/filesystem.ts +166 -0
  121. package/vendor/@anvil/common/src/testing/mocks/index.ts +101 -0
  122. package/vendor/@anvil/common/src/testing/mocks/logger.ts +135 -0
  123. package/vendor/@anvil/common/src/testing/mocks/stream.ts +313 -0
  124. package/vendor/@anvil/common/src/testing/mocks/timers.ts +132 -0
  125. package/vendor/@anvil/common/src/testing/mocks/tree-sitter.ts +127 -0
  126. package/vendor/@anvil/common/src/testing/setup.ts +282 -0
  127. package/vendor/@anvil/common/src/testing-env-ci.ts +15 -0
  128. package/vendor/@anvil/common/src/testing-env-process.ts +78 -0
  129. package/vendor/@anvil/common/src/tools/__tests__/compile-tool-definitions.test.ts +34 -0
  130. package/vendor/@anvil/common/src/tools/compile-tool-definitions.ts +157 -0
  131. package/vendor/@anvil/common/src/tools/constants.ts +117 -0
  132. package/vendor/@anvil/common/src/tools/list.ts +190 -0
  133. package/vendor/@anvil/common/src/tools/params/__tests__/coerce-to-array.test.ts +213 -0
  134. package/vendor/@anvil/common/src/tools/params/__tests__/read-docs.test.ts +47 -0
  135. package/vendor/@anvil/common/src/tools/params/__tests__/render-ui.test.ts +65 -0
  136. package/vendor/@anvil/common/src/tools/params/tool/__tests__/run-terminal-command-timeout.test.ts +49 -0
  137. package/vendor/@anvil/common/src/tools/params/tool/add-message.ts +39 -0
  138. package/vendor/@anvil/common/src/tools/params/tool/add-subgoal.ts +57 -0
  139. package/vendor/@anvil/common/src/tools/params/tool/apply-patch.ts +110 -0
  140. package/vendor/@anvil/common/src/tools/params/tool/ask-user.ts +181 -0
  141. package/vendor/@anvil/common/src/tools/params/tool/browser-logs.ts +85 -0
  142. package/vendor/@anvil/common/src/tools/params/tool/cloud-plan-ready.ts +94 -0
  143. package/vendor/@anvil/common/src/tools/params/tool/code-search.ts +159 -0
  144. package/vendor/@anvil/common/src/tools/params/tool/composio.ts +131 -0
  145. package/vendor/@anvil/common/src/tools/params/tool/create-plan.ts +80 -0
  146. package/vendor/@anvil/common/src/tools/params/tool/end-turn.ts +57 -0
  147. package/vendor/@anvil/common/src/tools/params/tool/find-files.ts +60 -0
  148. package/vendor/@anvil/common/src/tools/params/tool/glob.ts +80 -0
  149. package/vendor/@anvil/common/src/tools/params/tool/gravity-index.ts +93 -0
  150. package/vendor/@anvil/common/src/tools/params/tool/list-directory.ts +58 -0
  151. package/vendor/@anvil/common/src/tools/params/tool/lookup-agent-info.ts +37 -0
  152. package/vendor/@anvil/common/src/tools/params/tool/propose-str-replace.ts +103 -0
  153. package/vendor/@anvil/common/src/tools/params/tool/propose-write-file.ts +71 -0
  154. package/vendor/@anvil/common/src/tools/params/tool/read-docs.ts +90 -0
  155. package/vendor/@anvil/common/src/tools/params/tool/read-files.ts +109 -0
  156. package/vendor/@anvil/common/src/tools/params/tool/read-subtree.ts +79 -0
  157. package/vendor/@anvil/common/src/tools/params/tool/read-url.ts +81 -0
  158. package/vendor/@anvil/common/src/tools/params/tool/render-ui.ts +168 -0
  159. package/vendor/@anvil/common/src/tools/params/tool/run-file-change-hooks.ts +57 -0
  160. package/vendor/@anvil/common/src/tools/params/tool/run-terminal-command.ts +211 -0
  161. package/vendor/@anvil/common/src/tools/params/tool/screenshot.ts +97 -0
  162. package/vendor/@anvil/common/src/tools/params/tool/set-messages.ts +44 -0
  163. package/vendor/@anvil/common/src/tools/params/tool/set-output.ts +61 -0
  164. package/vendor/@anvil/common/src/tools/params/tool/skill.ts +59 -0
  165. package/vendor/@anvil/common/src/tools/params/tool/spawn-agent-inline.ts +56 -0
  166. package/vendor/@anvil/common/src/tools/params/tool/spawn-agents.ts +154 -0
  167. package/vendor/@anvil/common/src/tools/params/tool/str-replace.ts +107 -0
  168. package/vendor/@anvil/common/src/tools/params/tool/suggest-followups.ts +97 -0
  169. package/vendor/@anvil/common/src/tools/params/tool/task-completed.ts +61 -0
  170. package/vendor/@anvil/common/src/tools/params/tool/think-deeply.ts +56 -0
  171. package/vendor/@anvil/common/src/tools/params/tool/update-subgoal.ts +89 -0
  172. package/vendor/@anvil/common/src/tools/params/tool/vision-analyze.ts +82 -0
  173. package/vendor/@anvil/common/src/tools/params/tool/web-search.ts +73 -0
  174. package/vendor/@anvil/common/src/tools/params/tool/write-file.ts +71 -0
  175. package/vendor/@anvil/common/src/tools/params/tool/write-todos.ts +67 -0
  176. package/vendor/@anvil/common/src/tools/params/utils.ts +150 -0
  177. package/vendor/@anvil/common/src/tools/utils.ts +24 -0
  178. package/vendor/@anvil/common/src/types/__tests__/dynamic-agent-template.test.ts +20 -0
  179. package/vendor/@anvil/common/src/types/agent-template.ts +215 -0
  180. package/vendor/@anvil/common/src/types/anvil-session.ts +535 -0
  181. package/vendor/@anvil/common/src/types/anvil-streak.ts +6 -0
  182. package/vendor/@anvil/common/src/types/anvil-usage.ts +64 -0
  183. package/vendor/@anvil/common/src/types/api/agents/publish.ts +61 -0
  184. package/vendor/@anvil/common/src/types/bun-test.d.ts +5 -0
  185. package/vendor/@anvil/common/src/types/contracts/agent-runtime.ts +75 -0
  186. package/vendor/@anvil/common/src/types/contracts/analytics.ts +9 -0
  187. package/vendor/@anvil/common/src/types/contracts/bigquery.ts +55 -0
  188. package/vendor/@anvil/common/src/types/contracts/billing.ts +46 -0
  189. package/vendor/@anvil/common/src/types/contracts/client.ts +53 -0
  190. package/vendor/@anvil/common/src/types/contracts/database.ts +112 -0
  191. package/vendor/@anvil/common/src/types/contracts/env.ts +203 -0
  192. package/vendor/@anvil/common/src/types/contracts/llm.ts +191 -0
  193. package/vendor/@anvil/common/src/types/contracts/logger.ts +14 -0
  194. package/vendor/@anvil/common/src/types/contracts/logs.ts +34 -0
  195. package/vendor/@anvil/common/src/types/contracts/trace.ts +21 -0
  196. package/vendor/@anvil/common/src/types/dynamic-agent-template.ts +328 -0
  197. package/vendor/@anvil/common/src/types/filesystem.ts +10 -0
  198. package/vendor/@anvil/common/src/types/function-params.ts +33 -0
  199. package/vendor/@anvil/common/src/types/grant.ts +20 -0
  200. package/vendor/@anvil/common/src/types/gravity-index.ts +170 -0
  201. package/vendor/@anvil/common/src/types/json.ts +29 -0
  202. package/vendor/@anvil/common/src/types/mcp.ts +24 -0
  203. package/vendor/@anvil/common/src/types/messages/anvil-message.ts +58 -0
  204. package/vendor/@anvil/common/src/types/messages/content-part.ts +59 -0
  205. package/vendor/@anvil/common/src/types/messages/data-content.ts +14 -0
  206. package/vendor/@anvil/common/src/types/messages/provider-metadata.ts +13 -0
  207. package/vendor/@anvil/common/src/types/organization.ts +118 -0
  208. package/vendor/@anvil/common/src/types/print-mode.ts +121 -0
  209. package/vendor/@anvil/common/src/types/publisher.ts +67 -0
  210. package/vendor/@anvil/common/src/types/session-state.ts +149 -0
  211. package/vendor/@anvil/common/src/types/skill.ts +86 -0
  212. package/vendor/@anvil/common/src/types/source.ts +11 -0
  213. package/vendor/@anvil/common/src/types/spawn.ts +13 -0
  214. package/vendor/@anvil/common/src/types/subscription.ts +67 -0
  215. package/vendor/@anvil/common/src/types/usage.ts +16 -0
  216. package/vendor/@anvil/common/src/types/util.ts +3 -0
  217. package/vendor/@anvil/common/src/util/__tests__/ad-user-agent.test.ts +26 -0
  218. package/vendor/@anvil/common/src/util/__tests__/analytics-dispatcher.test.ts +122 -0
  219. package/vendor/@anvil/common/src/util/__tests__/analytics-log.test.ts +102 -0
  220. package/vendor/@anvil/common/src/util/__tests__/analytics-sampling.test.ts +143 -0
  221. package/vendor/@anvil/common/src/util/__tests__/anvil-streak.test.ts +202 -0
  222. package/vendor/@anvil/common/src/util/__tests__/anvil-usage-summary.test.ts +193 -0
  223. package/vendor/@anvil/common/src/util/__tests__/axiom-only-log.test.ts +140 -0
  224. package/vendor/@anvil/common/src/util/__tests__/client-user-agent.test.ts +148 -0
  225. package/vendor/@anvil/common/src/util/__tests__/engagement-tracker.test.ts +115 -0
  226. package/vendor/@anvil/common/src/util/__tests__/env-file-path.test.ts +57 -0
  227. package/vendor/@anvil/common/src/util/__tests__/error-abort.test.ts +774 -0
  228. package/vendor/@anvil/common/src/util/__tests__/error-api-details.test.ts +206 -0
  229. package/vendor/@anvil/common/src/util/__tests__/file-read-limits.test.ts +173 -0
  230. package/vendor/@anvil/common/src/util/__tests__/format-code-search.test.ts +60 -0
  231. package/vendor/@anvil/common/src/util/__tests__/log-mirror.test.ts +31 -0
  232. package/vendor/@anvil/common/src/util/__tests__/messages.test.ts +1252 -0
  233. package/vendor/@anvil/common/src/util/__tests__/partial-json-delta.test.ts +505 -0
  234. package/vendor/@anvil/common/src/util/__tests__/path.test.ts +22 -0
  235. package/vendor/@anvil/common/src/util/__tests__/project-ignore.test.ts +90 -0
  236. package/vendor/@anvil/common/src/util/__tests__/promise.test.ts +325 -0
  237. package/vendor/@anvil/common/src/util/__tests__/rate-limit.test.ts +28 -0
  238. package/vendor/@anvil/common/src/util/__tests__/reddit-anvil-retention.test.ts +130 -0
  239. package/vendor/@anvil/common/src/util/__tests__/saxy.test.ts +1008 -0
  240. package/vendor/@anvil/common/src/util/__tests__/split-data.test.ts +289 -0
  241. package/vendor/@anvil/common/src/util/__tests__/string.test.ts +239 -0
  242. package/vendor/@anvil/common/src/util/__tests__/thread-title.test.ts +70 -0
  243. package/vendor/@anvil/common/src/util/__tests__/tool-result-media-order.test.ts +108 -0
  244. package/vendor/@anvil/common/src/util/__tests__/ttft-histogram.test.ts +180 -0
  245. package/vendor/@anvil/common/src/util/__tests__/with-timeout.test.ts +24 -0
  246. package/vendor/@anvil/common/src/util/__tests__/zoned-time.test.ts +88 -0
  247. package/vendor/@anvil/common/src/util/ad-user-agent.ts +25 -0
  248. package/vendor/@anvil/common/src/util/agent-file-utils.ts +110 -0
  249. package/vendor/@anvil/common/src/util/agent-id-parsing.ts +136 -0
  250. package/vendor/@anvil/common/src/util/agent-name-normalization.ts +38 -0
  251. package/vendor/@anvil/common/src/util/agent-name-resolver.ts +86 -0
  252. package/vendor/@anvil/common/src/util/analytics-dispatcher.ts +82 -0
  253. package/vendor/@anvil/common/src/util/analytics-log.ts +78 -0
  254. package/vendor/@anvil/common/src/util/analytics-sampling.ts +255 -0
  255. package/vendor/@anvil/common/src/util/anvil-model-availability.ts +102 -0
  256. package/vendor/@anvil/common/src/util/anvil-privacy.ts +82 -0
  257. package/vendor/@anvil/common/src/util/anvil-streak-line.ts +103 -0
  258. package/vendor/@anvil/common/src/util/anvil-streak.ts +156 -0
  259. package/vendor/@anvil/common/src/util/anvil-usage-summary.ts +125 -0
  260. package/vendor/@anvil/common/src/util/array.ts +31 -0
  261. package/vendor/@anvil/common/src/util/axiom-only-log.ts +124 -0
  262. package/vendor/@anvil/common/src/util/cache-debug.ts +171 -0
  263. package/vendor/@anvil/common/src/util/client-user-agent.ts +114 -0
  264. package/vendor/@anvil/common/src/util/credentials.ts +26 -0
  265. package/vendor/@anvil/common/src/util/currency.ts +25 -0
  266. package/vendor/@anvil/common/src/util/dates.ts +81 -0
  267. package/vendor/@anvil/common/src/util/disposable-email.ts +277 -0
  268. package/vendor/@anvil/common/src/util/engagement-tracker.ts +129 -0
  269. package/vendor/@anvil/common/src/util/env-file-path.ts +40 -0
  270. package/vendor/@anvil/common/src/util/error.ts +562 -0
  271. package/vendor/@anvil/common/src/util/file-read-limits.ts +236 -0
  272. package/vendor/@anvil/common/src/util/file.ts +339 -0
  273. package/vendor/@anvil/common/src/util/format-code-search.ts +115 -0
  274. package/vendor/@anvil/common/src/util/lazy-response-ads.ts +92 -0
  275. package/vendor/@anvil/common/src/util/log-data.ts +57 -0
  276. package/vendor/@anvil/common/src/util/log-ingest.ts +58 -0
  277. package/vendor/@anvil/common/src/util/log-mirror.ts +34 -0
  278. package/vendor/@anvil/common/src/util/lru-cache.ts +67 -0
  279. package/vendor/@anvil/common/src/util/messages.ts +685 -0
  280. package/vendor/@anvil/common/src/util/min-heap.ts +87 -0
  281. package/vendor/@anvil/common/src/util/model-utils.ts +4 -0
  282. package/vendor/@anvil/common/src/util/object.ts +128 -0
  283. package/vendor/@anvil/common/src/util/partial-json-delta.ts +89 -0
  284. package/vendor/@anvil/common/src/util/path.ts +15 -0
  285. package/vendor/@anvil/common/src/util/project-ignore.ts +119 -0
  286. package/vendor/@anvil/common/src/util/promise.ts +73 -0
  287. package/vendor/@anvil/common/src/util/random.ts +15 -0
  288. package/vendor/@anvil/common/src/util/rate-limit.ts +56 -0
  289. package/vendor/@anvil/common/src/util/reddit-anvil-retention.ts +60 -0
  290. package/vendor/@anvil/common/src/util/reddit-capi-events.ts +36 -0
  291. package/vendor/@anvil/common/src/util/response-ad-positions.ts +54 -0
  292. package/vendor/@anvil/common/src/util/saxy.ts +741 -0
  293. package/vendor/@anvil/common/src/util/skills.test.ts +44 -0
  294. package/vendor/@anvil/common/src/util/skills.ts +36 -0
  295. package/vendor/@anvil/common/src/util/split-data.ts +259 -0
  296. package/vendor/@anvil/common/src/util/stop-sequence.ts +61 -0
  297. package/vendor/@anvil/common/src/util/string.ts +426 -0
  298. package/vendor/@anvil/common/src/util/system-info.ts +53 -0
  299. package/vendor/@anvil/common/src/util/thread-title.ts +46 -0
  300. package/vendor/@anvil/common/src/util/ttft-histogram.ts +73 -0
  301. package/vendor/@anvil/common/src/util/xml-parser.ts +27 -0
  302. package/vendor/@anvil/common/src/util/xml.ts +17 -0
  303. package/vendor/@anvil/common/src/util/zod-schema.ts +25 -0
  304. package/vendor/@anvil/common/src/util/zoned-time.ts +136 -0
  305. package/vendor/@anvil/common/src/utils/ask-user-bridge.ts +44 -0
  306. package/vendor/@anvil/sdk/dist/index.cjs +52297 -0
  307. package/vendor/@anvil/sdk/dist/index.cjs.map +440 -0
  308. package/vendor/@anvil/sdk/dist/index.d.ts +4277 -0
  309. package/vendor/@anvil/sdk/dist/index.mjs +52290 -0
  310. package/vendor/@anvil/sdk/dist/index.mjs.map +440 -0
  311. package/vendor/@anvil/sdk/dist/vendor/ai.cjs +30886 -0
  312. package/vendor/@anvil/sdk/dist/vendor/ripgrep/arm64-darwin/rg +0 -0
  313. package/vendor/@anvil/sdk/dist/vendor/ripgrep/arm64-linux/rg +0 -0
  314. package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-darwin/rg +0 -0
  315. package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-linux/rg +0 -0
  316. package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-win32/rg.exe +0 -0
  317. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-c-sharp.wasm +0 -0
  318. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-cpp.wasm +0 -0
  319. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-go.wasm +0 -0
  320. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-java.wasm +0 -0
  321. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-javascript.wasm +0 -0
  322. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-python.wasm +0 -0
  323. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-ruby.wasm +0 -0
  324. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-rust.wasm +0 -0
  325. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-tsx.wasm +0 -0
  326. package/vendor/@anvil/sdk/dist/wasm/tree-sitter-typescript.wasm +0 -0
  327. package/vendor/@anvil/sdk/dist/wasm/tree-sitter.wasm +0 -0
  328. package/vendor/@anvil/sdk/package.json +21 -0
@@ -0,0 +1,976 @@
1
+ /**
2
+ * Anvil account standing ("Access Level") — the per-account policy layer
3
+ * that decides how much free capacity an account gets.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * Every free-mode control before this was keyed on the ACCOUNT and applied the
8
+ * same number to every account: the same 5,000 requests/day, the same $50
9
+ * spend budget, the same 6 premium sessions. That is only a bound if accounts
10
+ * are scarce, and `docs/anvil-signup-gate.md` is the record of them not
11
+ * being. A relay pooling 100 minted accounts is entitled, entirely within the
12
+ * rules, to 100x every per-user limit.
13
+ *
14
+ * The signup gate raised the price of minting an account. This raises the
15
+ * price of a FRESH one being worth anything: a brand-new account from an
16
+ * unverifiable network gets a small fraction of the capacity, and an account
17
+ * that has demonstrably existed and done work for months gets considerably
18
+ * MORE than the flat limits ever gave it. Same fleet spend, redistributed
19
+ * toward the people we actually want to serve.
20
+ *
21
+ * ## Two axes, deliberately separate
22
+ *
23
+ * `AnvilAccessTier` (full / limited) is a REGION property, resolved per
24
+ * request from the caller's IP country. `AnvilTrustLevel` is an ACCOUNT
25
+ * property, resolved from durable facts about the account. They multiply:
26
+ * limits are a matrix, not a sum. A limited-region user can climb to `core`
27
+ * and get more than a full-region `new` account — which is the whole point of
28
+ * shipping this alongside the region split rather than instead of it.
29
+ *
30
+ * ## What the level must never be
31
+ *
32
+ * A ban input, or a reason to serve a degraded model. Everything here produces
33
+ * a NUMBER — a limit — and a limit that is reached produces a retryable 429
34
+ * naming the remedy. `docs/anvil-abuse-detection.md` records what it cost
35
+ * the last time a soft signal was allowed to convict (659 wrongly-banned
36
+ * accounts, 2026-08-03); none of the signals below is stronger than the ones
37
+ * that did it.
38
+ *
39
+ * ## Naming
40
+ *
41
+ * User-facing copy says "Access Level" and never "trust", because a user shown
42
+ * a low trust score reads an accusation. The code says `trustLevel` because
43
+ * that is what it is and a euphemism in an identifier costs a reader time.
44
+ * `ANVIL_TRUST_LEVEL_LABELS` is the one bridge between the two.
45
+ */
46
+
47
+ import type { AnvilAccessTier } from './anvil-models'
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Levels
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Ordered least- to most-established. The order is load-bearing:
55
+ * `ANVIL_TRUST_LEVELS.indexOf` is how "at least X" comparisons are done, so
56
+ * inserting a level in the middle re-ranks every comparison in one edit rather
57
+ * than requiring each call site to be found.
58
+ */
59
+ export const ANVIL_TRUST_LEVELS = [
60
+ 'new',
61
+ 'verified',
62
+ 'established',
63
+ 'core',
64
+ ] as const
65
+
66
+ export type AnvilTrustLevel = (typeof ANVIL_TRUST_LEVELS)[number]
67
+
68
+ /** The level an account holds before anything is known about it. Every failure
69
+ * path in the resolver must land somewhere DEFINITE, and this is not it — see
70
+ * `ANVIL_TRUST_FALLBACK_LEVEL`. */
71
+ export const ANVIL_TRUST_MIN_LEVEL: AnvilTrustLevel = 'new'
72
+
73
+ /**
74
+ * The level used when signals cannot be loaded (database error, timeout).
75
+ *
76
+ * `established` and NOT `new`, and this is the single most consequential
77
+ * constant in the file. This resolver runs on the free-mode hot path; if a
78
+ * Postgres hiccup dropped every caller to `new`, one degraded dependency would
79
+ * throttle the entire product to a fifth of its capacity, and it would look
80
+ * exactly like an outage nobody could attribute. Failing to the level that
81
+ * reproduces roughly today's flat limits means a broken resolver costs us the
82
+ * enforcement, never the users. Same reasoning as the signup gate's fail-open.
83
+ */
84
+ export const ANVIL_TRUST_FALLBACK_LEVEL: AnvilTrustLevel = 'established'
85
+
86
+ export function isAtLeastTrustLevel(
87
+ level: AnvilTrustLevel,
88
+ minimum: AnvilTrustLevel,
89
+ ): boolean {
90
+ return (
91
+ ANVIL_TRUST_LEVELS.indexOf(level) >=
92
+ ANVIL_TRUST_LEVELS.indexOf(minimum)
93
+ )
94
+ }
95
+
96
+ function lowerOf(
97
+ a: AnvilTrustLevel,
98
+ b: AnvilTrustLevel,
99
+ ): AnvilTrustLevel {
100
+ return isAtLeastTrustLevel(a, b) ? b : a
101
+ }
102
+
103
+ /** User-facing name. Never says "trust", "risk" or "score" — a user reading
104
+ * their own level is reading an explanation of their limits, not a verdict on
105
+ * their character. */
106
+ export const ANVIL_TRUST_LEVEL_LABELS: Record<AnvilTrustLevel, string> = {
107
+ new: 'Getting started',
108
+ verified: 'Verified',
109
+ established: 'Established',
110
+ core: 'Core member',
111
+ }
112
+
113
+ /** One line of user-facing copy per level, shown under the label. */
114
+ export const ANVIL_TRUST_LEVEL_BLURBS: Record<AnvilTrustLevel, string> = {
115
+ new: 'Welcome! Your account is brand new, so limits start small. They open up quickly — the steps below take a few minutes.',
116
+ verified:
117
+ 'Your account is verified. You have solid daily limits, and a bit of history unlocks the next level.',
118
+ established:
119
+ 'You are an established Anvil user with generous limits on messages, spend and premium sessions.',
120
+ core: 'You are a core member. You get the highest free limits we offer, in every region.',
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Limits
125
+ // ---------------------------------------------------------------------------
126
+
127
+ /**
128
+ * Everything a level controls, in one place.
129
+ *
130
+ * Every field is a per-account ceiling over a time window. None is a global
131
+ * budget and none is a concurrency cap — read each doc comment rather than
132
+ * inferring from the name.
133
+ */
134
+ export interface AnvilTrustLimits {
135
+ /**
136
+ * User prompts per Pacific day. NOT requests: one prompt is one root agent
137
+ * run, and an agentic turn behind it may make dozens of LLM calls.
138
+ *
139
+ * This is the limit that means what a person thinks "messages" means, and it
140
+ * is the one an honest heavy user should be able to feel without hitting.
141
+ * Counted only when a call OPENS a root run (`isNewPromptWindow`), so a
142
+ * caller that reuses one run id to hide many prompts pays the run-reuse
143
+ * guard's ceiling instead.
144
+ */
145
+ userMessagesPerDay: number
146
+ /** Total free-mode LLM requests in any 5-hour window: prompts plus every
147
+ * subagent, tool loop and retry underneath them. */
148
+ messagesPer5Hours: number
149
+ /** Total free-mode LLM requests per day. The overall ceiling — a premium
150
+ * request consumes this budget as well as its own. */
151
+ messagesPerDay: number
152
+ /**
153
+ * Settled non-BYOK provider cost, in USD, since midnight Pacific, at or
154
+ * above which no FRESH session is admitted. Live sessions and reclaims are
155
+ * never interrupted, so the honest reading is "how much we will spend
156
+ * starting new work for this account today".
157
+ */
158
+ dailySpendUsd: number
159
+ /**
160
+ * Premium-model sessions per Pacific day (the shared premium pool's base
161
+ * entitlement). Referral, streak and operator entitlement is ADDED on top of
162
+ * this, so a level never takes away something a user earned.
163
+ *
164
+ * Zero at every limited-region level: the limited tier cannot reach a
165
+ * premium model at all, and the number would be decoration.
166
+ */
167
+ premiumSessionsPerDay: number
168
+ }
169
+
170
+ /**
171
+ * Two axes were deliberately REMOVED from this interface, and the reasoning is
172
+ * worth keeping so they are not quietly re-added.
173
+ *
174
+ * **Browser sessions per day** (`ANVIL_WEB_STANDARD_SESSION_LIMIT`) and
175
+ * **concurrent Desktop tabs** (`ANVIL_DESKTOP_SESSION_LIMITS.unlimited`) are
176
+ * still enforced — they simply are not level-scaled. Both are session-SHAPE
177
+ * controls rather than cost controls: starting a session costs nothing, and a
178
+ * session that sits idle costs nothing either. What costs money is the traffic
179
+ * inside it, and that is already bounded four different ways by the fields
180
+ * above.
181
+ *
182
+ * Scaling them by level would therefore have taken visible, immediate
183
+ * capability away from exactly the users we most need to keep — a new user
184
+ * discovers "you may only open one tab" instantly, and it reads as a product
185
+ * that is broken rather than as a budget — in exchange for no measurable saving
186
+ * at all. The premium-session pool stays level-scaled because a premium session
187
+ * is the one whose mere existence commits us to expensive inference.
188
+ */
189
+
190
+ /**
191
+ * The matrix. Region tier picks the row, account level picks the column.
192
+ *
193
+ * ## How these numbers were chosen
194
+ *
195
+ * `established` × `full` REPRODUCES the flat limits that shipped before this
196
+ * file existed (5,000/day, 3,000/5h, $50, 6 premium), and `established` ×
197
+ * `limited` reproduces the limited row (3,000/day, 2,000/5h). That is
198
+ * deliberate and is what makes this change safe to ship:
199
+ * the population that already looked like ordinary heavy users keeps exactly
200
+ * what it had, `core` is the raise, and only accounts with no history at all
201
+ * are tightened.
202
+ *
203
+ * Sizing for the two new levels below `established` is anchored on the
204
+ * per-user-per-day distributions in `free-mode-rate-limiter.ts` (full tier p50
205
+ * 131, p90 837, p99 2,351): `verified` at 3,000/day sits above the full tier's
206
+ * p99, and `new` at 1,200/day sits above its p90. So a genuinely new user
207
+ * doing genuinely heavy work still fits, and the accounts that do not fit are
208
+ * the ones doing several times what any measured human does on their first
209
+ * day.
210
+ *
211
+ * `core` is roughly 1.6x `established` rather than unbounded. It is a reward,
212
+ * not an exemption — an account that reaches `core` and is then compromised or
213
+ * sold should still cost a bounded amount, and the fleet-wide spend has to
214
+ * survive every core member using their allowance on the same day.
215
+ *
216
+ * ## The limited row is not merely the full row scaled down
217
+ *
218
+ * Its floor is deliberately harsher (`new` × limited is a third of `new` ×
219
+ * full) because that intersection — brand-new account, unsupported region,
220
+ * often VPN — is the exact shape of the reselling farms. Its ceiling is
221
+ * deliberately generous (`core` × limited beats `verified` × full on every
222
+ * axis it can — premium is region-gated, not level-gated) because the entire
223
+ * promise this makes to a real developer in an unsupported country is that the
224
+ * region is a starting point and not a cage.
225
+ */
226
+ export const ANVIL_TRUST_LIMITS: Record<
227
+ AnvilAccessTier,
228
+ Record<AnvilTrustLevel, AnvilTrustLimits>
229
+ > = {
230
+ full: {
231
+ new: {
232
+ userMessagesPerDay: 120,
233
+ messagesPer5Hours: 800,
234
+ messagesPerDay: 1_200,
235
+ dailySpendUsd: 8,
236
+ premiumSessionsPerDay: 2,
237
+ },
238
+ verified: {
239
+ userMessagesPerDay: 300,
240
+ messagesPer5Hours: 1_800,
241
+ messagesPerDay: 3_000,
242
+ dailySpendUsd: 20,
243
+ premiumSessionsPerDay: 4,
244
+ },
245
+ established: {
246
+ userMessagesPerDay: 600,
247
+ messagesPer5Hours: 3_000,
248
+ messagesPerDay: 5_000,
249
+ dailySpendUsd: 50,
250
+ premiumSessionsPerDay: 6,
251
+ },
252
+ core: {
253
+ userMessagesPerDay: 1_000,
254
+ messagesPer5Hours: 5_000,
255
+ messagesPerDay: 8_000,
256
+ dailySpendUsd: 90,
257
+ premiumSessionsPerDay: 10,
258
+ },
259
+ },
260
+ limited: {
261
+ new: {
262
+ userMessagesPerDay: 40,
263
+ messagesPer5Hours: 400,
264
+ messagesPerDay: 500,
265
+ dailySpendUsd: 3,
266
+ premiumSessionsPerDay: 0,
267
+ },
268
+ verified: {
269
+ userMessagesPerDay: 120,
270
+ messagesPer5Hours: 1_000,
271
+ messagesPerDay: 1_500,
272
+ dailySpendUsd: 10,
273
+ premiumSessionsPerDay: 0,
274
+ },
275
+ established: {
276
+ userMessagesPerDay: 350,
277
+ messagesPer5Hours: 2_000,
278
+ messagesPerDay: 3_000,
279
+ dailySpendUsd: 25,
280
+ premiumSessionsPerDay: 0,
281
+ },
282
+ core: {
283
+ userMessagesPerDay: 700,
284
+ messagesPer5Hours: 3_500,
285
+ messagesPerDay: 5_500,
286
+ dailySpendUsd: 55,
287
+ premiumSessionsPerDay: 0,
288
+ },
289
+ },
290
+ }
291
+
292
+ export function anvilTrustLimits(
293
+ accessTier: AnvilAccessTier,
294
+ level: AnvilTrustLevel,
295
+ ): AnvilTrustLimits {
296
+ return ANVIL_TRUST_LIMITS[accessTier][level]
297
+ }
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // Signals
301
+ // ---------------------------------------------------------------------------
302
+
303
+ /**
304
+ * Everything the score is computed from.
305
+ *
306
+ * **`null` means "unknown", never "clean" and never "suspicious".** Every
307
+ * account created before `docs/anvil-signup-gate.md` shipped has null
308
+ * provenance, and every account that never linked GitHub has null GitHub
309
+ * facts. A scorer that read null as bad would demote most of the existing user
310
+ * base overnight; one that read it as good would hand every fresh signup a
311
+ * clean slate. Unknown earns nothing and costs nothing — which lands those
312
+ * accounts at whatever their other, positive signals justify.
313
+ */
314
+ export interface AnvilTrustSignals {
315
+ /** `user.created_at`. */
316
+ accountCreatedAt: Date | null
317
+ /** `referral_qualification.github_account_created_at` — set by GitHub
318
+ * server-side and not backdatable, which is what makes it worth points at
319
+ * all. A commit author date, by contrast, forges in one command. */
320
+ githubAccountCreatedAt: Date | null
321
+ /** Oldest public repo's creation date. Same non-forgeability. */
322
+ githubOldestRepoCreatedAt: Date | null
323
+ githubPublicRepos: number | null
324
+ githubFollowers: number | null
325
+ githubTwoFactorEnabled: boolean | null
326
+ /** Distinct Pacific days the account has used free mode
327
+ * (`anvil_daily_usage`). Cheap history that cannot be bought. */
328
+ activeDays: number
329
+ /** Approved bounty submissions. Reviewed proof-of-work — the single
330
+ * strongest earned signal here, and the one a limited-region user can act
331
+ * on today without owning an aged GitHub account. */
332
+ approvedBounties: number
333
+ /** Qualified referrals GIVEN (`referral_v2`, active + qualified). */
334
+ qualifiedReferrals: number
335
+ /** Has ever paid us anything. Wired now and worth points now; payments are
336
+ * planned, and the day they land this needs no scorer change. */
337
+ hasPaid: boolean
338
+ /** Privacy signals recorded at SIGNUP (`user.signup_privacy_signals`), split
339
+ * on comma. Empty array = checked and clean; null = never checked. */
340
+ signupPrivacySignals: readonly string[] | null
341
+ /** `user.signup_ip_source`. Anything other than `edge_secret`/`cloudflare`
342
+ * means the caller had some influence over the address. */
343
+ signupIpSource: string | null
344
+ /** Accounts sharing this account's signup /24 or /48. Includes this account,
345
+ * so 1 is the clean value. */
346
+ signupPrefixAccountCount: number | null
347
+ /** Accounts sharing this account's normalized mailbox. Includes this
348
+ * account. */
349
+ mailboxAccountCount: number | null
350
+ /** A ban event that was NOT reversed. Live bans never reach here (banned
351
+ * accounts are refused before any of this runs), so this is history: an
352
+ * account that was actioned and then unbanned on appeal. */
353
+ hasUnreversedBanEvent: boolean
354
+ /** `user.privacy_flagged_at` — first request ever seen on an ipinfo-flagged
355
+ * anonymizing egress, uncorroborated. Sticky by construction: written once,
356
+ * never cleared by code. The WEAK member of the sticky trio, so it carries
357
+ * the mildest cap below. */
358
+ privacyFlaggedAt: Date | null
359
+ /** `user.privacy_corroborated_at` — first request where a second provider
360
+ * agreed the egress was anonymizing. Sticky. */
361
+ privacyCorroboratedAt: Date | null
362
+ /** `user.third_party_client_at` — first free-mode request carrying a tool
363
+ * schema no Anvil client ships. Sticky, and behavioural rather than
364
+ * network-derived, which is what makes it worth a hard cap. */
365
+ thirdPartyClientAt: Date | null
366
+ /**
367
+ * The privacy verdict on the CURRENT request, from `getFreeModeRiskScore`.
368
+ * The one live signal in an otherwise durable set, and the one a user can
369
+ * change in a second by toggling a VPN — which is why it can only CAP a
370
+ * level, never contribute points.
371
+ */
372
+ currentRiskScore: number | null
373
+ }
374
+
375
+ /** A signal that moved the score, in user-facing language. */
376
+ export interface AnvilTrustFactor {
377
+ id: string
378
+ label: string
379
+ points: number
380
+ }
381
+
382
+ /** Something the user can do to move up, with what it is worth. */
383
+ export interface AnvilTrustNextStep {
384
+ id: string
385
+ label: string
386
+ detail: string
387
+ points: number
388
+ /** Where the UI should send them. Relative to the anvil web app. */
389
+ href?: string
390
+ }
391
+
392
+ export interface AnvilTrustAssessment {
393
+ level: AnvilTrustLevel
394
+ /** 0-100. Exposed so a user can see movement between levels, and so the
395
+ * thresholds below are auditable from the outside. */
396
+ score: number
397
+ /** The level the score alone earned, before caps. Equal to `level` unless a
398
+ * cap applied — which is how the UI knows to explain the cap rather than
399
+ * telling someone with 80 points to keep earning points. */
400
+ uncappedLevel: AnvilTrustLevel
401
+ /** Which cap bound, if any. */
402
+ cappedBy: string | null
403
+ factors: AnvilTrustFactor[]
404
+ nextSteps: AnvilTrustNextStep[]
405
+ }
406
+
407
+ // ---------------------------------------------------------------------------
408
+ // Scoring
409
+ // ---------------------------------------------------------------------------
410
+
411
+ /**
412
+ * The two signals a user can act on TODAY, from any country, with no aged
413
+ * GitHub account and no money.
414
+ *
415
+ * ## Why the caps are where they are
416
+ *
417
+ * They were originally 4 bounties (20) and 5 referrals (15). Together that is
418
+ * 35 points against a `core` threshold of 75, and the whole earned-only route
419
+ * — bounties, referrals, 90 days of account age, 30 active days, a clean
420
+ * residential signup — topped out at **70**. Five points short. `core` was
421
+ * literally unreachable by contribution: it required either an aged GitHub
422
+ * account or a payment.
423
+ *
424
+ * That is backwards for a program whose stated purpose is to give developers
425
+ * in unsupported regions a way to raise their own limits. It also made the
426
+ * incentive flat exactly where it should be steep — a user who completed ten
427
+ * bounties and referred twenty people scored the same as one who did four and
428
+ * five.
429
+ *
430
+ * At 6 and 10 the earned-only route reaches 95, so `core` is attainable by
431
+ * work alone, and each additional bounty or referral keeps paying well past
432
+ * the point where someone has proved they are real.
433
+ *
434
+ * ## Why raising them costs nothing against abuse
435
+ *
436
+ * Neither is cheap to manufacture. A bounty is reviewed proof-of-work, daily
437
+ * capped, and carries an anti-fraud agreement the claimant signs
438
+ * (docs/anvil-bounties.md). A qualified referral requires the REFERRED
439
+ * account to hold a GitHub account four calendar months old and to actually
440
+ * use the product, and referral farming has its own detector and clawback path
441
+ * (docs/anvil-abuse-referral-farming.md). An operator who can produce ten
442
+ * qualified referrals has already cleared a higher bar than this scorer sets.
443
+ */
444
+ export const ANVIL_TRUST_EARNED = {
445
+ BOUNTY_POINTS: 5,
446
+ BOUNTY_CAP: 6,
447
+ REFERRAL_POINTS: 3,
448
+ REFERRAL_CAP: 10,
449
+ } as const
450
+
451
+ const MAX_BOUNTY_POINTS =
452
+ ANVIL_TRUST_EARNED.BOUNTY_POINTS * ANVIL_TRUST_EARNED.BOUNTY_CAP
453
+ const MAX_REFERRAL_POINTS =
454
+ ANVIL_TRUST_EARNED.REFERRAL_POINTS * ANVIL_TRUST_EARNED.REFERRAL_CAP
455
+
456
+ /** Minimum score for each level. `new` is the floor and needs no entry. */
457
+ export const ANVIL_TRUST_THRESHOLDS: Record<
458
+ Exclude<AnvilTrustLevel, 'new'>,
459
+ number
460
+ > = {
461
+ verified: 25,
462
+ established: 50,
463
+ core: 75,
464
+ }
465
+
466
+ const DAY_MS = 24 * 60 * 60 * 1000
467
+ const MONTH_MS = 30 * DAY_MS
468
+ const YEAR_MS = 365 * DAY_MS
469
+
470
+ function ageMs(date: Date | null, now: Date): number | null {
471
+ if (!date) return null
472
+ const age = now.getTime() - date.getTime()
473
+ // A future date is a clock skew or a bad backfill, not an old account.
474
+ return age >= 0 ? age : 0
475
+ }
476
+
477
+ /** Highest threshold `score` clears. */
478
+ function levelForScore(score: number): AnvilTrustLevel {
479
+ if (score >= ANVIL_TRUST_THRESHOLDS.core) return 'core'
480
+ if (score >= ANVIL_TRUST_THRESHOLDS.established) return 'established'
481
+ if (score >= ANVIL_TRUST_THRESHOLDS.verified) return 'verified'
482
+ return 'new'
483
+ }
484
+
485
+ const PRIVACY_EGRESS_SIGNALS = new Set(['vpn', 'proxy', 'tor', 'hosting'])
486
+
487
+ function hasPrivacyEgressAtSignup(
488
+ signals: readonly string[] | null,
489
+ ): boolean | null {
490
+ if (signals === null) return null
491
+ return signals.some((signal) =>
492
+ PRIVACY_EGRESS_SIGNALS.has(signal.trim().toLowerCase()),
493
+ )
494
+ }
495
+
496
+ /**
497
+ * Score an account and resolve its level.
498
+ *
499
+ * Pure, so the policy is unit-testable and so the same function can run on the
500
+ * server (to enforce) and be reasoned about from a test (to check nobody moved
501
+ * a threshold by accident). Every I/O concern lives in the caller.
502
+ *
503
+ * ## Points earn, penalties cap
504
+ *
505
+ * Positive signals add points. Negative signals mostly do NOT subtract — they
506
+ * impose a CEILING on the resulting level. The difference matters: a
507
+ * subtracting penalty is defeated by accumulating enough of anything else,
508
+ * which is precisely what a farm operator with 200 aged GitHub accounts can
509
+ * do. A ceiling is not, and it also degrades honestly — a VPN user who cannot
510
+ * exceed `established` still gets `established`, which is today's limits.
511
+ */
512
+ export function assessAnvilTrust(
513
+ signals: AnvilTrustSignals,
514
+ now: Date = new Date(),
515
+ ): AnvilTrustAssessment {
516
+ const factors: AnvilTrustFactor[] = []
517
+ const nextSteps: AnvilTrustNextStep[] = []
518
+
519
+ const add = (id: string, label: string, points: number) => {
520
+ if (points === 0) return
521
+ factors.push({ id, label, points })
522
+ }
523
+ const step = (s: AnvilTrustNextStep) => nextSteps.push(s)
524
+
525
+ // --- GitHub -------------------------------------------------------------
526
+ // The heaviest block (up to 45) because it is the only one an abuser has to
527
+ // BUY. `docs/referrals.md` sets the economic invariant this inherits: keep
528
+ // the reward worth less than the grey-market price of an aged, qualifying
529
+ // GitHub account, and farming stops penciling out.
530
+ const githubAge = ageMs(signals.githubAccountCreatedAt, now)
531
+ if (githubAge === null) {
532
+ step({
533
+ id: 'connect_github',
534
+ label: 'Connect your GitHub account',
535
+ detail:
536
+ 'Linking a GitHub account you have had for a while is the fastest way to raise your limits. We read the account and oldest-repo creation dates, which GitHub sets and nobody can backdate.',
537
+ points: 30,
538
+ href: '/web/settings',
539
+ })
540
+ } else {
541
+ add('github_linked', 'GitHub account connected', 10)
542
+ if (githubAge >= 3 * YEAR_MS) {
543
+ add('github_age', 'GitHub account over 3 years old', 20)
544
+ } else if (githubAge >= YEAR_MS) {
545
+ add('github_age', 'GitHub account over a year old', 15)
546
+ } else if (githubAge >= 6 * MONTH_MS) {
547
+ add('github_age', 'GitHub account over 6 months old', 10)
548
+ } else {
549
+ step({
550
+ id: 'github_age',
551
+ label: 'Your GitHub account is still new',
552
+ detail:
553
+ 'Account age is worth up to 20 points and grows on its own — nothing to do here but keep the same account connected.',
554
+ points: 10,
555
+ })
556
+ }
557
+
558
+ const repoAge = ageMs(signals.githubOldestRepoCreatedAt, now)
559
+ if (repoAge !== null && repoAge >= 6 * MONTH_MS) {
560
+ add('github_repo', 'Public repo over 6 months old', 10)
561
+ }
562
+ if ((signals.githubPublicRepos ?? 0) >= 3) {
563
+ add('github_repos', '3 or more public repos', 5)
564
+ }
565
+ if ((signals.githubFollowers ?? 0) >= 5) {
566
+ add('github_followers', '5 or more GitHub followers', 5)
567
+ }
568
+ if (signals.githubTwoFactorEnabled) {
569
+ add('github_2fa', 'Two-factor auth enabled on GitHub', 5)
570
+ } else if (signals.githubTwoFactorEnabled === false) {
571
+ step({
572
+ id: 'github_2fa',
573
+ label: 'Turn on two-factor auth for GitHub',
574
+ detail:
575
+ 'Worth 5 points, and it protects the account your Anvil limits now depend on.',
576
+ points: 5,
577
+ href: 'https://github.com/settings/security',
578
+ })
579
+ }
580
+ }
581
+
582
+ // --- Account history ----------------------------------------------------
583
+ // Time and use, which cost an operator real calendar days per account and
584
+ // are the only signals a user gets for free by simply being real.
585
+ const accountAge = ageMs(signals.accountCreatedAt, now)
586
+ if (accountAge !== null) {
587
+ if (accountAge >= 90 * DAY_MS) {
588
+ add('account_age', 'Anvil account over 90 days old', 15)
589
+ } else if (accountAge >= 30 * DAY_MS) {
590
+ add('account_age', 'Anvil account over 30 days old', 10)
591
+ } else if (accountAge >= 7 * DAY_MS) {
592
+ add('account_age', 'Anvil account over 7 days old', 5)
593
+ }
594
+ }
595
+
596
+ if (signals.activeDays >= 30) {
597
+ add('active_days', 'Used Anvil on 30+ days', 10)
598
+ } else if (signals.activeDays >= 7) {
599
+ add('active_days', 'Used Anvil on 7+ days', 5)
600
+ }
601
+
602
+ // --- Earned -------------------------------------------------------------
603
+ // The routes that work from anywhere, on any account age. This is the answer
604
+ // to "I am in an unsupported region and my account is new": both of these
605
+ // are available today and together are worth 35 points, which is a level and
606
+ // a half.
607
+ const bountyPoints =
608
+ Math.min(signals.approvedBounties, ANVIL_TRUST_EARNED.BOUNTY_CAP) *
609
+ ANVIL_TRUST_EARNED.BOUNTY_POINTS
610
+ if (bountyPoints > 0) {
611
+ add(
612
+ 'bounties',
613
+ `${signals.approvedBounties} approved ${signals.approvedBounties === 1 ? 'bounty' : 'bounties'}`,
614
+ bountyPoints,
615
+ )
616
+ }
617
+ if (bountyPoints < MAX_BOUNTY_POINTS) {
618
+ step({
619
+ id: 'bounties',
620
+ label: 'Complete a bounty',
621
+ detail: `Approved bounties are worth ${ANVIL_TRUST_EARNED.BOUNTY_POINTS} points each, up to ${MAX_BOUNTY_POINTS}. They are reviewed, they work from any country, and they pay session grants on top.`,
622
+ points: MAX_BOUNTY_POINTS - bountyPoints,
623
+ href: '/web/earn',
624
+ })
625
+ }
626
+
627
+ const referralPoints =
628
+ Math.min(signals.qualifiedReferrals, ANVIL_TRUST_EARNED.REFERRAL_CAP) *
629
+ ANVIL_TRUST_EARNED.REFERRAL_POINTS
630
+ if (referralPoints > 0) {
631
+ add(
632
+ 'referrals',
633
+ `${signals.qualifiedReferrals} qualified ${signals.qualifiedReferrals === 1 ? 'referral' : 'referrals'}`,
634
+ referralPoints,
635
+ )
636
+ }
637
+ if (referralPoints < MAX_REFERRAL_POINTS) {
638
+ step({
639
+ id: 'referrals',
640
+ label: 'Invite other developers',
641
+ detail: `Each friend who signs up with a real GitHub account and uses Anvil is worth ${ANVIL_TRUST_EARNED.REFERRAL_POINTS} points, up to ${MAX_REFERRAL_POINTS} — plus the referral rewards themselves.`,
642
+ points: MAX_REFERRAL_POINTS - referralPoints,
643
+ href: '/web/earn',
644
+ })
645
+ }
646
+
647
+ if (signals.hasPaid) {
648
+ add('paid', 'Supported Anvil with a purchase', 25)
649
+ }
650
+
651
+ // --- Provenance ---------------------------------------------------------
652
+ // Small positives only. These cannot earn a level on their own; their job is
653
+ // to let a clean, ordinary signup reach `verified` without owning anything.
654
+ const signupPrivacy = hasPrivacyEgressAtSignup(signals.signupPrivacySignals)
655
+ if (signupPrivacy === false) {
656
+ add('clean_signup', 'Signed up from a residential connection', 5)
657
+ }
658
+ if (
659
+ signals.signupIpSource === 'edge_secret' ||
660
+ signals.signupIpSource === 'cloudflare'
661
+ ) {
662
+ add('verified_signup_ip', 'Verified network at signup', 5)
663
+ }
664
+
665
+ const score = Math.max(
666
+ 0,
667
+ Math.min(
668
+ 100,
669
+ factors.reduce((sum, factor) => sum + factor.points, 0),
670
+ ),
671
+ )
672
+ const uncappedLevel = levelForScore(score)
673
+
674
+ // --- Caps ---------------------------------------------------------------
675
+ // Applied after scoring, lowest wins. Each one names itself so the UI can
676
+ // explain a cap instead of telling a user with a high score to earn more.
677
+ let level = uncappedLevel
678
+ let cappedBy: string | null = null
679
+ const cap = (limit: AnvilTrustLevel, reason: string) => {
680
+ const capped = lowerOf(level, limit)
681
+ if (capped !== level) {
682
+ level = capped
683
+ cappedBy = reason
684
+ }
685
+ }
686
+
687
+ // A reversed ban is invisible here by construction — only unreversed events
688
+ // reach this field — so this is an account we actioned and did not take
689
+ // back. Not a ban (they are already unbanned) and not permanent, but not
690
+ // something to hand extra capacity to either.
691
+ if (signals.hasUnreversedBanEvent) {
692
+ cap('verified', 'past_enforcement')
693
+ }
694
+
695
+ // Signed up behind a VPN/proxy/Tor/hosting egress. Capped, not zeroed: this
696
+ // describes a lot of privacy-conscious developers as well as every farm, and
697
+ // `established` is what everyone had before this file existed.
698
+ if (signupPrivacy === true) {
699
+ cap('established', 'signup_privacy_egress')
700
+ }
701
+
702
+ // The live request is on an anonymizing network. Deliberately the only cap
703
+ // driven by a per-request signal, and deliberately the harshest, because it
704
+ // is the one an abuser toggles: without it, a farm signs up cleanly once and
705
+ // then runs everything through a proxy pool at core-member limits.
706
+ if (signals.currentRiskScore !== null && signals.currentRiskScore >= 75) {
707
+ cap('verified', 'anonymous_network')
708
+ }
709
+
710
+ // The sticky flags: things this account has DONE, remembered past the
711
+ // request that revealed them. Without these, every network cap above is
712
+ // defeated by toggling the VPN off for a day — the exact wash-trading of
713
+ // signals the caps exist to prevent. They cap rather than subtract for the
714
+ // standard reason (see "Points earn, penalties cap"), and they grade by
715
+ // evidence weight:
716
+ //
717
+ // corroborated egress -> verified two providers agreed
718
+ // foreign tool schema -> verified behavioural, not network luck
719
+ // ipinfo-only egress -> established one provider, the weak signal --
720
+ // `established` is what every
721
+ // account had before trust levels
722
+ // existed, so this cap forfeits
723
+ // only the `core` upside
724
+ //
725
+ // The 2026-08-03 mass-reversal is the reason none of these goes lower:
726
+ // network-derived evidence has wrongly actioned real users before, and a
727
+ // permanent flag with a harsh cap would make that mistake permanent too.
728
+ if (signals.privacyCorroboratedAt !== null) {
729
+ cap('verified', 'past_corroborated_egress')
730
+ }
731
+ if (signals.thirdPartyClientAt !== null) {
732
+ cap('verified', 'third_party_client')
733
+ }
734
+ if (signals.privacyFlaggedAt !== null) {
735
+ cap('established', 'past_privacy_egress')
736
+ }
737
+
738
+ // Signup networks and mailboxes that many accounts share. `?? 1` matters:
739
+ // null is unknown (pre-provenance accounts), and unknown must not cap.
740
+ if ((signals.signupPrefixAccountCount ?? 1) >= 8) {
741
+ cap('established', 'shared_signup_network')
742
+ }
743
+ if ((signals.mailboxAccountCount ?? 1) >= 3) {
744
+ cap('verified', 'shared_mailbox')
745
+ }
746
+
747
+ // Steps are ordered by what they are worth, EXCEPT that a binding cap goes
748
+ // first regardless. A capped account told to "complete a bounty for 20
749
+ // points" when points are not what binds them is being sent on an errand, so
750
+ // the cap is prepended after the sort rather than competing in it — it
751
+ // carries no points and would otherwise sink to the bottom.
752
+ const earnedSteps = nextSteps.sort((a, b) => b.points - a.points)
753
+ const actionableSteps =
754
+ cappedBy === null
755
+ ? earnedSteps
756
+ : [
757
+ {
758
+ id: `cap_${cappedBy}`,
759
+ label: CAP_REMEDIES[cappedBy]?.label ?? 'Your level is limited',
760
+ detail:
761
+ CAP_REMEDIES[cappedBy]?.detail ??
762
+ 'Something about this account limits how high your level can go.',
763
+ points: 0,
764
+ },
765
+ ...earnedSteps,
766
+ ]
767
+
768
+ return {
769
+ level,
770
+ score,
771
+ uncappedLevel,
772
+ cappedBy,
773
+ factors: factors.sort((a, b) => b.points - a.points),
774
+ nextSteps: actionableSteps,
775
+ }
776
+ }
777
+
778
+ /**
779
+ * User-facing explanation of each cap.
780
+ *
781
+ * Two of these describe something the user can fix in under a minute (turn the
782
+ * VPN off, use a different network) and are written to say exactly that. The
783
+ * other two describe history, and say so honestly rather than implying an
784
+ * action that does not exist — a "next step" a user cannot take is worse than
785
+ * no step at all.
786
+ */
787
+ const CAP_REMEDIES: Record<string, { label: string; detail: string }> = {
788
+ past_corroborated_egress: {
789
+ label: 'This account has used an anonymizing network',
790
+ detail:
791
+ 'Requests from this account were confirmed to come through a VPN, proxy or similar exit. That history caps this account at Verified. Everything else still counts toward your level.',
792
+ },
793
+ past_privacy_egress: {
794
+ label: 'This account has connected over a flagged network',
795
+ detail:
796
+ 'A connection from this account looked like an anonymizing network. That caps this account at Established. If this seems wrong — some office and university networks are misread — contact support.',
797
+ },
798
+ third_party_client: {
799
+ label: 'A non-Anvil client has used this account',
800
+ detail:
801
+ 'Requests from this account carried a client we do not ship. That caps this account at Verified. Only official Anvil apps are supported on free mode.',
802
+ },
803
+ anonymous_network: {
804
+ label: 'Turn off your VPN or proxy',
805
+ detail:
806
+ 'We cannot tell where requests from a VPN, proxy or Tor exit node come from, so those connections are capped at Verified no matter how much you have earned. Reconnect from your normal network and your level updates within a few minutes.',
807
+ },
808
+ signup_privacy_egress: {
809
+ label: 'You signed up over a VPN or proxy',
810
+ detail:
811
+ 'That caps this account at Established. Everything else still counts, and the cap applies to this account only — it is not a strike against you.',
812
+ },
813
+ shared_signup_network: {
814
+ label: 'Many accounts signed up from your network',
815
+ detail:
816
+ 'Shared offices, campuses and carrier NATs all look like this, so it caps rather than blocks. Approved bounties and referrals still raise your limits within the cap.',
817
+ },
818
+ shared_mailbox: {
819
+ label: 'Several accounts share your email address',
820
+ detail:
821
+ 'Address variations that reach one inbox (dots, or anything after a +) count as one mailbox. Using a single account raises your level.',
822
+ },
823
+ past_enforcement: {
824
+ label: 'This account was actioned in the past',
825
+ detail:
826
+ 'Your access is fully restored, but the level is capped at Verified. Contact support if you think that is wrong.',
827
+ },
828
+ }
829
+
830
+ // ---------------------------------------------------------------------------
831
+ // Wire shape
832
+ // ---------------------------------------------------------------------------
833
+
834
+ /**
835
+ * What a client is told about its own standing.
836
+ *
837
+ * Carries the resolved LIMITS as well as the level, because a client that had
838
+ * to map level → limits itself would hold a second copy of the matrix above
839
+ * and drift from it on the first tuning pass. The server owns the numbers; the
840
+ * client renders whatever it is sent.
841
+ */
842
+ /**
843
+ * What a level means, in words.
844
+ *
845
+ * ## Why the numbers do not leave the server
846
+ *
847
+ * Three reasons, and the first is the one that matters most:
848
+ *
849
+ * 1. **A published limit is a published target.** `docs/anvil-abuse-
850
+ * detection.md` records that the abuse pattern here is not bursting, it is
851
+ * "sustained pacing just under the daily caps" — so telling an operator
852
+ * exactly where the cap sits is telling them exactly how to sit under it.
853
+ * Every threshold in this file is a number we would rather they had to
854
+ * discover.
855
+ * 2. **The numbers are ours, not theirs.** `dailySpendUsd` in particular is
856
+ * our provider cost, and a user shown "$25/day" learns something about our
857
+ * margins and nothing about what they may do.
858
+ * 3. **Exact figures invite exactly the wrong conversation.** The first
859
+ * version showed them and produced people comparing screenshots and asking
860
+ * whether a smaller number meant they had been punished. A limit is meant
861
+ * to answer "can I get my work done", and that question has a qualitative
862
+ * answer.
863
+ *
864
+ * So `AnvilStandingInfo` carries these phrases and NOT `AnvilTrustLimits`
865
+ * — the raw matrix never crosses the wire, which means no client can render it
866
+ * by accident and no future surface has to remember not to.
867
+ *
868
+ * Where a user genuinely needs a count, they already have an exact one: the
869
+ * model picker renders "N of M sessions used" from the live quota snapshot,
870
+ * which is authoritative and per-model. Duplicating it here could only
871
+ * disagree with it.
872
+ */
873
+ export interface AnvilStandingHighlight {
874
+ label: string
875
+ value: string
876
+ }
877
+
878
+ const LIMIT_PHRASES: Record<
879
+ AnvilTrustLevel,
880
+ { prompts: string; depth: string; premium: string }
881
+ > = {
882
+ new: {
883
+ prompts: 'Enough to get a project started',
884
+ depth: 'Focused, shorter agent runs',
885
+ premium: 'Occasional access',
886
+ },
887
+ verified: {
888
+ prompts: 'Comfortable for everyday work',
889
+ depth: 'Full agent runs',
890
+ premium: 'Regular access',
891
+ },
892
+ established: {
893
+ prompts: 'Comfortable on heavy days',
894
+ depth: 'Long runs with plenty of subagents',
895
+ premium: 'Generous access',
896
+ },
897
+ core: {
898
+ prompts: 'The most we offer',
899
+ depth: 'The most we offer',
900
+ premium: 'The most we offer',
901
+ },
902
+ }
903
+
904
+ export function anvilStandingHighlights(
905
+ accessTier: AnvilAccessTier,
906
+ level: AnvilTrustLevel,
907
+ ): AnvilStandingHighlight[] {
908
+ const phrases = LIMIT_PHRASES[level]
909
+ return [
910
+ { label: 'Prompts a day', value: phrases.prompts },
911
+ { label: 'Work per prompt', value: phrases.depth },
912
+ {
913
+ label: 'Premium models',
914
+ // Stated as a region fact rather than as something this account lacks:
915
+ // no level in the limited row can reach a premium model, so framing it
916
+ // as a level shortfall would send the user chasing points that cannot
917
+ // buy it.
918
+ value:
919
+ anvilTrustLimits(accessTier, level).premiumSessionsPerDay > 0
920
+ ? phrases.premium
921
+ : 'Not available in your region yet',
922
+ },
923
+ ]
924
+ }
925
+
926
+ /**
927
+ * NOTE FOR CALLERS: `highlights` is what the level WOULD grant, which is only
928
+ * what the account actually gets once `ANVIL_TRUST_LEVELS=enforce`. Both
929
+ * producers gate on that (the Earn route and the session `standing` field), so
930
+ * a client that receives this can render it as fact. A third producer must do
931
+ * the same — see the comment in anvil/web/src/app/api/web/standing/route.ts
932
+ * for what happens otherwise.
933
+ */
934
+ export interface AnvilStandingInfo {
935
+ level: AnvilTrustLevel
936
+ label: string
937
+ blurb: string
938
+ score: number
939
+ /** Score at which the next level starts, or null at `core`. */
940
+ nextLevelAt: number | null
941
+ nextLevel: AnvilTrustLevel | null
942
+ cappedBy: string | null
943
+ cappedReason: string | null
944
+ factors: AnvilTrustFactor[]
945
+ nextSteps: AnvilTrustNextStep[]
946
+ accessTier: AnvilAccessTier
947
+ /** Semantic, never numeric — see AnvilStandingHighlight. */
948
+ highlights: AnvilStandingHighlight[]
949
+ }
950
+
951
+ export function toAnvilStandingInfo(
952
+ assessment: AnvilTrustAssessment,
953
+ accessTier: AnvilAccessTier,
954
+ ): AnvilStandingInfo {
955
+ const index = ANVIL_TRUST_LEVELS.indexOf(assessment.level)
956
+ const nextLevel = ANVIL_TRUST_LEVELS[index + 1] ?? null
957
+ return {
958
+ level: assessment.level,
959
+ label: ANVIL_TRUST_LEVEL_LABELS[assessment.level],
960
+ blurb: ANVIL_TRUST_LEVEL_BLURBS[assessment.level],
961
+ score: assessment.score,
962
+ nextLevel,
963
+ nextLevelAt:
964
+ nextLevel && nextLevel !== 'new'
965
+ ? ANVIL_TRUST_THRESHOLDS[nextLevel]
966
+ : null,
967
+ cappedBy: assessment.cappedBy,
968
+ cappedReason: assessment.cappedBy
969
+ ? (CAP_REMEDIES[assessment.cappedBy]?.detail ?? null)
970
+ : null,
971
+ factors: assessment.factors,
972
+ nextSteps: assessment.nextSteps,
973
+ accessTier,
974
+ highlights: anvilStandingHighlights(accessTier, assessment.level),
975
+ }
976
+ }