@wenathlan/saddle 1.8.18 → 2.0.2

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 (802) hide show
  1. package/Dockerfile +1339 -0
  2. package/README.md +33 -12
  3. package/alternatives.ts +1218 -0
  4. package/biome.json +153 -0
  5. package/boards.json +1517 -0
  6. package/compute.ts +3144 -0
  7. package/cores.json +1801 -0
  8. package/dist/acquisition.d.ts +359 -0
  9. package/dist/acquisition.d.ts.map +1 -0
  10. package/dist/acquisition.js +629 -0
  11. package/dist/acquisition.js.map +1 -0
  12. package/dist/alternatives.d.ts +509 -0
  13. package/dist/alternatives.d.ts.map +1 -0
  14. package/dist/alternatives.js +951 -0
  15. package/dist/alternatives.js.map +1 -0
  16. package/dist/automation.d.ts +426 -0
  17. package/dist/automation.d.ts.map +1 -0
  18. package/dist/automation.js +561 -0
  19. package/dist/automation.js.map +1 -0
  20. package/dist/browser.d.ts +562 -0
  21. package/dist/browser.d.ts.map +1 -0
  22. package/dist/browser.js +1057 -0
  23. package/dist/browser.js.map +1 -0
  24. package/dist/capacitor.config.d.ts +13 -0
  25. package/dist/capacitor.config.d.ts.map +1 -0
  26. package/dist/capacitor.config.js +26 -0
  27. package/dist/capacitor.config.js.map +1 -0
  28. package/dist/cli.d.ts +17 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +111 -0
  31. package/dist/cli.js.map +1 -0
  32. package/dist/communication.d.ts +192 -0
  33. package/dist/communication.d.ts.map +1 -0
  34. package/dist/communication.js +418 -0
  35. package/dist/communication.js.map +1 -0
  36. package/dist/compute.d.ts +1161 -0
  37. package/dist/compute.d.ts.map +1 -0
  38. package/dist/compute.js +2273 -0
  39. package/dist/compute.js.map +1 -0
  40. package/dist/distribution.d.ts +529 -0
  41. package/dist/distribution.d.ts.map +1 -0
  42. package/dist/distribution.js +811 -0
  43. package/dist/distribution.js.map +1 -0
  44. package/dist/execution.d.ts +530 -0
  45. package/dist/execution.d.ts.map +1 -0
  46. package/dist/execution.js +761 -0
  47. package/dist/execution.js.map +1 -0
  48. package/dist/format.d.ts +19 -0
  49. package/dist/format.d.ts.map +1 -0
  50. package/dist/format.js +60 -0
  51. package/dist/format.js.map +1 -0
  52. package/dist/foundation.d.ts +176 -0
  53. package/dist/foundation.d.ts.map +1 -0
  54. package/dist/foundation.js +309 -0
  55. package/dist/foundation.js.map +1 -0
  56. package/dist/index.d.ts +733 -123
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +1018 -123
  59. package/dist/index.js.map +1 -1
  60. package/dist/integration.d.ts +236 -0
  61. package/dist/integration.d.ts.map +1 -0
  62. package/dist/integration.js +236 -0
  63. package/dist/integration.js.map +1 -0
  64. package/dist/intelligence.d.ts +94 -0
  65. package/dist/intelligence.d.ts.map +1 -0
  66. package/dist/intelligence.js +145 -0
  67. package/dist/intelligence.js.map +1 -0
  68. package/dist/{isolation/contracts.d.ts → isolation.d.ts} +12 -1
  69. package/dist/isolation.d.ts.map +1 -0
  70. package/dist/{isolation/contracts.js → isolation.js} +13 -1
  71. package/dist/isolation.js.map +1 -0
  72. package/dist/media.d.ts +344 -0
  73. package/dist/media.d.ts.map +1 -0
  74. package/dist/media.js +986 -0
  75. package/dist/media.js.map +1 -0
  76. package/dist/modes.d.ts +359 -0
  77. package/dist/modes.d.ts.map +1 -0
  78. package/dist/modes.js +134 -0
  79. package/dist/modes.js.map +1 -0
  80. package/dist/{library/public.d.ts → operations.d.ts} +15 -1
  81. package/dist/operations.d.ts.map +1 -0
  82. package/dist/{library/public.js → operations.js} +18 -7
  83. package/dist/operations.js.map +1 -0
  84. package/dist/orchestrator.d.ts +1339 -0
  85. package/dist/orchestrator.d.ts.map +1 -0
  86. package/dist/orchestrator.js +4308 -0
  87. package/dist/orchestrator.js.map +1 -0
  88. package/dist/performance.d.ts +416 -0
  89. package/dist/performance.d.ts.map +1 -0
  90. package/dist/performance.js +827 -0
  91. package/dist/performance.js.map +1 -0
  92. package/dist/quantum.d.ts +810 -0
  93. package/dist/quantum.d.ts.map +1 -0
  94. package/dist/quantum.js +2073 -0
  95. package/dist/quantum.js.map +1 -0
  96. package/dist/render.d.ts +381 -0
  97. package/dist/render.d.ts.map +1 -0
  98. package/dist/render.js +1073 -0
  99. package/dist/render.js.map +1 -0
  100. package/dist/scheduler.d.ts +596 -0
  101. package/dist/scheduler.d.ts.map +1 -0
  102. package/dist/scheduler.js +1259 -0
  103. package/dist/scheduler.js.map +1 -0
  104. package/dist/security.d.ts +501 -0
  105. package/dist/security.d.ts.map +1 -0
  106. package/dist/security.js +1170 -0
  107. package/dist/security.js.map +1 -0
  108. package/dist/server.d.ts +21 -0
  109. package/dist/server.d.ts.map +1 -0
  110. package/dist/{server/node.js → server.js} +16 -2
  111. package/dist/server.js.map +1 -0
  112. package/dist/tiers.d.ts +1002 -0
  113. package/dist/tiers.d.ts.map +1 -0
  114. package/dist/tiers.js +2165 -0
  115. package/dist/tiers.js.map +1 -0
  116. package/dist/virtual.d.ts +641 -0
  117. package/dist/virtual.d.ts.map +1 -0
  118. package/dist/virtual.js +1285 -0
  119. package/dist/virtual.js.map +1 -0
  120. package/dist/virtualcpu.d.ts +316 -0
  121. package/dist/virtualcpu.d.ts.map +1 -0
  122. package/dist/virtualcpu.js +1180 -0
  123. package/dist/virtualcpu.js.map +1 -0
  124. package/dist/virtualgpu.d.ts +345 -0
  125. package/dist/virtualgpu.d.ts.map +1 -0
  126. package/dist/virtualgpu.js +1000 -0
  127. package/dist/virtualgpu.js.map +1 -0
  128. package/dist/virtualization.d.ts +607 -0
  129. package/dist/virtualization.d.ts.map +1 -0
  130. package/dist/virtualization.js +1187 -0
  131. package/dist/virtualization.js.map +1 -0
  132. package/dist/virtualmemory.d.ts +818 -0
  133. package/dist/virtualmemory.d.ts.map +1 -0
  134. package/dist/virtualmemory.js +1796 -0
  135. package/dist/virtualmemory.js.map +1 -0
  136. package/dist/webscrape.d.ts +720 -0
  137. package/dist/webscrape.d.ts.map +1 -0
  138. package/dist/webscrape.js +2697 -0
  139. package/dist/webscrape.js.map +1 -0
  140. package/docker.config +769 -0
  141. package/docs/CONVERSA.txt +1523 -0
  142. package/docs/alternatives.md +499 -0
  143. package/docs/architecture-1.8.19.md +76 -0
  144. package/docs/architecture-1.8.19.mmd +33 -0
  145. package/docs/architecture-1.8.19.png +0 -0
  146. package/docs/architecture.md +355 -0
  147. package/docs/brancharchive-2026-08-18.md +18 -0
  148. package/docs/browser.md +7 -0
  149. package/docs/consolidation.md +70 -0
  150. package/docs/e2ugh-engine.md +528 -0
  151. package/{dist/examples/localjob.js → docs/example-localjob.ts} +3 -3
  152. package/{dist/examples/publicapi.js → docs/example-publicapi.ts} +2 -2
  153. package/docs/hardware.md +342 -0
  154. package/docs/optimization.md +126 -0
  155. package/docs/performance.md +499 -0
  156. package/docs/planning.1.8.18.md +94 -0
  157. package/docs/planning.1.8.19.md +49 -0
  158. package/docs/releasenotes-1.8.19.md +35 -0
  159. package/docs/research-1.8.19-virtual-browser.md +27 -0
  160. package/docs/security.md +454 -0
  161. package/docs/todo-1.8.18.md +12 -0
  162. package/docs/todo-1.8.19.md +241 -0
  163. package/docs/viability.md +777 -0
  164. package/docs/virtualization.md +335 -0
  165. package/docs/web-duplicatecleanup.md +9 -0
  166. package/docs/web-ideas.md +106 -0
  167. package/docs/workflowimprovements-2026-08-19.md +70 -0
  168. package/gpumonitor.cpp +1543 -0
  169. package/gpus.json +2408 -0
  170. package/index.ts +1478 -0
  171. package/media.ts +1337 -0
  172. package/mttg.config +1549 -0
  173. package/orchestrator.ts +5462 -0
  174. package/package.json +304 -56
  175. package/passage.config +1331 -0
  176. package/performance.ts +1049 -0
  177. package/processors.json +2217 -0
  178. package/qemu.config +1062 -0
  179. package/qemubridge.py +1340 -0
  180. package/quantum.ts +2393 -0
  181. package/render.ts +1325 -0
  182. package/scheduler.ts +1600 -0
  183. package/security.ts +1449 -0
  184. package/tiers.ts +2871 -0
  185. package/tsconfig.json +29 -0
  186. package/virtualcpu.ts +1273 -0
  187. package/virtualgpu.ts +1181 -0
  188. package/virtualhardware.c +1011 -0
  189. package/virtualhardware.json +739 -0
  190. package/virtualization.ts +1628 -0
  191. package/virtualizationcore.cpp +4635 -0
  192. package/virtualmemory.ts +2303 -0
  193. package/vm.config.json +1910 -0
  194. package/{extension → web/extension}/manifest.json +1 -1
  195. package/web/readme.md +382 -0
  196. package/web/tsconfig.json +25 -0
  197. package/dist/adapters/forge.d.ts +0 -17
  198. package/dist/adapters/forge.d.ts.map +0 -1
  199. package/dist/adapters/forge.js +0 -19
  200. package/dist/adapters/forge.js.map +0 -1
  201. package/dist/adapters/forgejo.d.ts +0 -49
  202. package/dist/adapters/forgejo.d.ts.map +0 -1
  203. package/dist/adapters/forgejo.js +0 -8
  204. package/dist/adapters/forgejo.js.map +0 -1
  205. package/dist/adapters/github.d.ts +0 -12
  206. package/dist/adapters/github.d.ts.map +0 -1
  207. package/dist/adapters/github.js +0 -20
  208. package/dist/adapters/github.js.map +0 -1
  209. package/dist/adapters/gitlab.d.ts +0 -17
  210. package/dist/adapters/gitlab.d.ts.map +0 -1
  211. package/dist/adapters/gitlab.js +0 -10
  212. package/dist/adapters/gitlab.js.map +0 -1
  213. package/dist/adapters/huggingface.d.ts +0 -17
  214. package/dist/adapters/huggingface.d.ts.map +0 -1
  215. package/dist/adapters/huggingface.js +0 -6
  216. package/dist/adapters/huggingface.js.map +0 -1
  217. package/dist/adapters/socket.d.ts +0 -11
  218. package/dist/adapters/socket.d.ts.map +0 -1
  219. package/dist/adapters/socket.js +0 -17
  220. package/dist/adapters/socket.js.map +0 -1
  221. package/dist/adapters/transport.d.ts +0 -7
  222. package/dist/adapters/transport.d.ts.map +0 -1
  223. package/dist/adapters/transport.js +0 -36
  224. package/dist/adapters/transport.js.map +0 -1
  225. package/dist/ai/chunk.d.ts +0 -2
  226. package/dist/ai/chunk.d.ts.map +0 -1
  227. package/dist/ai/chunk.js +0 -36
  228. package/dist/ai/chunk.js.map +0 -1
  229. package/dist/ai/llmstxt.d.ts +0 -6
  230. package/dist/ai/llmstxt.d.ts.map +0 -1
  231. package/dist/ai/llmstxt.js +0 -12
  232. package/dist/ai/llmstxt.js.map +0 -1
  233. package/dist/ai/provenance.d.ts +0 -22
  234. package/dist/ai/provenance.d.ts.map +0 -1
  235. package/dist/ai/provenance.js +0 -25
  236. package/dist/ai/provenance.js.map +0 -1
  237. package/dist/ai/rag.d.ts +0 -26
  238. package/dist/ai/rag.d.ts.map +0 -1
  239. package/dist/ai/rag.js +0 -22
  240. package/dist/ai/rag.js.map +0 -1
  241. package/dist/ai/tokens.d.ts +0 -15
  242. package/dist/ai/tokens.d.ts.map +0 -1
  243. package/dist/ai/tokens.js +0 -9
  244. package/dist/ai/tokens.js.map +0 -1
  245. package/dist/api/auth.d.ts +0 -16
  246. package/dist/api/auth.d.ts.map +0 -1
  247. package/dist/api/auth.js +0 -19
  248. package/dist/api/auth.js.map +0 -1
  249. package/dist/api/contracts.d.ts +0 -28
  250. package/dist/api/contracts.d.ts.map +0 -1
  251. package/dist/api/contracts.js +0 -14
  252. package/dist/api/contracts.js.map +0 -1
  253. package/dist/api/control.d.ts +0 -10
  254. package/dist/api/control.d.ts.map +0 -1
  255. package/dist/api/control.js +0 -35
  256. package/dist/api/control.js.map +0 -1
  257. package/dist/api/http.d.ts +0 -7
  258. package/dist/api/http.d.ts.map +0 -1
  259. package/dist/api/http.js +0 -12
  260. package/dist/api/http.js.map +0 -1
  261. package/dist/api/rate.d.ts +0 -28
  262. package/dist/api/rate.d.ts.map +0 -1
  263. package/dist/api/rate.js +0 -36
  264. package/dist/api/rate.js.map +0 -1
  265. package/dist/api/security.d.ts +0 -12
  266. package/dist/api/security.d.ts.map +0 -1
  267. package/dist/api/security.js +0 -56
  268. package/dist/api/security.js.map +0 -1
  269. package/dist/api/service.d.ts +0 -5
  270. package/dist/api/service.d.ts.map +0 -1
  271. package/dist/api/service.js +0 -70
  272. package/dist/api/service.js.map +0 -1
  273. package/dist/apps/registry.d.ts +0 -33
  274. package/dist/apps/registry.d.ts.map +0 -1
  275. package/dist/apps/registry.js +0 -24
  276. package/dist/apps/registry.js.map +0 -1
  277. package/dist/binary/archive.d.ts +0 -27
  278. package/dist/binary/archive.d.ts.map +0 -1
  279. package/dist/binary/archive.js +0 -46
  280. package/dist/binary/archive.js.map +0 -1
  281. package/dist/binary/build.d.ts +0 -29
  282. package/dist/binary/build.d.ts.map +0 -1
  283. package/dist/binary/build.js +0 -17
  284. package/dist/binary/build.js.map +0 -1
  285. package/dist/binary/transform.d.ts +0 -70
  286. package/dist/binary/transform.d.ts.map +0 -1
  287. package/dist/binary/transform.js +0 -105
  288. package/dist/binary/transform.js.map +0 -1
  289. package/dist/bot/adapter.d.ts +0 -5
  290. package/dist/bot/adapter.d.ts.map +0 -1
  291. package/dist/bot/adapter.js +0 -11
  292. package/dist/bot/adapter.js.map +0 -1
  293. package/dist/bot/bot.d.ts +0 -28
  294. package/dist/bot/bot.d.ts.map +0 -1
  295. package/dist/bot/bot.js +0 -52
  296. package/dist/bot/bot.js.map +0 -1
  297. package/dist/bot/commands.d.ts +0 -8
  298. package/dist/bot/commands.d.ts.map +0 -1
  299. package/dist/bot/commands.js +0 -19
  300. package/dist/bot/commands.js.map +0 -1
  301. package/dist/bot/permissions.d.ts +0 -13
  302. package/dist/bot/permissions.d.ts.map +0 -1
  303. package/dist/bot/permissions.js +0 -16
  304. package/dist/bot/permissions.js.map +0 -1
  305. package/dist/browser/actions.d.ts +0 -33
  306. package/dist/browser/actions.d.ts.map +0 -1
  307. package/dist/browser/actions.js +0 -44
  308. package/dist/browser/actions.js.map +0 -1
  309. package/dist/browser/agent.d.ts +0 -21
  310. package/dist/browser/agent.d.ts.map +0 -1
  311. package/dist/browser/agent.js +0 -13
  312. package/dist/browser/agent.js.map +0 -1
  313. package/dist/browser/context.d.ts +0 -43
  314. package/dist/browser/context.d.ts.map +0 -1
  315. package/dist/browser/context.js +0 -54
  316. package/dist/browser/context.js.map +0 -1
  317. package/dist/browser/fingerprint.d.ts +0 -4
  318. package/dist/browser/fingerprint.d.ts.map +0 -1
  319. package/dist/browser/fingerprint.js +0 -12
  320. package/dist/browser/fingerprint.js.map +0 -1
  321. package/dist/browser/index.d.ts +0 -11
  322. package/dist/browser/index.d.ts.map +0 -1
  323. package/dist/browser/index.js +0 -11
  324. package/dist/browser/index.js.map +0 -1
  325. package/dist/browser/playwright.d.ts +0 -11
  326. package/dist/browser/playwright.d.ts.map +0 -1
  327. package/dist/browser/playwright.js +0 -24
  328. package/dist/browser/playwright.js.map +0 -1
  329. package/dist/browser/recorder.d.ts +0 -21
  330. package/dist/browser/recorder.d.ts.map +0 -1
  331. package/dist/browser/recorder.js +0 -58
  332. package/dist/browser/recorder.js.map +0 -1
  333. package/dist/browser/session.d.ts +0 -14
  334. package/dist/browser/session.d.ts.map +0 -1
  335. package/dist/browser/session.js +0 -21
  336. package/dist/browser/session.js.map +0 -1
  337. package/dist/browser/snapshot.d.ts +0 -69
  338. package/dist/browser/snapshot.d.ts.map +0 -1
  339. package/dist/browser/snapshot.js +0 -133
  340. package/dist/browser/snapshot.js.map +0 -1
  341. package/dist/captcha/contract.d.ts +0 -35
  342. package/dist/captcha/contract.d.ts.map +0 -1
  343. package/dist/captcha/contract.js +0 -17
  344. package/dist/captcha/contract.js.map +0 -1
  345. package/dist/captcha/evidence.d.ts +0 -10
  346. package/dist/captcha/evidence.d.ts.map +0 -1
  347. package/dist/captcha/evidence.js +0 -9
  348. package/dist/captcha/evidence.js.map +0 -1
  349. package/dist/captcha/guard.d.ts +0 -23
  350. package/dist/captcha/guard.d.ts.map +0 -1
  351. package/dist/captcha/guard.js +0 -12
  352. package/dist/captcha/guard.js.map +0 -1
  353. package/dist/cli/main.d.ts +0 -4
  354. package/dist/cli/main.d.ts.map +0 -1
  355. package/dist/cli/main.js +0 -46
  356. package/dist/cli/main.js.map +0 -1
  357. package/dist/core/errors.d.ts +0 -84
  358. package/dist/core/errors.d.ts.map +0 -1
  359. package/dist/core/errors.js +0 -54
  360. package/dist/core/errors.js.map +0 -1
  361. package/dist/core/events.d.ts +0 -9
  362. package/dist/core/events.d.ts.map +0 -1
  363. package/dist/core/events.js +0 -21
  364. package/dist/core/events.js.map +0 -1
  365. package/dist/core/hash.d.ts +0 -10
  366. package/dist/core/hash.d.ts.map +0 -1
  367. package/dist/core/hash.js +0 -82
  368. package/dist/core/hash.js.map +0 -1
  369. package/dist/core/ids.d.ts +0 -10
  370. package/dist/core/ids.d.ts.map +0 -1
  371. package/dist/core/ids.js +0 -15
  372. package/dist/core/ids.js.map +0 -1
  373. package/dist/delivery/manifest.d.ts +0 -35
  374. package/dist/delivery/manifest.d.ts.map +0 -1
  375. package/dist/delivery/manifest.js +0 -68
  376. package/dist/delivery/manifest.js.map +0 -1
  377. package/dist/deploy/index.d.ts +0 -6
  378. package/dist/deploy/index.d.ts.map +0 -1
  379. package/dist/deploy/index.js +0 -6
  380. package/dist/deploy/index.js.map +0 -1
  381. package/dist/dispatch/resumable.d.ts +0 -102
  382. package/dist/dispatch/resumable.d.ts.map +0 -1
  383. package/dist/dispatch/resumable.js +0 -64
  384. package/dist/dispatch/resumable.js.map +0 -1
  385. package/dist/dispatch/workflow.d.ts +0 -6
  386. package/dist/dispatch/workflow.d.ts.map +0 -1
  387. package/dist/dispatch/workflow.js +0 -37
  388. package/dist/dispatch/workflow.js.map +0 -1
  389. package/dist/domain/artifacts.d.ts +0 -12
  390. package/dist/domain/artifacts.d.ts.map +0 -1
  391. package/dist/domain/artifacts.js +0 -14
  392. package/dist/domain/artifacts.js.map +0 -1
  393. package/dist/domain/jobs.d.ts +0 -12
  394. package/dist/domain/jobs.d.ts.map +0 -1
  395. package/dist/domain/jobs.js +0 -20
  396. package/dist/domain/jobs.js.map +0 -1
  397. package/dist/domain/providers.d.ts +0 -10
  398. package/dist/domain/providers.d.ts.map +0 -1
  399. package/dist/domain/providers.js +0 -8
  400. package/dist/domain/providers.js.map +0 -1
  401. package/dist/domain/runtime.d.ts +0 -14
  402. package/dist/domain/runtime.d.ts.map +0 -1
  403. package/dist/domain/runtime.js +0 -10
  404. package/dist/domain/runtime.js.map +0 -1
  405. package/dist/domain/sessions.d.ts +0 -12
  406. package/dist/domain/sessions.d.ts.map +0 -1
  407. package/dist/domain/sessions.js +0 -57
  408. package/dist/domain/sessions.js.map +0 -1
  409. package/dist/examples/localjob.d.ts +0 -2
  410. package/dist/examples/localjob.d.ts.map +0 -1
  411. package/dist/examples/localjob.js.map +0 -1
  412. package/dist/examples/publicapi.d.ts +0 -2
  413. package/dist/examples/publicapi.d.ts.map +0 -1
  414. package/dist/examples/publicapi.js.map +0 -1
  415. package/dist/extension/build.d.ts +0 -9
  416. package/dist/extension/build.d.ts.map +0 -1
  417. package/dist/extension/build.js +0 -85
  418. package/dist/extension/build.js.map +0 -1
  419. package/dist/extension/content.d.ts +0 -6
  420. package/dist/extension/content.d.ts.map +0 -1
  421. package/dist/extension/content.js +0 -135
  422. package/dist/extension/content.js.map +0 -1
  423. package/dist/extension/index.d.ts +0 -7
  424. package/dist/extension/index.d.ts.map +0 -1
  425. package/dist/extension/index.js +0 -7
  426. package/dist/extension/index.js.map +0 -1
  427. package/dist/extension/pagebridge.d.ts +0 -6
  428. package/dist/extension/pagebridge.d.ts.map +0 -1
  429. package/dist/extension/pagebridge.js +0 -42
  430. package/dist/extension/pagebridge.js.map +0 -1
  431. package/dist/extension/permissions.d.ts +0 -26
  432. package/dist/extension/permissions.d.ts.map +0 -1
  433. package/dist/extension/permissions.js +0 -28
  434. package/dist/extension/permissions.js.map +0 -1
  435. package/dist/extension/popup.d.ts +0 -5
  436. package/dist/extension/popup.d.ts.map +0 -1
  437. package/dist/extension/popup.js +0 -26
  438. package/dist/extension/popup.js.map +0 -1
  439. package/dist/extension/protocol.d.ts +0 -58
  440. package/dist/extension/protocol.d.ts.map +0 -1
  441. package/dist/extension/protocol.js +0 -101
  442. package/dist/extension/protocol.js.map +0 -1
  443. package/dist/extension/serviceworker.d.ts +0 -25
  444. package/dist/extension/serviceworker.d.ts.map +0 -1
  445. package/dist/extension/serviceworker.js +0 -102
  446. package/dist/extension/serviceworker.js.map +0 -1
  447. package/dist/extension/worker.d.ts +0 -28
  448. package/dist/extension/worker.d.ts.map +0 -1
  449. package/dist/extension/worker.js +0 -22
  450. package/dist/extension/worker.js.map +0 -1
  451. package/dist/format/check.d.ts +0 -8
  452. package/dist/format/check.d.ts.map +0 -1
  453. package/dist/format/check.js +0 -38
  454. package/dist/format/check.js.map +0 -1
  455. package/dist/isolation/contracts.d.ts.map +0 -1
  456. package/dist/isolation/contracts.js.map +0 -1
  457. package/dist/library/public.d.ts.map +0 -1
  458. package/dist/library/public.js.map +0 -1
  459. package/dist/mcp/browser.d.ts +0 -9
  460. package/dist/mcp/browser.d.ts.map +0 -1
  461. package/dist/mcp/browser.js +0 -13
  462. package/dist/mcp/browser.js.map +0 -1
  463. package/dist/mcp/server.d.ts +0 -35
  464. package/dist/mcp/server.d.ts.map +0 -1
  465. package/dist/mcp/server.js +0 -40
  466. package/dist/mcp/server.js.map +0 -1
  467. package/dist/mcp/transport.d.ts +0 -7
  468. package/dist/mcp/transport.d.ts.map +0 -1
  469. package/dist/mcp/transport.js +0 -17
  470. package/dist/mcp/transport.js.map +0 -1
  471. package/dist/memory/bridge.d.ts +0 -14
  472. package/dist/memory/bridge.d.ts.map +0 -1
  473. package/dist/memory/bridge.js +0 -16
  474. package/dist/memory/bridge.js.map +0 -1
  475. package/dist/memory/engine.d.ts +0 -38
  476. package/dist/memory/engine.d.ts.map +0 -1
  477. package/dist/memory/engine.js +0 -109
  478. package/dist/memory/engine.js.map +0 -1
  479. package/dist/memory/modes.d.ts +0 -43
  480. package/dist/memory/modes.d.ts.map +0 -1
  481. package/dist/memory/modes.js +0 -66
  482. package/dist/memory/modes.js.map +0 -1
  483. package/dist/memory/objects.d.ts +0 -21
  484. package/dist/memory/objects.d.ts.map +0 -1
  485. package/dist/memory/objects.js +0 -20
  486. package/dist/memory/objects.js.map +0 -1
  487. package/dist/memory/planner.d.ts +0 -66
  488. package/dist/memory/planner.d.ts.map +0 -1
  489. package/dist/memory/planner.js +0 -108
  490. package/dist/memory/planner.js.map +0 -1
  491. package/dist/memory/targets.d.ts +0 -78
  492. package/dist/memory/targets.d.ts.map +0 -1
  493. package/dist/memory/targets.js +0 -29
  494. package/dist/memory/targets.js.map +0 -1
  495. package/dist/memory/transforms.d.ts +0 -19
  496. package/dist/memory/transforms.d.ts.map +0 -1
  497. package/dist/memory/transforms.js +0 -14
  498. package/dist/memory/transforms.js.map +0 -1
  499. package/dist/modes/matrix.d.ts +0 -23
  500. package/dist/modes/matrix.d.ts.map +0 -1
  501. package/dist/modes/matrix.js +0 -18
  502. package/dist/modes/matrix.js.map +0 -1
  503. package/dist/modes/modes.d.ts +0 -33
  504. package/dist/modes/modes.d.ts.map +0 -1
  505. package/dist/modes/modes.js +0 -15
  506. package/dist/modes/modes.js.map +0 -1
  507. package/dist/modes/resolve.d.ts +0 -96
  508. package/dist/modes/resolve.d.ts.map +0 -1
  509. package/dist/modes/resolve.js +0 -48
  510. package/dist/modes/resolve.js.map +0 -1
  511. package/dist/observability/metrics.d.ts +0 -14
  512. package/dist/observability/metrics.d.ts.map +0 -1
  513. package/dist/observability/metrics.js +0 -18
  514. package/dist/observability/metrics.js.map +0 -1
  515. package/dist/packager/manifest.d.ts +0 -66
  516. package/dist/packager/manifest.d.ts.map +0 -1
  517. package/dist/packager/manifest.js +0 -94
  518. package/dist/packager/manifest.js.map +0 -1
  519. package/dist/packager/publish.d.ts +0 -51
  520. package/dist/packager/publish.d.ts.map +0 -1
  521. package/dist/packager/publish.js +0 -18
  522. package/dist/packager/publish.js.map +0 -1
  523. package/dist/packager/targetcli.d.ts +0 -7
  524. package/dist/packager/targetcli.d.ts.map +0 -1
  525. package/dist/packager/targetcli.js +0 -22
  526. package/dist/packager/targetcli.js.map +0 -1
  527. package/dist/persistence/adapter.d.ts +0 -5
  528. package/dist/persistence/adapter.d.ts.map +0 -1
  529. package/dist/persistence/adapter.js +0 -11
  530. package/dist/persistence/adapter.js.map +0 -1
  531. package/dist/persistence/drizzle.d.ts +0 -2
  532. package/dist/persistence/drizzle.d.ts.map +0 -1
  533. package/dist/persistence/drizzle.js +0 -12
  534. package/dist/persistence/drizzle.js.map +0 -1
  535. package/dist/persistence/memory.d.ts +0 -2
  536. package/dist/persistence/memory.d.ts.map +0 -1
  537. package/dist/persistence/memory.js +0 -27
  538. package/dist/persistence/memory.js.map +0 -1
  539. package/dist/persistence/migrations.d.ts +0 -19
  540. package/dist/persistence/migrations.d.ts.map +0 -1
  541. package/dist/persistence/migrations.js +0 -13
  542. package/dist/persistence/migrations.js.map +0 -1
  543. package/dist/persistence/prisma.d.ts +0 -2
  544. package/dist/persistence/prisma.d.ts.map +0 -1
  545. package/dist/persistence/prisma.js +0 -25
  546. package/dist/persistence/prisma.js.map +0 -1
  547. package/dist/persistence/schema.d.ts +0 -63
  548. package/dist/persistence/schema.d.ts.map +0 -1
  549. package/dist/persistence/schema.js +0 -28
  550. package/dist/persistence/schema.js.map +0 -1
  551. package/dist/persistence/sql.d.ts +0 -3
  552. package/dist/persistence/sql.d.ts.map +0 -1
  553. package/dist/persistence/sql.js +0 -37
  554. package/dist/persistence/sql.js.map +0 -1
  555. package/dist/protocol/blocks.d.ts +0 -10
  556. package/dist/protocol/blocks.d.ts.map +0 -1
  557. package/dist/protocol/blocks.js +0 -26
  558. package/dist/protocol/blocks.js.map +0 -1
  559. package/dist/protocol/json.d.ts +0 -6
  560. package/dist/protocol/json.d.ts.map +0 -1
  561. package/dist/protocol/json.js +0 -6
  562. package/dist/protocol/json.js.map +0 -1
  563. package/dist/protocol/ndjson.d.ts +0 -3
  564. package/dist/protocol/ndjson.d.ts.map +0 -1
  565. package/dist/protocol/ndjson.js +0 -20
  566. package/dist/protocol/ndjson.js.map +0 -1
  567. package/dist/protocol/sse.d.ts +0 -7
  568. package/dist/protocol/sse.d.ts.map +0 -1
  569. package/dist/protocol/sse.js +0 -25
  570. package/dist/protocol/sse.js.map +0 -1
  571. package/dist/proxy/pool.d.ts +0 -9
  572. package/dist/proxy/pool.d.ts.map +0 -1
  573. package/dist/proxy/pool.js +0 -27
  574. package/dist/proxy/pool.js.map +0 -1
  575. package/dist/queue/idempotency.d.ts +0 -10
  576. package/dist/queue/idempotency.d.ts.map +0 -1
  577. package/dist/queue/idempotency.js +0 -13
  578. package/dist/queue/idempotency.js.map +0 -1
  579. package/dist/queue/persistent.d.ts +0 -14
  580. package/dist/queue/persistent.d.ts.map +0 -1
  581. package/dist/queue/persistent.js +0 -66
  582. package/dist/queue/persistent.js.map +0 -1
  583. package/dist/queue/queue.d.ts +0 -7
  584. package/dist/queue/queue.d.ts.map +0 -1
  585. package/dist/queue/queue.js +0 -55
  586. package/dist/queue/queue.js.map +0 -1
  587. package/dist/queue/saga.d.ts +0 -5
  588. package/dist/queue/saga.d.ts.map +0 -1
  589. package/dist/queue/saga.js +0 -20
  590. package/dist/queue/saga.js.map +0 -1
  591. package/dist/release/assets.d.ts +0 -144
  592. package/dist/release/assets.d.ts.map +0 -1
  593. package/dist/release/assets.js +0 -157
  594. package/dist/release/assets.js.map +0 -1
  595. package/dist/release/evidence.d.ts +0 -92
  596. package/dist/release/evidence.d.ts.map +0 -1
  597. package/dist/release/evidence.js +0 -148
  598. package/dist/release/evidence.js.map +0 -1
  599. package/dist/release/verify.d.ts +0 -12
  600. package/dist/release/verify.d.ts.map +0 -1
  601. package/dist/release/verify.js +0 -112
  602. package/dist/release/verify.js.map +0 -1
  603. package/dist/runners/chain.d.ts +0 -125
  604. package/dist/runners/chain.d.ts.map +0 -1
  605. package/dist/runners/chain.js +0 -95
  606. package/dist/runners/chain.js.map +0 -1
  607. package/dist/runners/health.d.ts +0 -36
  608. package/dist/runners/health.d.ts.map +0 -1
  609. package/dist/runners/health.js +0 -26
  610. package/dist/runners/health.js.map +0 -1
  611. package/dist/runners/heartbeat.d.ts +0 -23
  612. package/dist/runners/heartbeat.d.ts.map +0 -1
  613. package/dist/runners/heartbeat.js +0 -29
  614. package/dist/runners/heartbeat.js.map +0 -1
  615. package/dist/runners/inprocess.d.ts +0 -17
  616. package/dist/runners/inprocess.d.ts.map +0 -1
  617. package/dist/runners/inprocess.js +0 -20
  618. package/dist/runners/inprocess.js.map +0 -1
  619. package/dist/runners/scheduler.d.ts +0 -5
  620. package/dist/runners/scheduler.d.ts.map +0 -1
  621. package/dist/runners/scheduler.js +0 -19
  622. package/dist/runners/scheduler.js.map +0 -1
  623. package/dist/runtime/abort.d.ts +0 -8
  624. package/dist/runtime/abort.d.ts.map +0 -1
  625. package/dist/runtime/abort.js +0 -13
  626. package/dist/runtime/abort.js.map +0 -1
  627. package/dist/runtime/compatibility.d.ts +0 -37
  628. package/dist/runtime/compatibility.d.ts.map +0 -1
  629. package/dist/runtime/compatibility.js +0 -10
  630. package/dist/runtime/compatibility.js.map +0 -1
  631. package/dist/runtime/detect.d.ts +0 -13
  632. package/dist/runtime/detect.d.ts.map +0 -1
  633. package/dist/runtime/detect.js +0 -18
  634. package/dist/runtime/detect.js.map +0 -1
  635. package/dist/runtime/engine.d.ts +0 -20
  636. package/dist/runtime/engine.d.ts.map +0 -1
  637. package/dist/runtime/engine.js +0 -61
  638. package/dist/runtime/engine.js.map +0 -1
  639. package/dist/runtime/retry.d.ts +0 -19
  640. package/dist/runtime/retry.d.ts.map +0 -1
  641. package/dist/runtime/retry.js +0 -54
  642. package/dist/runtime/retry.js.map +0 -1
  643. package/dist/runtime/worker.d.ts +0 -10
  644. package/dist/runtime/worker.d.ts.map +0 -1
  645. package/dist/runtime/worker.js +0 -25
  646. package/dist/runtime/worker.js.map +0 -1
  647. package/dist/scrape/cache.d.ts +0 -45
  648. package/dist/scrape/cache.d.ts.map +0 -1
  649. package/dist/scrape/cache.js +0 -45
  650. package/dist/scrape/cache.js.map +0 -1
  651. package/dist/scrape/crawl.d.ts +0 -47
  652. package/dist/scrape/crawl.d.ts.map +0 -1
  653. package/dist/scrape/crawl.js +0 -114
  654. package/dist/scrape/crawl.js.map +0 -1
  655. package/dist/scrape/extract.d.ts +0 -63
  656. package/dist/scrape/extract.d.ts.map +0 -1
  657. package/dist/scrape/extract.js +0 -47
  658. package/dist/scrape/extract.js.map +0 -1
  659. package/dist/scrape/normalize.d.ts +0 -52
  660. package/dist/scrape/normalize.d.ts.map +0 -1
  661. package/dist/scrape/normalize.js +0 -102
  662. package/dist/scrape/normalize.js.map +0 -1
  663. package/dist/scrape/robots.d.ts +0 -37
  664. package/dist/scrape/robots.d.ts.map +0 -1
  665. package/dist/scrape/robots.js +0 -71
  666. package/dist/scrape/robots.js.map +0 -1
  667. package/dist/scrape/schema.d.ts +0 -23
  668. package/dist/scrape/schema.d.ts.map +0 -1
  669. package/dist/scrape/schema.js +0 -92
  670. package/dist/scrape/schema.js.map +0 -1
  671. package/dist/scrape/scraper.d.ts +0 -6
  672. package/dist/scrape/scraper.d.ts.map +0 -1
  673. package/dist/scrape/scraper.js +0 -46
  674. package/dist/scrape/scraper.js.map +0 -1
  675. package/dist/scrape/semantic.d.ts +0 -25
  676. package/dist/scrape/semantic.d.ts.map +0 -1
  677. package/dist/scrape/semantic.js +0 -27
  678. package/dist/scrape/semantic.js.map +0 -1
  679. package/dist/server/node.d.ts +0 -7
  680. package/dist/server/node.d.ts.map +0 -1
  681. package/dist/server/node.js.map +0 -1
  682. package/dist/sessions/file.d.ts +0 -25
  683. package/dist/sessions/file.d.ts.map +0 -1
  684. package/dist/sessions/file.js +0 -13
  685. package/dist/sessions/file.js.map +0 -1
  686. package/dist/sessions/replay.d.ts +0 -9
  687. package/dist/sessions/replay.d.ts.map +0 -1
  688. package/dist/sessions/replay.js +0 -70
  689. package/dist/sessions/replay.js.map +0 -1
  690. package/dist/sessions/store.d.ts +0 -25
  691. package/dist/sessions/store.d.ts.map +0 -1
  692. package/dist/sessions/store.js +0 -13
  693. package/dist/sessions/store.js.map +0 -1
  694. package/dist/storage/adapter.d.ts +0 -5
  695. package/dist/storage/adapter.d.ts.map +0 -1
  696. package/dist/storage/adapter.js +0 -11
  697. package/dist/storage/adapter.js.map +0 -1
  698. package/dist/storage/cache.d.ts +0 -23
  699. package/dist/storage/cache.d.ts.map +0 -1
  700. package/dist/storage/cache.js +0 -79
  701. package/dist/storage/cache.js.map +0 -1
  702. package/dist/storage/checksum.d.ts +0 -3
  703. package/dist/storage/checksum.d.ts.map +0 -1
  704. package/dist/storage/checksum.js +0 -19
  705. package/dist/storage/checksum.js.map +0 -1
  706. package/dist/storage/chunked.d.ts +0 -21
  707. package/dist/storage/chunked.d.ts.map +0 -1
  708. package/dist/storage/chunked.js +0 -67
  709. package/dist/storage/chunked.js.map +0 -1
  710. package/dist/storage/content.d.ts +0 -24
  711. package/dist/storage/content.d.ts.map +0 -1
  712. package/dist/storage/content.js +0 -44
  713. package/dist/storage/content.js.map +0 -1
  714. package/dist/storage/filehosting.d.ts +0 -2
  715. package/dist/storage/filehosting.d.ts.map +0 -1
  716. package/dist/storage/filehosting.js +0 -24
  717. package/dist/storage/filehosting.js.map +0 -1
  718. package/dist/storage/githubcontents.d.ts +0 -2
  719. package/dist/storage/githubcontents.d.ts.map +0 -1
  720. package/dist/storage/githubcontents.js +0 -29
  721. package/dist/storage/githubcontents.js.map +0 -1
  722. package/dist/storage/index.d.ts +0 -12
  723. package/dist/storage/index.d.ts.map +0 -1
  724. package/dist/storage/index.js +0 -12
  725. package/dist/storage/index.js.map +0 -1
  726. package/dist/storage/local.d.ts +0 -2
  727. package/dist/storage/local.d.ts.map +0 -1
  728. package/dist/storage/local.js +0 -72
  729. package/dist/storage/local.js.map +0 -1
  730. package/dist/storage/memory.d.ts +0 -6
  731. package/dist/storage/memory.d.ts.map +0 -1
  732. package/dist/storage/memory.js +0 -36
  733. package/dist/storage/memory.js.map +0 -1
  734. package/dist/storage/pool.d.ts +0 -95
  735. package/dist/storage/pool.d.ts.map +0 -1
  736. package/dist/storage/pool.js +0 -202
  737. package/dist/storage/pool.js.map +0 -1
  738. package/dist/storage/s3compatible.d.ts +0 -3
  739. package/dist/storage/s3compatible.d.ts.map +0 -1
  740. package/dist/storage/s3compatible.js +0 -70
  741. package/dist/storage/s3compatible.js.map +0 -1
  742. package/dist/storage/sync.d.ts +0 -47
  743. package/dist/storage/sync.d.ts.map +0 -1
  744. package/dist/storage/sync.js +0 -75
  745. package/dist/storage/sync.js.map +0 -1
  746. package/dist/surfaces/adapters.d.ts +0 -97
  747. package/dist/surfaces/adapters.d.ts.map +0 -1
  748. package/dist/surfaces/adapters.js +0 -50
  749. package/dist/surfaces/adapters.js.map +0 -1
  750. package/dist/surfaces/controls.d.ts +0 -18
  751. package/dist/surfaces/controls.d.ts.map +0 -1
  752. package/dist/surfaces/controls.js +0 -45
  753. package/dist/surfaces/controls.js.map +0 -1
  754. package/dist/surfaces/manifest.d.ts +0 -68
  755. package/dist/surfaces/manifest.d.ts.map +0 -1
  756. package/dist/surfaces/manifest.js +0 -42
  757. package/dist/surfaces/manifest.js.map +0 -1
  758. package/dist/surfaces/n8n.d.ts +0 -25
  759. package/dist/surfaces/n8n.d.ts.map +0 -1
  760. package/dist/surfaces/n8n.js +0 -31
  761. package/dist/surfaces/n8n.js.map +0 -1
  762. package/dist/surfaces/operations.d.ts +0 -32
  763. package/dist/surfaces/operations.d.ts.map +0 -1
  764. package/dist/surfaces/operations.js +0 -53
  765. package/dist/surfaces/operations.js.map +0 -1
  766. package/dist/surfaces/requirements.d.ts +0 -29
  767. package/dist/surfaces/requirements.d.ts.map +0 -1
  768. package/dist/surfaces/requirements.js +0 -50
  769. package/dist/surfaces/requirements.js.map +0 -1
  770. package/dist/surfaces/targets.d.ts +0 -196
  771. package/dist/surfaces/targets.d.ts.map +0 -1
  772. package/dist/surfaces/targets.js +0 -27
  773. package/dist/surfaces/targets.js.map +0 -1
  774. package/dist/webhook/delivery.d.ts +0 -18
  775. package/dist/webhook/delivery.d.ts.map +0 -1
  776. package/dist/webhook/delivery.js +0 -36
  777. package/dist/webhook/delivery.js.map +0 -1
  778. package/dist/webhook/receiver.d.ts +0 -26
  779. package/dist/webhook/receiver.d.ts.map +0 -1
  780. package/dist/webhook/receiver.js +0 -24
  781. package/dist/webhook/receiver.js.map +0 -1
  782. package/dist/webhook/signature.d.ts +0 -3
  783. package/dist/webhook/signature.d.ts.map +0 -1
  784. package/dist/webhook/signature.js +0 -7
  785. package/dist/webhook/signature.js.map +0 -1
  786. package/dist/workflow/manifest.d.ts +0 -22
  787. package/dist/workflow/manifest.d.ts.map +0 -1
  788. package/dist/workflow/manifest.js +0 -21
  789. package/dist/workflow/manifest.js.map +0 -1
  790. package/dist/workflow/registry.d.ts +0 -7
  791. package/dist/workflow/registry.d.ts.map +0 -1
  792. package/dist/workflow/registry.js +0 -18
  793. package/dist/workflow/registry.js.map +0 -1
  794. package/dist/workflow/templates.d.ts +0 -6
  795. package/dist/workflow/templates.d.ts.map +0 -1
  796. package/dist/workflow/templates.js +0 -18
  797. package/dist/workflow/templates.js.map +0 -1
  798. package/dist/workflow/triggers.d.ts +0 -64
  799. package/dist/workflow/triggers.d.ts.map +0 -1
  800. package/dist/workflow/triggers.js +0 -99
  801. package/dist/workflow/triggers.js.map +0 -1
  802. /package/{extension/README.md → docs/extension.md} +0 -0
package/dist/tiers.js ADDED
@@ -0,0 +1,2165 @@
1
+ /**
2
+ * tiers - the "everything is VRAM" virtual memory layer of saddle.
3
+ *
4
+ * The v17 doctrine: in the end everything is VRAM. The VM impersonates
5
+ * physical hardware the way browsers spoof headers, and the only physical
6
+ * memory allowed lives in GitHub files (artifacts/GHCR), free bucket
7
+ * accounts and the npm CDNs. This module implements that vision on four
8
+ * tiers: L1 ram (Map working set plus SharedArrayBuffer ring), L2 vram
9
+ * (compute-bound tier of the catalog), L3 storage ram (repositories: a
10
+ * real node:sqlite kvstore, the npm chunk farm, GitHub REST) and L4
11
+ * external buckets (hf, kaggle, terabox, r2, storj, turso). It ships a
12
+ * real WAL SQLite backend with LRU eviction, the npm chunk planner with
13
+ * jsDelivr/UNPKG/esm.run URLs, a GitHub REST client that degrades to
14
+ * planner mode without a token, the VDR 64-bit BigInt address space with
15
+ * a ring that auto-flushes at the 512 MB ceiling and demotes blocks into
16
+ * L3, printable kernel bridge recipes (zram/tmpfs/swap/mmap/sysctl/
17
+ * cgroups v2), the storage==compute thesis with a real magic byte
18
+ * sniffer, and the memoryengine facade. Zero dependencies, node:* only,
19
+ * native fetch, no localhost, no emoji.
20
+ *
21
+ * Contexts (24): tierserror; TIERS catalog; LATENCYLADDER +
22
+ * autoscalebysize; FREEPOOL quotas; sqlitekv (WAL/LRU); storagebackend +
23
+ * rambufferbackend; sqlitel3backend; zip codec (crc32/zipbuild/zipread);
24
+ * plannpmchunks + reassembleplan + publishplan; npmchunkregistry;
25
+ * githubstorage (artifacts/releases/blobs); VDR addressing + memoryblock
26
+ * + vrdblockheaders; pagetable + localpagetable; upstashplanner;
27
+ * vrdringbuffer; universalvdrengine; kernel recipes (zram/tmpfs/swap);
28
+ * storagerambridgeplanner (mmap); sysctldropin + cgroupsv2slice;
29
+ * sniffmagic + virtualfilesystem; memoryengine; creatiersengine;
30
+ * quotaplanner (hf/kaggle/terabox/r2/storj/forges); tiersreport.
31
+ *
32
+ * Canonical sources: readme1.md lines 367-534 (memory 7-tier model,
33
+ * MemoryEngine API, storage to RAM bridge, SQLite as RAM), 505-519
34
+ * (free pool and quotas), 898-913 (VDR design). Toolchain TS 7.0.2.
35
+ */
36
+ import { createHash } from 'node:crypto';
37
+ import { tmpdir } from 'node:os';
38
+ import { join } from 'node:path';
39
+ import { DatabaseSync } from 'node:sqlite';
40
+ import { deflateRawSync, gunzipSync, gzipSync, inflateRawSync } from 'node:zlib';
41
+ /* ------------------------------------------------------------------ */
42
+ /* Section 1: errors */
43
+ /* ------------------------------------------------------------------ */
44
+ /** Error thrown by every tiers subsystem with an optional cause chain. */
45
+ export class tierserror extends Error {
46
+ /** Machine readable subsystem tag. */
47
+ subsystem;
48
+ constructor(message, options) {
49
+ super(message, options);
50
+ this.name = 'tierserror';
51
+ this.subsystem = options?.subsystem ?? 'tiers';
52
+ }
53
+ }
54
+ /** Wraps a fallible callback into a tierserror with subsystem context. */
55
+ function guard(subsystem, what, body) {
56
+ try {
57
+ return body();
58
+ }
59
+ catch (cause) {
60
+ if (cause instanceof tierserror) {
61
+ throw cause;
62
+ }
63
+ throw new tierserror(`${what} failed`, { cause: cause, subsystem });
64
+ }
65
+ }
66
+ /**
67
+ * TIERS: the consolidated tier model. L1 is system RAM (~100 ns, limited,
68
+ * ephemeral), L2 is GPU VRAM (compute-bound), L3 is storage RAM held by
69
+ * repositories (practically unlimited) and L4 is the external bucket farm
70
+ * (unlimited, quoting the ~50 us r2 edge rung of the ladder).
71
+ */
72
+ export const TIERS = {
73
+ l1: {
74
+ id: 'l1',
75
+ label: 'RAM',
76
+ source: 'system',
77
+ capacity: 'limited, ephemeral',
78
+ latencyns: 100,
79
+ detail: 'working set Map plus SharedArrayBuffer ring; zram amplification applies',
80
+ },
81
+ l2: {
82
+ id: 'l2',
83
+ label: 'VRAM',
84
+ source: 'gpu',
85
+ capacity: 'limited, compute-bound',
86
+ latencyns: 500,
87
+ detail: 'the tier the whole VM impersonates; served by float32 VDR blocks',
88
+ },
89
+ l3: {
90
+ id: 'l3',
91
+ label: 'Storage RAM',
92
+ source: 'repositories',
93
+ capacity: 'practically unlimited',
94
+ latencyns: 10000,
95
+ detail: 'sqlite kvstore, npm chunk packages, github artifacts and releases',
96
+ },
97
+ l4: {
98
+ id: 'l4',
99
+ label: 'External buckets',
100
+ source: 'hf, kaggle, terabox, r2, storj',
101
+ capacity: 'unlimited',
102
+ latencyns: 50000,
103
+ detail: 'bucket farm aggregated by rclone into one virtual pool',
104
+ },
105
+ };
106
+ /** Returns one tier descriptor, throwing for unknown identifiers. */
107
+ export function selecttier(id) {
108
+ const tier = TIERS[id];
109
+ if (tier === undefined) {
110
+ throw new tierserror(`unknown vram tier "${id}"; valid tiers: ${Object.keys(TIERS).join(', ')}`);
111
+ }
112
+ return tier;
113
+ }
114
+ /**
115
+ * LATENCYLADDER: the honest ordering of every bridge technique, from
116
+ * ~100 ns DRAM through ~500 ns zram pages, ~1 us tmpfs, ~5 us mmap page
117
+ * faults, ~10 us WAL SQLite reads, down to the ~50 us R2 edge best case.
118
+ */
119
+ export const LATENCYLADDER = [
120
+ { kind: 'ram', latencyns: 100, note: 'DRAM working set, Map or SharedArrayBuffer' },
121
+ { kind: 'zram', latencyns: 500, note: 'compressed block device in RAM as swap (zstd/lz4, 2-3x)' },
122
+ { kind: 'tmpfs', latencyns: 1000, note: 'ramdisk, ~10x faster than SSD, half of RAM by default' },
123
+ {
124
+ kind: 'mmap',
125
+ latencyns: 5000,
126
+ note: 'file mapped into virtual memory: storage and RAM at once',
127
+ },
128
+ { kind: 'sqlite', latencyns: 10000, note: 'WAL DatabaseSync kvstore read' },
129
+ { kind: 'r2', latencyns: 50000, note: 'edge cache best case; remote reality is 50-300 ms' },
130
+ ];
131
+ /** Returns the latency of one ladder rung, throwing for unknown kinds. */
132
+ export function latencyfor(kind) {
133
+ const rung = LATENCYLADDER.find((level) => level.kind === kind);
134
+ if (rung === undefined) {
135
+ throw new tierserror(`unknown latency kind "${kind}"`);
136
+ }
137
+ return rung.latencyns;
138
+ }
139
+ /**
140
+ * autoscalebysize applies the canonical sizing rule: below 64 MB the
141
+ * block lives in memfs (L1), below 1 GB it rides the mmap bridge (L1 with
142
+ * file backing) and anything larger goes to the SQLite kvstore (L3) or to
143
+ * R2 (L4) when the remote hint is set.
144
+ */
145
+ export function autoscalebysize(bytes, hint) {
146
+ return guard('autoscale', 'autoscalebysize', () => {
147
+ if (!Number.isFinite(bytes) || bytes < 0) {
148
+ throw new tierserror(`bytes must be a non-negative number, received ${bytes}`);
149
+ }
150
+ if (bytes < 64 * 1024 * 1024) {
151
+ return {
152
+ strategy: 'memfs',
153
+ tier: 'l1',
154
+ latencyns: latencyfor('ram'),
155
+ rationale: `${bytes} bytes < 64 MB: memfs keeps the block in the working set`,
156
+ };
157
+ }
158
+ if (bytes < 1024 * 1024 * 1024) {
159
+ return {
160
+ strategy: 'mmap',
161
+ tier: 'l1',
162
+ latencyns: latencyfor('mmap'),
163
+ rationale: `${bytes} bytes < 1 GB: the mmap bridge files the block without copying`,
164
+ };
165
+ }
166
+ return hint === 'remote'
167
+ ? {
168
+ strategy: 'r2',
169
+ tier: 'l4',
170
+ latencyns: latencyfor('r2'),
171
+ rationale: `${bytes} bytes with the remote hint: R2 chunks at the bucket farm`,
172
+ }
173
+ : {
174
+ strategy: 'sqlite',
175
+ tier: 'l3',
176
+ latencyns: latencyfor('sqlite'),
177
+ rationale: `${bytes} bytes >= 1 GB: the WAL kvstore pages the block on demand`,
178
+ };
179
+ });
180
+ }
181
+ /** Citation of the free pool table (saddle docs). */
182
+ export const FREEPOOLSOURCE = 'saddle docs readme1.md lines 505-519 (free infrastructure pool and quotas), lines 474-492 (storage backends)';
183
+ /**
184
+ * FREEPOOL: the real quota table of the only physical memory the doctrine
185
+ * allows. The counted families sum to 33029.5 GB (~33 TB) before the
186
+ * uncountable npm CDN farm joins, hence the ">33 TB" headline.
187
+ */
188
+ export const FREEPOOL = [
189
+ {
190
+ backend: 'github',
191
+ quota: '500 MB artifacts + 10 GB actions cache (7-90 d), 2000 min/mo',
192
+ gb: 10.5,
193
+ source: FREEPOOLSOURCE,
194
+ },
195
+ {
196
+ backend: 'huggingface',
197
+ quota: '~10 TB free best-effort (10 TB public + 1 TB private PRO, Xet 500 GB/file)',
198
+ gb: 10000,
199
+ source: FREEPOOLSOURCE,
200
+ },
201
+ {
202
+ backend: 'kaggle',
203
+ quota: '20 TB public datasets (200 GB/dataset, 50 top-level files, free CDN egress)',
204
+ gb: 20000,
205
+ source: FREEPOOLSOURCE,
206
+ },
207
+ {
208
+ backend: 'terabox',
209
+ quota: '3 TB over three 1 TB accounts (4 GB/file free, 300 files/transfer)',
210
+ gb: 3000,
211
+ source: FREEPOOLSOURCE,
212
+ },
213
+ {
214
+ backend: 'npm',
215
+ quota: '~unlimited: 250 MB tarball/version served verbatim by three CDNs',
216
+ gb: null,
217
+ source: FREEPOOLSOURCE,
218
+ },
219
+ {
220
+ backend: 'r2',
221
+ quota: '10 GB free, 10 M ops/mo, egress free, 5 GB per file',
222
+ gb: 10,
223
+ source: FREEPOOLSOURCE,
224
+ },
225
+ {
226
+ backend: 'turso',
227
+ quota: '9 GB free SQL (500 databases, 500 M rows)',
228
+ gb: 9,
229
+ source: FREEPOOLSOURCE,
230
+ },
231
+ ];
232
+ /** Sums the counted families and returns the headline plus npm on top. */
233
+ export function freepooltotalgb() {
234
+ return guard('freepool', 'freepooltotalgb', () => {
235
+ let counted = 0;
236
+ for (const entry of FREEPOOL) {
237
+ if (entry.gb !== null) {
238
+ counted += entry.gb;
239
+ }
240
+ }
241
+ return {
242
+ countedgb: counted,
243
+ headline: `>${(counted / 1000).toFixed(0)} TB counted (${counted} GB) plus the unlimited npm CDN farm`,
244
+ };
245
+ });
246
+ }
247
+ /**
248
+ * sqlitekv is the real L3 local backend: a node:sqlite DatabaseSync file
249
+ * opened in the tmpdir by default (SADDLE_TIERS_DB overrides the path)
250
+ * with the canonical RAM pragmas (WAL, synchronous=NORMAL,
251
+ * cache_size=10000, temp_store=MEMORY) and the kvstore table of the
252
+ * docs. Reads touch the LRU column through UPDATE ... SET accessedat =
253
+ * strftime ... RETURNING value, lruvacuate evicts oldest-first until the
254
+ * budget fits, and the class is Disposable for `using` declarations.
255
+ */
256
+ export class sqlitekv {
257
+ /** Absolute path of the database file. */
258
+ path;
259
+ #db;
260
+ #closed = false;
261
+ #getstmt = null;
262
+ #setstmt = null;
263
+ #delstmt = null;
264
+ constructor(path) {
265
+ this.path = path ?? process.env.SADDLE_TIERS_DB ?? join(tmpdir(), 'saddle-tiers.db');
266
+ this.#db = guard('sqlitekv', `sqlitekv open ${this.path}`, () => {
267
+ const db = new DatabaseSync(this.path);
268
+ db.exec('PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; ' +
269
+ 'PRAGMA cache_size = 10000; PRAGMA temp_store = MEMORY;');
270
+ db.exec('CREATE TABLE IF NOT EXISTS kvstore (key TEXT PRIMARY KEY, value BLOB NOT NULL, ' +
271
+ 'createdat INTEGER NOT NULL, accessedat INTEGER NOT NULL); ' +
272
+ 'CREATE INDEX IF NOT EXISTS kvstore_lru ON kvstore(accessedat, createdat);');
273
+ return db;
274
+ });
275
+ }
276
+ /** True once Symbol.dispose has closed the handle. */
277
+ get closed() {
278
+ return this.#closed;
279
+ }
280
+ /** Reads one key while touching the LRU clock in the same statement. */
281
+ get(key) {
282
+ return guard('sqlitekv', `sqlitekv.get(${key})`, () => {
283
+ this.#assertopen();
284
+ if (typeof key !== 'string' || key.length === 0 || key.length > 512) {
285
+ throw new tierserror(`invalid kvstore key length ${key?.length ?? 0}`);
286
+ }
287
+ this.#getstmt ??= this.#db.prepare("UPDATE kvstore SET accessedat = CAST(strftime('%s','now') AS INTEGER) " +
288
+ 'WHERE key = ? RETURNING value');
289
+ const row = this.#getstmt.get(key);
290
+ return row === undefined ? null : Buffer.from(row.value);
291
+ });
292
+ }
293
+ /** Upserts one key with a fresh LRU clock, keeping the original createdat. */
294
+ set(key, value) {
295
+ guard('sqlitekv', `sqlitekv.set(${key})`, () => {
296
+ this.#assertopen();
297
+ if (typeof key !== 'string' || key.length === 0 || key.length > 512) {
298
+ throw new tierserror(`invalid kvstore key length ${key?.length ?? 0}`);
299
+ }
300
+ if (!Buffer.isBuffer(value)) {
301
+ throw new tierserror('sqlitekv.set expects a Buffer value');
302
+ }
303
+ this.#setstmt ??= this.#db.prepare('INSERT INTO kvstore (key, value, createdat, accessedat) VALUES (?, ?, ' +
304
+ "CAST(strftime('%s','now') AS INTEGER), CAST(strftime('%s','now') AS INTEGER)) " +
305
+ 'ON CONFLICT(key) DO UPDATE SET value = excluded.value, accessedat = excluded.accessedat');
306
+ this.#setstmt.run(key, value);
307
+ });
308
+ }
309
+ /** Deletes one key, returning true when a row was removed. */
310
+ delete(key) {
311
+ return guard('sqlitekv', `sqlitekv.delete(${key})`, () => {
312
+ this.#assertopen();
313
+ this.#delstmt ??= this.#db.prepare('DELETE FROM kvstore WHERE key = ?');
314
+ return this.#delstmt.run(key).changes > 0;
315
+ });
316
+ }
317
+ /** Lists keys, optionally filtered by prefix (page table reloads). */
318
+ keys(prefix) {
319
+ return guard('sqlitekv', 'sqlitekv.keys', () => {
320
+ this.#assertopen();
321
+ const rows = (prefix === undefined
322
+ ? this.#db.prepare('SELECT key FROM kvstore ORDER BY key').all()
323
+ : this.#db
324
+ .prepare('SELECT key FROM kvstore WHERE key LIKE ? ESCAPE "\\" ORDER BY key')
325
+ .all(`${prefix.replace(/[%_\\]/g, (c) => `\\${c}`)}%`));
326
+ return rows.map((row) => row.key);
327
+ });
328
+ }
329
+ /**
330
+ * Evicts least recently used rows until the stored bytes fit the budget,
331
+ * returning the evicted count; accessedat orders, createdat breaks ties.
332
+ */
333
+ lruvacuate(tobytes) {
334
+ return guard('sqlitekv', 'sqlitekv.lruvacuate', () => {
335
+ this.#assertopen();
336
+ if (!Number.isFinite(tobytes) || tobytes < 0) {
337
+ throw new tierserror(`tobytes must be non-negative, received ${tobytes}`);
338
+ }
339
+ let evicted = 0;
340
+ for (;;) {
341
+ const totals = this.#db
342
+ .prepare('SELECT TOTAL(LENGTH(value)) AS bytes FROM kvstore')
343
+ .get();
344
+ if (totals.bytes === null || totals.bytes <= tobytes) {
345
+ return evicted;
346
+ }
347
+ const oldest = this.#db
348
+ .prepare('SELECT key FROM kvstore ORDER BY accessedat, createdat, key LIMIT 1')
349
+ .get();
350
+ if (oldest === undefined) {
351
+ return evicted;
352
+ }
353
+ this.#db.prepare('DELETE FROM kvstore WHERE key = ?').run(oldest.key);
354
+ evicted += 1;
355
+ }
356
+ });
357
+ }
358
+ /** Returns the live statistics of the database. */
359
+ stats() {
360
+ return guard('sqlitekv', 'sqlitekv.stats', () => {
361
+ this.#assertopen();
362
+ const counts = this.#db.prepare('SELECT COUNT(*) AS keys FROM kvstore').get();
363
+ const sizes = this.#db.prepare('SELECT TOTAL(LENGTH(value)) AS bytes FROM kvstore').get();
364
+ const journal = this.#db.prepare('PRAGMA journal_mode').get();
365
+ const oldest = this.#db
366
+ .prepare('SELECT MIN(accessedat) AS oldestaccess FROM kvstore')
367
+ .get();
368
+ return {
369
+ keys: counts.keys,
370
+ bytes: Math.round(sizes.bytes ?? 0),
371
+ path: this.path,
372
+ journalmode: journal.journal_mode,
373
+ oldestaccess: oldest.oldestaccess,
374
+ };
375
+ });
376
+ }
377
+ /** Closes the database handle (Disposable for `using` declarations). */
378
+ [Symbol.dispose]() {
379
+ guard('sqlitekv', 'sqlitekv dispose', () => {
380
+ if (!this.#closed) {
381
+ this.#db.close();
382
+ this.#closed = true;
383
+ }
384
+ });
385
+ }
386
+ #assertopen() {
387
+ if (this.#closed) {
388
+ throw new tierserror('sqlitekv is closed');
389
+ }
390
+ }
391
+ }
392
+ /**
393
+ * rambufferbackend is the L1 working set: a Map of keys to Buffers with
394
+ * an insertion-order LRU ceiling; the oldest entry drops when the ceiling
395
+ * is exceeded (the L3 backends still hold the bytes, so the drop is a
396
+ * demotion in disguise, not a loss).
397
+ */
398
+ export class rambufferbackend {
399
+ id = 'ram';
400
+ tier = 'l1';
401
+ label = 'l1 ram working set (Map)';
402
+ latencyns = 100;
403
+ planner = false;
404
+ #slots = new Map();
405
+ #maxbytes;
406
+ constructor(options) {
407
+ this.#maxbytes = options?.maxbytes ?? 64 * 1024 * 1024;
408
+ }
409
+ /** Maximum bytes kept in the working set. */
410
+ get maxbytes() {
411
+ return this.#maxbytes;
412
+ }
413
+ async get(key) {
414
+ return guard('ram', `rambufferbackend.get(${key})`, () => {
415
+ const hit = this.#slots.get(key) ?? null;
416
+ if (hit !== null) {
417
+ this.#slots.delete(key);
418
+ this.#slots.set(key, hit);
419
+ }
420
+ return hit;
421
+ });
422
+ }
423
+ async set(key, buffer) {
424
+ guard('ram', `rambufferbackend.set(${key})`, () => {
425
+ this.#slots.delete(key);
426
+ this.#slots.set(key, buffer);
427
+ while (this.bytes() > this.#maxbytes && this.#slots.size > 1) {
428
+ const oldest = this.#slots.keys().next();
429
+ if (oldest.done === true) {
430
+ break;
431
+ }
432
+ this.#slots.delete(oldest.value);
433
+ }
434
+ });
435
+ }
436
+ async delete(key) {
437
+ return this.#slots.delete(key);
438
+ }
439
+ /** Bytes currently held by the working set. */
440
+ bytes() {
441
+ let total = 0;
442
+ for (const buffer of this.#slots.values()) {
443
+ total += buffer.byteLength;
444
+ }
445
+ return total;
446
+ }
447
+ }
448
+ /* ------------------------------------------------------------------ */
449
+ /* Section 7: sqlite L3 backend */
450
+ /* ------------------------------------------------------------------ */
451
+ /**
452
+ * sqlitel3backend adapts sqlitekv to the storagebackend contract: the L3
453
+ * storage ram of the ladder at ~10 us per read. It owns the kv handle it
454
+ * creates and closes it on dispose; a borrowed handle stays open.
455
+ */
456
+ export class sqlitel3backend {
457
+ id = 'sqlite';
458
+ tier = 'l3';
459
+ label = 'l3 storage ram (node:sqlite WAL kvstore)';
460
+ latencyns = 10000;
461
+ planner = false;
462
+ #kv;
463
+ #owned;
464
+ constructor(options) {
465
+ if (options?.kv !== undefined) {
466
+ this.#kv = options.kv;
467
+ this.#owned = false;
468
+ }
469
+ else {
470
+ this.#kv = new sqlitekv(options?.path);
471
+ this.#owned = true;
472
+ }
473
+ }
474
+ /** The kv handle behind this backend. */
475
+ get kv() {
476
+ return this.#kv;
477
+ }
478
+ async get(key) {
479
+ return this.#kv.get(key);
480
+ }
481
+ async set(key, buffer) {
482
+ this.#kv.set(key, buffer);
483
+ }
484
+ async delete(key) {
485
+ return this.#kv.delete(key);
486
+ }
487
+ /** Bytes stored in the kv table. */
488
+ bytes() {
489
+ return this.#kv.stats().bytes;
490
+ }
491
+ /** Live statistics passthrough. */
492
+ stats() {
493
+ return this.#kv.stats();
494
+ }
495
+ /** Closes the kv handle when this backend owns it. */
496
+ [Symbol.dispose]() {
497
+ if (this.#owned) {
498
+ this.#kv[Symbol.dispose]();
499
+ }
500
+ }
501
+ }
502
+ /* ------------------------------------------------------------------ */
503
+ /* Section 8: minimal zip codec (crc32, zipbuild, zipread) */
504
+ /* ------------------------------------------------------------------ */
505
+ const CRC32TABLE = (() => {
506
+ const table = new Uint32Array(256);
507
+ for (let n = 0; n < 256; n += 1) {
508
+ let c = n;
509
+ for (let k = 0; k < 8; k += 1) {
510
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
511
+ }
512
+ table[n] = c >>> 0;
513
+ }
514
+ return table;
515
+ })();
516
+ /** CRC-32 (IEEE 802.3, polynomial 0xedb88320) of a byte payload. */
517
+ export function crc32(data) {
518
+ let c = 0xffffffff;
519
+ for (let i = 0; i < data.length; i += 1) {
520
+ c = CRC32TABLE[(c ^ data[i]) & 0xff] ^ (c >>> 8);
521
+ }
522
+ return (c ^ 0xffffffff) >>> 0;
523
+ }
524
+ /**
525
+ * zipbuild assembles a real zip archive in memory with deflate payloads
526
+ * from node:zlib: local file headers (PK 03 04), one central directory
527
+ * entry per file (PK 01 02) and the end of central directory record (PK
528
+ * 05 06). GitHub artifacts are zip envelopes, so the GitHub backend
529
+ * ships this writer instead of shelling out.
530
+ */
531
+ export function zipbuild(entries) {
532
+ return guard('zip', 'zipbuild', () => {
533
+ const locals = [];
534
+ const centrals = [];
535
+ let offset = 0;
536
+ for (const entry of entries) {
537
+ const namebuf = Buffer.from(entry.name, 'utf8');
538
+ const checksum = crc32(entry.data);
539
+ const deflated = deflateRawSync(entry.data);
540
+ const local = Buffer.alloc(30);
541
+ local.writeUInt32LE(0x04034b50, 0);
542
+ local.writeUInt16LE(20, 4);
543
+ local.writeUInt16LE(8, 8);
544
+ local.writeUInt16LE(0x21, 12);
545
+ local.writeUInt32LE(checksum, 14);
546
+ local.writeUInt32LE(deflated.length, 18);
547
+ local.writeUInt32LE(entry.data.length, 22);
548
+ local.writeUInt16LE(namebuf.length, 26);
549
+ locals.push(local, namebuf, deflated);
550
+ const central = Buffer.alloc(46);
551
+ central.writeUInt32LE(0x02014b50, 0);
552
+ central.writeUInt16LE(20, 4);
553
+ central.writeUInt16LE(20, 6);
554
+ central.writeUInt16LE(8, 10);
555
+ central.writeUInt16LE(0x21, 14);
556
+ central.writeUInt32LE(checksum, 16);
557
+ central.writeUInt32LE(deflated.length, 20);
558
+ central.writeUInt32LE(entry.data.length, 24);
559
+ central.writeUInt16LE(namebuf.length, 28);
560
+ central.writeUInt32LE(offset, 42);
561
+ centrals.push(central, namebuf);
562
+ offset += 30 + namebuf.length + deflated.length;
563
+ }
564
+ const centraldir = Buffer.concat(centrals);
565
+ const eocd = Buffer.alloc(22);
566
+ eocd.writeUInt32LE(0x06054b50, 0);
567
+ eocd.writeUInt16LE(entries.length, 8);
568
+ eocd.writeUInt16LE(entries.length, 10);
569
+ eocd.writeUInt32LE(centraldir.length, 12);
570
+ eocd.writeUInt32LE(offset, 16);
571
+ return Buffer.concat([...locals, centraldir, eocd]);
572
+ });
573
+ }
574
+ /**
575
+ * zipread parses an archive from any zip writer: it scans the end of
576
+ * central directory record, walks the central directory, re-reads each
577
+ * local header for the true extra field length and inflates the payload
578
+ * with inflateRawSync.
579
+ */
580
+ export function zipread(archive) {
581
+ return guard('zip', 'zipread', () => {
582
+ if (archive.length < 22) {
583
+ throw new tierserror('archive is shorter than the end of central directory record');
584
+ }
585
+ let eocd = -1;
586
+ for (let i = archive.length - 22; i >= Math.max(0, archive.length - 65557); i -= 1) {
587
+ if (archive.readUInt32LE(i) === 0x06054b50) {
588
+ eocd = i;
589
+ break;
590
+ }
591
+ }
592
+ if (eocd < 0) {
593
+ throw new tierserror('end of central directory signature not found');
594
+ }
595
+ const count = archive.readUInt16LE(eocd + 10);
596
+ let cursor = archive.readUInt32LE(eocd + 16);
597
+ const entries = [];
598
+ for (let i = 0; i < count; i += 1) {
599
+ if (cursor + 46 > archive.length || archive.readUInt32LE(cursor) !== 0x02014b50) {
600
+ throw new tierserror(`corrupt central directory entry ${i}`);
601
+ }
602
+ const method = archive.readUInt16LE(cursor + 10);
603
+ const compsize = archive.readUInt32LE(cursor + 20);
604
+ const namelen = archive.readUInt16LE(cursor + 28);
605
+ const trail = archive.readUInt16LE(cursor + 30) + archive.readUInt16LE(cursor + 32);
606
+ const localoffset = archive.readUInt32LE(cursor + 42);
607
+ const name = archive.subarray(cursor + 46, cursor + 46 + namelen).toString('utf8');
608
+ if (archive.readUInt32LE(localoffset) !== 0x04034b50) {
609
+ throw new tierserror(`corrupt local header for ${name}`);
610
+ }
611
+ const datastart = localoffset +
612
+ 30 +
613
+ archive.readUInt16LE(localoffset + 26) +
614
+ archive.readUInt16LE(localoffset + 28);
615
+ const stored = archive.subarray(datastart, datastart + compsize);
616
+ const data = method === 0 ? Buffer.from(stored) : method === 8 ? inflateRawSync(stored) : null;
617
+ if (data === null) {
618
+ throw new tierserror(`unsupported compression method ${method} for ${name}`);
619
+ }
620
+ entries.push({ name, data });
621
+ cursor += 46 + namelen + trail;
622
+ }
623
+ return entries;
624
+ });
625
+ }
626
+ /* ------------------------------------------------------------------ */
627
+ /* Section 9: npm chunk planner (npm as physical storage) */
628
+ /* ------------------------------------------------------------------ */
629
+ /** Default chunk size: 200 MB, the saddle split benchmark. */
630
+ export const NPMCHUNKBYTES = 200 * 1024 * 1024;
631
+ /**
632
+ * The honest constant of the npm farm: the payload travels as
633
+ * dist/chunk-NNN.bin.js inside one package per chunk because the .bin.js
634
+ * suffix lets binary bytes pass the npm tarball scan while jsDelivr,
635
+ * UNPKG and esm.run serve the file verbatim from their CDNs.
636
+ */
637
+ export const NPMCHUNKNOTE = 'content ships as dist/chunk-NNN.bin.js: the .bin.js suffix escapes the npm binary scan; ' +
638
+ 'jsDelivr/UNPKG/esm.run serve the bytes verbatim (100 MB cap per jsDelivr npm file, ' +
639
+ '250 MB tarball per version, so 200 MB chunks ride one package each)';
640
+ function sha256hex(data) {
641
+ return createHash('sha256').update(data).digest('hex');
642
+ }
643
+ function sanitizescope(scope) {
644
+ const clean = scope.replace(/^@+/, '').toLowerCase();
645
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(clean) || clean.length > 64) {
646
+ throw new tierserror(`invalid npm scope "${scope}"`);
647
+ }
648
+ return clean;
649
+ }
650
+ /**
651
+ * plannpmchunks splits one payload into the @scope/assets-NNN package
652
+ * series at 200 MB by default: each chunk gets its dist/chunk-NNN.bin.js
653
+ * filename, its real sha256 checksum and the three CDN URLs (jsDelivr,
654
+ * UNPKG, esm.run) that serve it without any npm install. A 300 MB buffer
655
+ * yields two chunks (200 MB + 100 MB), the canonical split of the docs.
656
+ */
657
+ export function plannpmchunks(buffer, options) {
658
+ return guard('npmchunks', 'plannpmchunks', () => {
659
+ const scope = sanitizescope(options?.scope ?? 'saddle');
660
+ const chunkbytes = options?.chunkbytes ?? NPMCHUNKBYTES;
661
+ const version = options?.version ?? '1.0.0';
662
+ if (!Number.isFinite(chunkbytes) || chunkbytes < 1) {
663
+ throw new tierserror(`chunkbytes must be positive, received ${chunkbytes}`);
664
+ }
665
+ if (!/^\d+\.\d+\.\d+(-[a-z0-9.-]+)?$/.test(version)) {
666
+ throw new tierserror(`invalid semver "${version}"`);
667
+ }
668
+ const chunks = [];
669
+ const total = buffer.byteLength;
670
+ for (let start = 0, index = 0; start < total || index === 0; start += chunkbytes, index += 1) {
671
+ const slice = buffer.subarray(start, Math.min(start + chunkbytes, total));
672
+ const tag = String(index + 1).padStart(3, '0');
673
+ const packagename = `@${scope}/assets-${tag}`;
674
+ const filename = `dist/chunk-${tag}.bin.js`;
675
+ chunks.push({
676
+ index,
677
+ packagename,
678
+ filename,
679
+ bytes: slice.byteLength,
680
+ sha256: sha256hex(slice),
681
+ cdnurls: [
682
+ `https://cdn.jsdelivr.net/npm/${packagename}@${version}/${filename}`,
683
+ `https://unpkg.com/${packagename}@${version}/${filename}`,
684
+ `https://esm.run/${packagename}@${version}/${filename}`,
685
+ ],
686
+ });
687
+ if (start + chunkbytes >= total) {
688
+ break;
689
+ }
690
+ }
691
+ return { scope, version, chunkbytes, totalbytes: total, chunks, note: NPMCHUNKNOTE };
692
+ });
693
+ }
694
+ /**
695
+ * reassembleplan turns a layout into the executable reconstruction
696
+ * script: one curl per CDN URL, one sha256 verification per chunk and the
697
+ * final cat that rebuilds the original bytes in order.
698
+ */
699
+ export function reassembleplan(layout) {
700
+ return guard('npmchunks', 'reassembleplan', () => {
701
+ const steps = [];
702
+ const verify = [];
703
+ const manifest = [];
704
+ for (const chunk of layout.chunks) {
705
+ const local = chunk.filename.split('/').pop();
706
+ steps.push(`curl -fsSL "${chunk.cdnurls[0]}" -o "${local}"`);
707
+ verify.push(`echo "${chunk.sha256} ${local}" | sha256sum -c -`);
708
+ manifest.push({ url: chunk.cdnurls[0], sha256: chunk.sha256 });
709
+ }
710
+ steps.push(`cat ${layout.chunks.map((chunk) => chunk.filename.split('/').pop()).join(' ')} > payload-reassembled.bin`);
711
+ return { steps, verify, manifest, expectedbytes: layout.totalbytes };
712
+ });
713
+ }
714
+ /**
715
+ * publishplan emits the real publish sequence per chunk: package.json,
716
+ * payload copy into dist/, `npm publish --access public`, and optionally
717
+ * `npm unpublish` after the flush to a durable tier, exactly like the
718
+ * VDR npm-ephemeral fabric describes.
719
+ */
720
+ export function publishplan(layout, options) {
721
+ return guard('npmchunks', 'publishplan', () => {
722
+ const perchunk = layout.chunks.map((chunk) => {
723
+ const dir = chunk.packagename.split('/')[1];
724
+ return {
725
+ packagename: chunk.packagename,
726
+ commands: [
727
+ `mkdir -p ${dir}/dist`,
728
+ `printf '{"name":"${chunk.packagename}","version":"${layout.version}","description":"storage chunk"}' > ${dir}/package.json`,
729
+ `cp ${chunk.filename.split('/').pop()} ${dir}/${chunk.filename}`,
730
+ `cd ${dir} && npm publish --access public`,
731
+ ],
732
+ };
733
+ });
734
+ return {
735
+ perchunk,
736
+ unpublish: options?.unpublishafterflush
737
+ ? layout.chunks.map((chunk) => `npm unpublish ${chunk.packagename}@${layout.version} --force # after flush`)
738
+ : [],
739
+ note: NPMCHUNKNOTE,
740
+ };
741
+ });
742
+ }
743
+ /* ------------------------------------------------------------------ */
744
+ /* Section 10: npmchunkregistry (planner backend) */
745
+ /* ------------------------------------------------------------------ */
746
+ /**
747
+ * npmchunkregistry registers payloads as npm chunk layouts: set() plans
748
+ * the @scope/assets-NNN series without touching the network and get() is
749
+ * honest about needing the CDN fetch described by reassembleplan (this
750
+ * backend is a planner, flagged as such), so the engine keeps walking
751
+ * down the ladder instead of faking a hit.
752
+ */
753
+ export class npmchunkregistry {
754
+ id = 'npmchunks';
755
+ tier = 'l3';
756
+ label = 'l3 npm cdn farm (planner: @scope/assets-NNN .bin.js chunks)';
757
+ latencyns = 50000;
758
+ planner = true;
759
+ #scope;
760
+ #chunkbytes;
761
+ #layouts = new Map();
762
+ constructor(options) {
763
+ this.#scope = sanitizescope(options?.scope ?? 'saddle');
764
+ this.#chunkbytes = options?.chunkbytes ?? NPMCHUNKBYTES;
765
+ }
766
+ async get(key) {
767
+ // planner backend: retrieval needs the CDN fetch of reassembleplan.
768
+ void key;
769
+ return null;
770
+ }
771
+ async set(key, buffer) {
772
+ guard('npmchunks', `npmchunkregistry.set(${key})`, () => {
773
+ this.#layouts.set(key, plannpmchunks(buffer, { scope: this.#scope, chunkbytes: this.#chunkbytes }));
774
+ });
775
+ }
776
+ async delete(key) {
777
+ return this.#layouts.delete(key);
778
+ }
779
+ /** Bytes counted across registered layouts. */
780
+ bytes() {
781
+ let total = 0;
782
+ for (const layout of this.#layouts.values()) {
783
+ total += layout.totalbytes;
784
+ }
785
+ return total;
786
+ }
787
+ /** Returns the planned layout of one key, or null when absent. */
788
+ layout(key) {
789
+ return this.#layouts.get(key) ?? null;
790
+ }
791
+ /** Lists every registered key with its chunk count. */
792
+ keys() {
793
+ return [...this.#layouts.entries()].map(([key, layout]) => ({
794
+ key,
795
+ chunks: layout.chunks.length,
796
+ }));
797
+ }
798
+ }
799
+ /**
800
+ * githubstorage is the REST client for the only physical memory the
801
+ * doctrine allows: GitHub artifacts (zip envelopes from the local codec,
802
+ * POSTed with a correct multipart boundary), release assets on
803
+ * uploads.github.com (2 GiB per file, enforced locally before the wire)
804
+ * and the contents API as the page-table sync (GET to read, PUT with the
805
+ * sha of the previous revision: the honest If-Match equivalent). Without
806
+ * SADDLE_GITHUB_TOKEN every call degrades to planner mode and returns the
807
+ * exact request it would have sent. The public REST surface documents
808
+ * list/get/delete plus the zip download redirect; the upload path
809
+ * POSTs to /actions/artifacts/{id} as the Actions runtime does.
810
+ */
811
+ export class githubstorage {
812
+ id = 'github';
813
+ tier = 'l3';
814
+ label = 'l3 github artifacts/releases/blobs';
815
+ latencyns = 50000;
816
+ owner;
817
+ repo;
818
+ #token;
819
+ #apiversion = '2022-11-28';
820
+ #planned = new Map();
821
+ constructor(options) {
822
+ const parsed = guard('github', 'githubstorage constructor', () => {
823
+ if (!/^[A-Za-z0-9.-]+$/.test(options.owner) || !/^[A-Za-z0-9._-]+$/.test(options.repo)) {
824
+ throw new tierserror(`invalid repository ${options.owner}/${options.repo}`);
825
+ }
826
+ return {
827
+ owner: options.owner,
828
+ repo: options.repo,
829
+ token: options.token ?? process.env.SADDLE_GITHUB_TOKEN ?? null,
830
+ };
831
+ });
832
+ this.owner = parsed.owner;
833
+ this.repo = parsed.repo;
834
+ this.#token = parsed.token;
835
+ }
836
+ /** True when no token is configured and every call plans instead of IO. */
837
+ get planner() {
838
+ return this.#token === null;
839
+ }
840
+ /**
841
+ * uploadartifact packs the buffer into a zip envelope (one entry per
842
+ * artifact name) and POSTs it to the actions artifacts surface with a
843
+ * correct multipart/form-data boundary; planner mode returns the exact
844
+ * call plus the recorded target.
845
+ */
846
+ async uploadartifact(name, buffer, artifactid) {
847
+ return guard('github', `uploadartifact(${name})`, async () => {
848
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
849
+ throw new tierserror(`invalid artifact name "${name}"`);
850
+ }
851
+ const zip = zipbuild([{ name: `${name}.bin`, data: buffer }]);
852
+ const target = `/repos/${this.owner}/${this.repo}/actions/artifacts/${artifactid ?? '<artifact-id>'}`;
853
+ const boundary = 'saddletiersartifactboundary';
854
+ const multipart = Buffer.concat([
855
+ Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="artifact"; filename="${name}.zip"\r\nContent-Type: application/zip\r\n\r\n`),
856
+ zip,
857
+ Buffer.from(`\r\n--${boundary}--\r\n`),
858
+ ]);
859
+ const headers = this.#headers({
860
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
861
+ 'Content-Length': String(multipart.byteLength),
862
+ });
863
+ const url = `https://api.github.com${target}`;
864
+ if (this.#token === null) {
865
+ this.#planned.set(name, { bytes: buffer.byteLength, url: target });
866
+ return {
867
+ planned: true,
868
+ reason: 'SADDLE_GITHUB_TOKEN is not set: the backend stays in planner mode',
869
+ method: 'POST',
870
+ url,
871
+ headers,
872
+ bytes: buffer.byteLength,
873
+ };
874
+ }
875
+ const response = await fetch(url, { method: 'POST', headers, body: multipart });
876
+ return {
877
+ planned: false,
878
+ status: response.status,
879
+ location: response.headers.get('Location'),
880
+ };
881
+ });
882
+ }
883
+ /**
884
+ * downloadartifact follows the documented zip redirect (303 to the
885
+ * signed URL) and unpacks the archive with the local zip codec.
886
+ */
887
+ async downloadartifact(artifactid) {
888
+ return guard('github', `downloadartifact(${artifactid})`, async () => {
889
+ const url = `https://api.github.com/repos/${this.owner}/${this.repo}/actions/artifacts/${artifactid}/zip`;
890
+ if (this.#token === null) {
891
+ return {
892
+ planned: true,
893
+ reason: 'SADDLE_GITHUB_TOKEN is not set: the backend stays in planner mode',
894
+ method: 'GET',
895
+ url,
896
+ headers: this.#headers(),
897
+ bytes: 0,
898
+ };
899
+ }
900
+ const response = await fetch(url, { headers: this.#headers() });
901
+ if (!response.ok) {
902
+ throw new tierserror(`artifact download failed with HTTP ${response.status}`);
903
+ }
904
+ return zipread(Buffer.from(await response.arrayBuffer()));
905
+ });
906
+ }
907
+ /**
908
+ * uploadreleaseasset streams one buffer to uploads.github.com; the
909
+ * 2 GiB per-file limit is enforced locally before the wire.
910
+ */
911
+ async uploadreleaseasset(releaseid, name, buffer) {
912
+ return guard('github', `uploadreleaseasset(${name})`, async () => {
913
+ if (buffer.byteLength > 2 * 1024 * 1024 * 1024) {
914
+ throw new tierserror(`release asset of ${buffer.byteLength} bytes exceeds the 2 GiB per-file limit`);
915
+ }
916
+ const url = `https://uploads.github.com/repos/${this.owner}/${this.repo}/releases/${releaseid}/assets` +
917
+ `?name=${encodeURIComponent(name)}`;
918
+ const headers = this.#headers({
919
+ 'Content-Type': 'application/octet-stream',
920
+ 'Content-Length': String(buffer.byteLength),
921
+ });
922
+ if (this.#token === null) {
923
+ return {
924
+ planned: true,
925
+ reason: 'SADDLE_GITHUB_TOKEN is not set: the backend stays in planner mode',
926
+ method: 'POST',
927
+ url,
928
+ headers,
929
+ bytes: buffer.byteLength,
930
+ };
931
+ }
932
+ const response = await fetch(url, { method: 'POST', headers, body: buffer });
933
+ return {
934
+ planned: false,
935
+ status: response.status,
936
+ location: response.headers.get('Location'),
937
+ };
938
+ });
939
+ }
940
+ /**
941
+ * getblob reads one repository file through the contents API and hands
942
+ * back the decoded bytes plus the sha and ETag a subsequent PUT needs.
943
+ */
944
+ async getblob(path) {
945
+ return guard('github', `getblob(${path})`, async () => {
946
+ const url = `${this.#contentsurl(path)}`;
947
+ const response = await fetch(url, { headers: this.#headers() });
948
+ if (response.status === 404) {
949
+ return null;
950
+ }
951
+ if (!response.ok) {
952
+ throw new tierserror(`blob read failed with HTTP ${response.status}`);
953
+ }
954
+ const payload = (await response.json());
955
+ if (payload.content === undefined || payload.sha === undefined) {
956
+ throw new tierserror(`blob ${path} returned no content`);
957
+ }
958
+ return {
959
+ content: Buffer.from(payload.content.replace(/\n/g, ''), 'base64'),
960
+ sha: payload.sha,
961
+ etag: response.headers.get('ETag'),
962
+ };
963
+ });
964
+ }
965
+ /**
966
+ * putblob writes one repository file: the sha of the current revision
967
+ * travels in the body and the ETag (when known) rides as the If-Match
968
+ * header, the honest optimistic-concurrency sync behind the VDR page
969
+ * table on GitHub.
970
+ */
971
+ async putblob(path, content, options) {
972
+ return guard('github', `putblob(${path})`, async () => {
973
+ const url = this.#contentsurl(path);
974
+ const headers = this.#headers({
975
+ 'Content-Type': 'application/json',
976
+ ...(options?.etag === undefined ? {} : { 'If-Match': options.etag }),
977
+ });
978
+ const body = JSON.stringify({
979
+ message: options?.message ?? `tiers: sync ${path}`,
980
+ content: content.toString('base64'),
981
+ ...(options?.sha === undefined ? {} : { sha: options.sha }),
982
+ });
983
+ if (this.#token === null) {
984
+ return {
985
+ planned: true,
986
+ reason: 'SADDLE_GITHUB_TOKEN is not set: the backend stays in planner mode',
987
+ method: 'PUT',
988
+ url,
989
+ headers,
990
+ bytes: content.byteLength,
991
+ };
992
+ }
993
+ const response = await fetch(url, { method: 'PUT', headers, body });
994
+ return {
995
+ planned: false,
996
+ status: response.status,
997
+ location: response.headers.get('Location'),
998
+ };
999
+ });
1000
+ }
1001
+ /** Lists the calls planned so far in planner mode. */
1002
+ planneduploads() {
1003
+ return [...this.#planned.entries()].map(([key, plan]) => ({ key, ...plan }));
1004
+ }
1005
+ /** storagebackend surface: reads blobs under tiers/ (live mode only). */
1006
+ async get(key) {
1007
+ return guard('github', `githubstorage.get(${key})`, async () => {
1008
+ if (this.#token === null) {
1009
+ return null;
1010
+ }
1011
+ const blob = await this.getblob(this.#blobpath(key));
1012
+ return blob === null ? null : blob.content;
1013
+ });
1014
+ }
1015
+ /** storagebackend surface: writes blobs with sha conflict handling. */
1016
+ async set(key, buffer) {
1017
+ return guard('github', `githubstorage.set(${key})`, async () => {
1018
+ const path = this.#blobpath(key);
1019
+ if (this.#token === null) {
1020
+ this.#planned.set(key, {
1021
+ bytes: buffer.byteLength,
1022
+ url: `/repos/${this.owner}/${this.repo}/contents/${path}`,
1023
+ });
1024
+ return;
1025
+ }
1026
+ const existing = await this.getblob(path);
1027
+ const result = await this.putblob(path, buffer, {
1028
+ sha: existing?.sha,
1029
+ etag: existing?.etag ?? undefined,
1030
+ });
1031
+ if (!result.planned && result.status >= 300 && result.status !== 200) {
1032
+ throw new tierserror(`github blob write failed with HTTP ${result.status}`);
1033
+ }
1034
+ });
1035
+ }
1036
+ /** storagebackend surface: deletes blobs through the contents API. */
1037
+ async delete(key) {
1038
+ return guard('github', `githubstorage.delete(${key})`, async () => {
1039
+ if (this.#token === null) {
1040
+ return false;
1041
+ }
1042
+ const path = this.#blobpath(key);
1043
+ const existing = await this.getblob(path);
1044
+ if (existing === null) {
1045
+ return false;
1046
+ }
1047
+ const response = await fetch(this.#contentsurl(path), {
1048
+ method: 'DELETE',
1049
+ headers: this.#headers({ 'Content-Type': 'application/json' }),
1050
+ body: JSON.stringify({ message: `tiers: release ${path}`, sha: existing.sha }),
1051
+ });
1052
+ return response.ok;
1053
+ });
1054
+ }
1055
+ #contentsurl(path) {
1056
+ return `https://api.github.com/repos/${this.owner}/${this.repo}/contents/${path
1057
+ .split('/')
1058
+ .map(encodeURIComponent)
1059
+ .join('/')}`;
1060
+ }
1061
+ #blobpath(key) {
1062
+ return `tiers/${encodeURIComponent(key)}.bin`;
1063
+ }
1064
+ #headers(extra) {
1065
+ const headers = {
1066
+ Accept: 'application/vnd.github+json',
1067
+ 'X-GitHub-Api-Version': this.#apiversion,
1068
+ 'User-Agent': 'saddle-tiers',
1069
+ ...extra,
1070
+ };
1071
+ if (this.#token !== null) {
1072
+ headers.Authorization = `Bearer ${this.#token}`;
1073
+ }
1074
+ return headers;
1075
+ }
1076
+ }
1077
+ /* ------------------------------------------------------------------ */
1078
+ /* Section 12: VDR addressing, memoryblock and wire headers */
1079
+ /* ------------------------------------------------------------------ */
1080
+ /** Highest address of the VDR space: 2^64-1. */
1081
+ export const MAXVDRADDRESS = 0xffffffffffffffffn;
1082
+ /** The address space in the signed reading of the VDR notes (~9.22 EB). */
1083
+ export const VDRADDRESSEB = 9.22;
1084
+ /** Content type of a VDR memory block on the wire. */
1085
+ export const VDRBLOCKCONTENTTYPE = 'application/vnd.vdr-block+bin';
1086
+ /** formatvdraddress renders 0x-prefixed zero padded hex, the canonical spelling. */
1087
+ export function formatvdraddress(address) {
1088
+ validatevdraddress(address);
1089
+ return `0x${address.toString(16).padStart(16, '0')}`;
1090
+ }
1091
+ /** parsevdraddress accepts 0x hex or decimal text and validates the range. */
1092
+ export function parsevdraddress(text) {
1093
+ return guard('vdr', `parsevdraddress(${text})`, () => {
1094
+ const trimmed = text.trim().toLowerCase();
1095
+ const value = BigInt(trimmed);
1096
+ validatevdraddress(value);
1097
+ return value;
1098
+ });
1099
+ }
1100
+ /** validatevdraddress asserts a non-negative 64-bit BigInt. */
1101
+ export function validatevdraddress(address) {
1102
+ if (typeof address !== 'bigint' || address < 0n || address > MAXVDRADDRESS) {
1103
+ throw new tierserror(`vdr address ${String(address)} outside 0x0..0xffffffffffffffff (~${VDRADDRESSEB} EB)`);
1104
+ }
1105
+ }
1106
+ /**
1107
+ * vrdblockheaders generates the real header set of the VDR protocol:
1108
+ * Content-Type application/vnd.vdr-block+bin plus X-VDR-Chunk-ID,
1109
+ * X-VDR-Pointer-Offset, X-VDR-Compression and X-VDR-Integrity (sha256 of
1110
+ * the payload bytes, computed here when a buffer is given or quoted from
1111
+ * the stored checksum otherwise). The zstd value is accepted on the wire
1112
+ * while this runtime compresses with gzip from node:zlib.
1113
+ */
1114
+ export function vrdblockheaders(values) {
1115
+ return guard('vdr', 'vrdblockheaders', () => {
1116
+ const checksum = values.buffer !== undefined ? sha256hex(values.buffer) : values.checksum;
1117
+ if (checksum === undefined) {
1118
+ throw new tierserror('vrdblockheaders needs a buffer or a stored checksum');
1119
+ }
1120
+ return {
1121
+ 'Content-Type': VDRBLOCKCONTENTTYPE,
1122
+ 'X-VDR-Chunk-ID': values.chunkid,
1123
+ 'X-VDR-Pointer-Offset': values.pointeroffset.toString(),
1124
+ 'X-VDR-Compression': values.compression,
1125
+ 'X-VDR-Integrity': checksum,
1126
+ };
1127
+ });
1128
+ }
1129
+ function blocktojson(block) {
1130
+ return JSON.stringify({
1131
+ address: formatvdraddress(block.address),
1132
+ bytelength: block.bytelength,
1133
+ ...(block.remoteurl === undefined ? {} : { remoteurl: block.remoteurl }),
1134
+ checksum: block.checksum,
1135
+ iscompressed: block.iscompressed,
1136
+ });
1137
+ }
1138
+ function blockfromjson(text) {
1139
+ const raw = JSON.parse(text);
1140
+ return {
1141
+ address: parsevdraddress(raw.address),
1142
+ bytelength: raw.bytelength,
1143
+ ...(raw.remoteurl === undefined ? {} : { remoteurl: raw.remoteurl }),
1144
+ checksum: raw.checksum,
1145
+ iscompressed: raw.iscompressed,
1146
+ };
1147
+ }
1148
+ /**
1149
+ * localpagetable keeps blocks in a Map and (when a kv handle is given)
1150
+ * persists every record into sqlitekv under the vdr:page: prefix, so the
1151
+ * address space survives restarts exactly like the Upstash page table of
1152
+ * the design, minus the network.
1153
+ */
1154
+ export class localpagetable {
1155
+ backend = 'local map + sqlitekv persistence';
1156
+ #blocks = new Map();
1157
+ #kv;
1158
+ constructor(options) {
1159
+ this.#kv = options?.kv ?? null;
1160
+ const kv = this.#kv;
1161
+ guard('pagetable', 'localpagetable constructor', () => {
1162
+ if (kv === null) {
1163
+ return;
1164
+ }
1165
+ for (const key of kv.keys('vdr:page:')) {
1166
+ const stored = kv.get(key);
1167
+ if (stored !== null) {
1168
+ this.#blocks.set(key, blockfromjson(stored.toString('utf8')));
1169
+ }
1170
+ }
1171
+ });
1172
+ }
1173
+ getblock(address) {
1174
+ return this.#blocks.get(`vdr:page:${formatvdraddress(address)}`) ?? null;
1175
+ }
1176
+ setblock(block) {
1177
+ guard('pagetable', 'localpagetable.setblock', () => {
1178
+ const key = `vdr:page:${formatvdraddress(block.address)}`;
1179
+ this.#blocks.set(key, block);
1180
+ this.#kv?.set(key, Buffer.from(blocktojson(block), 'utf8'));
1181
+ });
1182
+ }
1183
+ deleteblock(address) {
1184
+ return guard('pagetable', 'localpagetable.deleteblock', () => {
1185
+ const key = `vdr:page:${formatvdraddress(address)}`;
1186
+ const removed = this.#blocks.delete(key);
1187
+ this.#kv?.delete(key);
1188
+ return removed;
1189
+ });
1190
+ }
1191
+ blocks() {
1192
+ return [...this.#blocks.values()];
1193
+ }
1194
+ size() {
1195
+ return this.#blocks.size;
1196
+ }
1197
+ }
1198
+ /**
1199
+ * upstashplanner documents the remote page table of the VDR design as a
1200
+ * plan object: the free Upstash Redis REST tier (10k requests/day) with
1201
+ * the real GET/SET/DEL commands for the vdr:page: keyspace. No IO is
1202
+ * performed; the caller executes the plan when an account exists.
1203
+ */
1204
+ export function upstashplanner(options) {
1205
+ return guard('pagetable', 'upstashplanner', () => {
1206
+ const keyname = options?.keyname ?? 'vdr:page:0x0000000000000010';
1207
+ return {
1208
+ resturl: '$UPSTASH_REDIS_REST_URL',
1209
+ commands: [
1210
+ `curl -fsSL -H "Authorization: Bearer $UPSTASH_REDIS_REST_TOKEN" "$UPSTASH_REDIS_REST_URL/get/${keyname}"`,
1211
+ `curl -fsSL -H "Authorization: Bearer $UPSTASH_REDIS_REST_TOKEN" -X POST "$UPSTASH_REDIS_REST_URL/set" -H "Content-Type: application/json" -d '{"key":"${keyname}","value":"{\\"address\\":\\"0x0000000000000010\\"}"}'`,
1212
+ `curl -fsSL -H "Authorization: Bearer $UPSTASH_REDIS_REST_TOKEN" "$UPSTASH_REDIS_REST_URL/del/${keyname}"`,
1213
+ ],
1214
+ envvars: ['UPSTASH_REDIS_REST_URL', 'UPSTASH_REDIS_REST_TOKEN'],
1215
+ quota: '10,000 requests/day on the free tier (saddle docs readme1.md line 519)',
1216
+ source: 'saddle docs readme1.md lines 908 (page table: Upstash Redis) and 519 (quota)',
1217
+ };
1218
+ });
1219
+ }
1220
+ /**
1221
+ * vrdringbuffer is the L1 VDRAM: one SharedArrayBuffer with a Float32Array
1222
+ * view, 4-byte aligned slots and the auto-flush ceiling of the design
1223
+ * (512 MB default, SADDLE_VDR_CAP in MB overrides). When a write no longer
1224
+ * fits, every live block is flushed through the onflush callback (the
1225
+ * transparent demotion to L3) and the cursor restarts at zero: ring
1226
+ * paging at the RAM ceiling, exactly like the VDR notes describe.
1227
+ */
1228
+ export class vrdringbuffer {
1229
+ #shared;
1230
+ #bytes;
1231
+ #floats;
1232
+ #capbytes;
1233
+ #cursor = 0;
1234
+ #slots = new Map();
1235
+ #onflush;
1236
+ #flushcount = 0;
1237
+ #demotedbytes = 0;
1238
+ constructor(options) {
1239
+ const built = guard('ring', 'vrdringbuffer constructor', () => {
1240
+ const envcap = Number.parseInt(process.env.SADDLE_VDR_CAP ?? '', 10);
1241
+ const capmb = options?.capmb ?? (Number.isFinite(envcap) ? envcap : 512);
1242
+ const capbytes = options?.capbytes ?? Math.max(64 * 1024, Math.floor(capmb * 1024 * 1024));
1243
+ if (!Number.isFinite(capbytes) || capbytes < 64 * 1024) {
1244
+ throw new tierserror('ring capacity must be at least 64 KiB');
1245
+ }
1246
+ const shared = new SharedArrayBuffer(capbytes);
1247
+ return {
1248
+ shared,
1249
+ bytes: new Uint8Array(shared),
1250
+ floats: new Float32Array(shared),
1251
+ capbytes,
1252
+ };
1253
+ });
1254
+ this.#shared = built.shared;
1255
+ this.#bytes = built.bytes;
1256
+ this.#floats = built.floats;
1257
+ this.#capbytes = built.capbytes;
1258
+ this.#onflush = options?.onflush ?? null;
1259
+ }
1260
+ /** Ceiling in bytes. */
1261
+ get capbytes() {
1262
+ return this.#capbytes;
1263
+ }
1264
+ /** Bytes currently live in the ring. */
1265
+ get usedbytes() {
1266
+ return this.#cursor;
1267
+ }
1268
+ /** Number of full flushes executed at the ceiling. */
1269
+ get flushcount() {
1270
+ return this.#flushcount;
1271
+ }
1272
+ /** Bytes handed to the demotion callback so far. */
1273
+ get demotedbytes() {
1274
+ return this.#demotedbytes;
1275
+ }
1276
+ /**
1277
+ * writefloats stores one Float32Array payload at a 4-byte aligned slot
1278
+ * and reports whether the ceiling forced a full flush first. Payloads
1279
+ * larger than the whole ring are rejected (the engine demotes those
1280
+ * straight to L3 without paging them).
1281
+ */
1282
+ writefloats(address, samples) {
1283
+ return guard('ring', 'vrdringbuffer.writefloats', () => {
1284
+ validatevdraddress(address);
1285
+ if (samples.byteLength === 0 || samples.byteLength % 4 !== 0) {
1286
+ throw new tierserror('float payload length must be a positive multiple of 4 bytes');
1287
+ }
1288
+ if (samples.byteLength > this.#capbytes) {
1289
+ throw new tierserror(`payload of ${samples.byteLength} bytes exceeds the ring ceiling of ${this.#capbytes} bytes`);
1290
+ }
1291
+ const flushed = this.#ensurecapacity(samples.byteLength);
1292
+ const offset = this.#align4(this.#cursor);
1293
+ this.#floats.set(samples, offset / 4);
1294
+ this.#slots.set(address, { offset, bytelength: samples.byteLength });
1295
+ this.#cursor = offset + samples.byteLength;
1296
+ return { flushed };
1297
+ });
1298
+ }
1299
+ /** writebytes stores an opaque byte payload (any alignment). */
1300
+ writebytes(address, payload) {
1301
+ return guard('ring', 'vrdringbuffer.writebytes', () => {
1302
+ validatevdraddress(address);
1303
+ if (payload.byteLength === 0 || payload.byteLength > this.#capbytes) {
1304
+ throw new tierserror(`payload of ${payload.byteLength} bytes is empty or exceeds the ring ceiling`);
1305
+ }
1306
+ const flushed = this.#ensurecapacity(payload.byteLength);
1307
+ const offset = this.#align4(this.#cursor);
1308
+ this.#bytes.set(payload, offset);
1309
+ this.#slots.set(address, { offset, bytelength: payload.byteLength });
1310
+ this.#cursor = offset + payload.byteLength;
1311
+ return { flushed };
1312
+ });
1313
+ }
1314
+ /** readfloats returns a private copy of one live float payload. */
1315
+ readfloats(address) {
1316
+ return guard('ring', 'vrdringbuffer.readfloats', () => {
1317
+ const slot = this.#slots.get(address);
1318
+ return slot === undefined
1319
+ ? null
1320
+ : new Float32Array(this.#shared.slice(slot.offset, slot.offset + slot.bytelength));
1321
+ });
1322
+ }
1323
+ /** readbytes returns a private copy of one live byte payload. */
1324
+ readbytes(address) {
1325
+ return guard('ring', 'vrdringbuffer.readbytes', () => {
1326
+ const slot = this.#slots.get(address);
1327
+ return slot === undefined
1328
+ ? null
1329
+ : this.#bytes.slice(slot.offset, slot.offset + slot.bytelength);
1330
+ });
1331
+ }
1332
+ /** drop removes one address from the ring without demoting it. */
1333
+ drop(address) {
1334
+ return this.#slots.delete(address);
1335
+ }
1336
+ /** Addresses currently live, oldest first. */
1337
+ addresses() {
1338
+ return [...this.#slots.keys()];
1339
+ }
1340
+ /** flush demotes every live block now and resets the cursor. */
1341
+ flush() {
1342
+ return guard('ring', 'vrdringbuffer.flush', () => {
1343
+ for (const [address, slot] of this.#slots) {
1344
+ this.#onflush?.(address, this.#bytes.slice(slot.offset, slot.offset + slot.bytelength));
1345
+ }
1346
+ this.#demotedbytes += this.#cursor;
1347
+ this.#slots.clear();
1348
+ this.#cursor = 0;
1349
+ this.#flushcount += 1;
1350
+ return this.#flushcount;
1351
+ });
1352
+ }
1353
+ /** Live statistics of the ring. */
1354
+ stats() {
1355
+ return {
1356
+ capbytes: this.#capbytes,
1357
+ usedbytes: this.#cursor,
1358
+ liveblocks: this.#slots.size,
1359
+ flushcount: this.#flushcount,
1360
+ demotedbytes: this.#demotedbytes,
1361
+ };
1362
+ }
1363
+ #ensurecapacity(bytelength) {
1364
+ const start = this.#align4(this.#cursor);
1365
+ if (start + bytelength <= this.#capbytes) {
1366
+ return false;
1367
+ }
1368
+ this.flush();
1369
+ return true;
1370
+ }
1371
+ #align4(value) {
1372
+ return value + ((4 - (value % 4)) % 4);
1373
+ }
1374
+ }
1375
+ /**
1376
+ * universalvdrengine is the L1-to-L3 pipeline of the VDR design:
1377
+ * writevdr lands Float32Array blocks in the SharedArrayBuffer ring (L1,
1378
+ * microsecond latency) and registers them in the page table; when the
1379
+ * ring hits its ceiling the flush callback queues every live block for
1380
+ * demotion into the L3 backend (gzip when the block is compressed).
1381
+ * readvdr answers from the ring first and, on a miss, drains the pending
1382
+ * demotions, fetches the block from L3, verifies the sha256 checksum of
1383
+ * the design and hands back the floats: the demotion is transparent to
1384
+ * the caller, which is the entire "everything is VRAM" thesis.
1385
+ */
1386
+ export class universalvdrengine {
1387
+ #ring;
1388
+ #pages;
1389
+ #l3;
1390
+ #pending = [];
1391
+ #demotedblocks = 0;
1392
+ constructor(options) {
1393
+ this.#pages = options?.pages ?? new localpagetable();
1394
+ this.#l3 = options?.l3 ?? null;
1395
+ const pages = this.#pages;
1396
+ const pending = this.#pending;
1397
+ this.#ring = guard('vdr', 'universalvdrengine constructor', () => new vrdringbuffer({
1398
+ capmb: options?.capmb,
1399
+ onflush: (address, bytes) => {
1400
+ const block = pages.getblock(address);
1401
+ pending.push({
1402
+ key: `vdr:${formatvdraddress(address)}`,
1403
+ bytes: block?.iscompressed === true ? gzipSync(Buffer.from(bytes)) : Buffer.from(bytes),
1404
+ });
1405
+ },
1406
+ }));
1407
+ }
1408
+ /** Writes one float block at a VDR address and returns its record. */
1409
+ writevdr(address, samples, options) {
1410
+ return guard('vdr', `writevdr(${address})`, () => {
1411
+ validatevdraddress(address);
1412
+ const bytes = Buffer.from(new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength));
1413
+ const block = {
1414
+ address,
1415
+ bytelength: bytes.byteLength,
1416
+ checksum: sha256hex(bytes),
1417
+ iscompressed: options?.compress ?? false,
1418
+ ...(this.#l3 === null ? {} : { remoteurl: `tiers://l3/vdr:${formatvdraddress(address)}` }),
1419
+ };
1420
+ if (bytes.byteLength > this.#ring.capbytes) {
1421
+ this.#pending.push({
1422
+ key: `vdr:${formatvdraddress(address)}`,
1423
+ bytes: options?.compress === true ? gzipSync(bytes) : bytes,
1424
+ });
1425
+ this.#demotedblocks += 1;
1426
+ }
1427
+ else {
1428
+ this.#ring.writebytes(address, bytes);
1429
+ }
1430
+ this.#pages.setblock(block);
1431
+ return block;
1432
+ });
1433
+ }
1434
+ /**
1435
+ * Reads one address back as floats: the ring answers first; on a miss
1436
+ * the pending demotions drain into L3, the block returns from storage,
1437
+ * the checksum is verified and the floats are rebuilt. Returns null
1438
+ * when the address never existed.
1439
+ */
1440
+ async readvdr(address) {
1441
+ return guard('vdr', `readvdr(${address})`, async () => {
1442
+ validatevdraddress(address);
1443
+ const live = this.#ring.readbytes(address);
1444
+ if (live !== null) {
1445
+ return this.#tofloats(Buffer.from(live));
1446
+ }
1447
+ const block = this.#pages.getblock(address);
1448
+ if (block === null || this.#l3 === null) {
1449
+ return null;
1450
+ }
1451
+ await this.#drain();
1452
+ const stored = await this.#l3.get(`vdr:${formatvdraddress(address)}`);
1453
+ if (stored === null) {
1454
+ return null;
1455
+ }
1456
+ const plain = block.iscompressed ? gunzipSync(stored) : stored;
1457
+ if (sha256hex(plain) !== block.checksum) {
1458
+ throw new tierserror(`checksum mismatch at ${formatvdraddress(address)}: block integrity violated`);
1459
+ }
1460
+ return this.#tofloats(plain);
1461
+ });
1462
+ }
1463
+ /**
1464
+ * freeramaddress releases one address from every tier: the ring slot,
1465
+ * the page table record and the L3 block. Returns true when anything
1466
+ * was actually freed.
1467
+ */
1468
+ async freeramaddress(address) {
1469
+ return guard('vdr', `freeramaddress(${address})`, async () => {
1470
+ validatevdraddress(address);
1471
+ const dropped = this.#ring.drop(address);
1472
+ const untracked = this.#pages.deleteblock(address);
1473
+ const removed = this.#l3 === null ? false : await this.#l3.delete(`vdr:${formatvdraddress(address)}`);
1474
+ return dropped || untracked || removed;
1475
+ });
1476
+ }
1477
+ /**
1478
+ * blockheaders renders the real wire headers of one address: integrity
1479
+ * comes from the live bytes when the block sits in the ring and from
1480
+ * the stored checksum after demotion.
1481
+ */
1482
+ blockheaders(address) {
1483
+ return guard('vdr', 'blockheaders', () => {
1484
+ validatevdraddress(address);
1485
+ const block = this.#pages.getblock(address);
1486
+ if (block === null) {
1487
+ throw new tierserror(`no block mapped at ${formatvdraddress(address)}`);
1488
+ }
1489
+ const live = this.#ring.readbytes(address);
1490
+ return vrdblockheaders({
1491
+ chunkid: `vdr-${formatvdraddress(address)}`,
1492
+ pointeroffset: 0,
1493
+ compression: block.iscompressed ? 'gzip' : 'none',
1494
+ ...(live === null ? { checksum: block.checksum } : { buffer: live }),
1495
+ });
1496
+ });
1497
+ }
1498
+ /** Page table accessor. */
1499
+ get pages() {
1500
+ return this.#pages;
1501
+ }
1502
+ /** Ring statistics plus demotion counters. */
1503
+ stats() {
1504
+ const ring = this.#ring.stats();
1505
+ return {
1506
+ capbytes: ring.capbytes,
1507
+ usedbytes: ring.usedbytes,
1508
+ liveblocks: ring.liveblocks,
1509
+ pagetablesize: this.#pages.size(),
1510
+ pendingdemes: this.#pending.length,
1511
+ demotedblocks: this.#demotedblocks,
1512
+ demotedbytes: ring.demotedbytes,
1513
+ };
1514
+ }
1515
+ /** Drops the ring without touching L3 (dispose path). */
1516
+ [Symbol.dispose]() {
1517
+ this.#pending = [];
1518
+ this.#ring.flush();
1519
+ }
1520
+ async #drain() {
1521
+ if (this.#l3 === null) {
1522
+ this.#pending = [];
1523
+ return;
1524
+ }
1525
+ for (const item of this.#pending) {
1526
+ await this.#l3.set(item.key, item.bytes);
1527
+ this.#demotedblocks += 1;
1528
+ }
1529
+ this.#pending = [];
1530
+ }
1531
+ #tofloats(data) {
1532
+ if (data.byteLength % 4 !== 0) {
1533
+ throw new tierserror(`block of ${data.byteLength} bytes is not float32 aligned`);
1534
+ }
1535
+ const view = new Float32Array(data.byteLength / 4);
1536
+ view.set(new Float32Array(data.buffer, data.byteOffset, view.length));
1537
+ return view;
1538
+ }
1539
+ }
1540
+ /** Recipe sources of the bridge section. */
1541
+ export const RECIPESOURCE = 'saddle docs readme1.md lines 398-421 (storage to RAM bridge mechanics)';
1542
+ /**
1543
+ * zramrecipe plans the compressed RAM swap sequence: modprobe, zstd (or
1544
+ * lz4) algorithm, disksize, mkswap, swapon with priority 100. Printable
1545
+ * strings only: this module never executes syscalls.
1546
+ */
1547
+ export function zramrecipe(sizegb = 8, algo = 'zstd') {
1548
+ return guard('recipes', 'zramrecipe', () => {
1549
+ if (!Number.isFinite(sizegb) || sizegb < 1 || sizegb > 512) {
1550
+ throw new tierserror(`sizegb must be between 1 and 512, received ${sizegb}`);
1551
+ }
1552
+ return {
1553
+ kind: 'zram',
1554
+ commands: [
1555
+ 'modprobe zram num_devices=2',
1556
+ `echo ${algo} > /sys/block/zram0/comp_algorithm`,
1557
+ `echo ${sizegb}G > /sys/block/zram0/disksize`,
1558
+ 'mkswap /dev/zram0',
1559
+ 'swapon --priority 100 /dev/zram0',
1560
+ 'zramctl # verify: 2-3x amplification at zstd',
1561
+ ],
1562
+ effect: `${sizegb} GB compressed swap in RAM (~${sizegb * 2}-${sizegb * 3} GB effective at 2:1-3:1)`,
1563
+ source: RECIPESOURCE,
1564
+ };
1565
+ });
1566
+ }
1567
+ /** tmpfsrecipe plans a ramdisk mount sized in gigabytes. */
1568
+ export function tmpfsrecipe(mount = '/mnt/ramdisk', sizegb = 8) {
1569
+ return guard('recipes', 'tmpfsrecipe', () => {
1570
+ if (!mount.startsWith('/')) {
1571
+ throw new tierserror(`mount point must be absolute, received "${mount}"`);
1572
+ }
1573
+ if (!Number.isFinite(sizegb) || sizegb < 1) {
1574
+ throw new tierserror(`sizegb must be at least 1, received ${sizegb}`);
1575
+ }
1576
+ return {
1577
+ kind: 'tmpfs',
1578
+ commands: [`mkdir -p ${mount}`, `mount -t tmpfs -o size=${sizegb}G,mode=1777 tmpfs ${mount}`],
1579
+ effect: `${sizegb} GB ramdisk (~10x faster than SSD; /dev/shm already provides half of RAM)`,
1580
+ source: RECIPESOURCE,
1581
+ };
1582
+ });
1583
+ }
1584
+ /** swapfilerecipe plans the fallocate/mkswap/swapon swap file sequence. */
1585
+ export function swapfilerecipe(path = '/mnt/swapfile', sizegb = 16) {
1586
+ return guard('recipes', 'swapfilerecipe', () => {
1587
+ if (!path.startsWith('/')) {
1588
+ throw new tierserror(`swap path must be absolute, received "${path}"`);
1589
+ }
1590
+ if (!Number.isFinite(sizegb) || sizegb < 1) {
1591
+ throw new tierserror(`sizegb must be at least 1, received ${sizegb}`);
1592
+ }
1593
+ return {
1594
+ kind: 'swapfile',
1595
+ commands: [
1596
+ `fallocate -l ${sizegb}G ${path}`,
1597
+ `chmod 600 ${path}`,
1598
+ `mkswap ${path}`,
1599
+ `swapon ${path}`,
1600
+ ],
1601
+ effect: `${sizegb} GB of virtual RAM from disk, the plainest storage-to-RAM bridge`,
1602
+ source: RECIPESOURCE,
1603
+ };
1604
+ });
1605
+ }
1606
+ /* ------------------------------------------------------------------ */
1607
+ /* Section 18: storagerambridgeplanner (mmap plan objects) */
1608
+ /* ------------------------------------------------------------------ */
1609
+ /** The mmap constants of the StorageRAMBridge API. */
1610
+ export const MMAPCONSTANTS = {
1611
+ PROT_READ: 0x1,
1612
+ PROT_WRITE: 0x2,
1613
+ MAP_SHARED: 0x01,
1614
+ MADV_RANDOM: 1,
1615
+ };
1616
+ /**
1617
+ * storagerambridgeplanner renders the StorageRAMBridge as plan objects:
1618
+ * seed 1 MB with fs.writeFileSync(storagePath, Buffer.alloc(1024*1024)),
1619
+ * map with PROT_READ|PROT_WRITE and MAP_SHARED, advise MADV_RANDOM, then
1620
+ * read/write by offset, sync with msync, grow with fs.ftruncateSync(fd,
1621
+ * newSize) followed by a re-map, and close. Every step is an argv-style
1622
+ * record: the module plans, the runner executes.
1623
+ */
1624
+ export function storagerambridgeplanner(storagepath, options) {
1625
+ return guard('recipes', 'storagerambridgeplanner', () => {
1626
+ if (!storagepath.startsWith('/')) {
1627
+ throw new tierserror(`storagepath must be absolute, received "${storagepath}"`);
1628
+ }
1629
+ const syncintervalms = options?.syncintervalms ?? 1000;
1630
+ return {
1631
+ storagepath,
1632
+ ramlimitmb: options?.ramlimitmb ?? 512,
1633
+ syncintervalms,
1634
+ usesharedmemory: options?.usesharedmemory ?? false,
1635
+ constants: MMAPCONSTANTS,
1636
+ seedbytes: 1024 * 1024,
1637
+ steps: [
1638
+ {
1639
+ operation: 'fs.writeFileSync',
1640
+ args: [storagepath, 'Buffer.alloc(1024 * 1024)'],
1641
+ effect: 'seed a 1 MB file so the first map has a page range',
1642
+ },
1643
+ {
1644
+ operation: 'fs.openSync',
1645
+ args: [storagepath, "'r+'"],
1646
+ effect: 'open the descriptor that backs the mapping',
1647
+ },
1648
+ {
1649
+ operation: 'mmap',
1650
+ args: ['null', 'length', 'PROT_READ | PROT_WRITE', 'MAP_SHARED', 'fd', '0'],
1651
+ effect: 'map the file into virtual memory: storage and RAM at once',
1652
+ },
1653
+ {
1654
+ operation: 'mmap.advise',
1655
+ args: ['buffer', 'MADV_RANDOM'],
1656
+ effect: 'random access pattern for kv page reads',
1657
+ },
1658
+ {
1659
+ operation: 'read',
1660
+ args: ['offset', 'length'],
1661
+ effect: 'zero-copy read straight out of the mapping',
1662
+ },
1663
+ {
1664
+ operation: 'write',
1665
+ args: ['offset', 'data'],
1666
+ effect: 'writes land in RAM and in the file simultaneously',
1667
+ },
1668
+ {
1669
+ operation: 'sync',
1670
+ args: ['msync', String(syncintervalms)],
1671
+ effect: `flush dirty pages every ${syncintervalms} ms`,
1672
+ },
1673
+ {
1674
+ operation: 'grow',
1675
+ args: ['fs.ftruncateSync(fd, newSize)', 'mmap(null, newSize, ..., MAP_SHARED, fd, 0)'],
1676
+ effect: 'extend the file then re-map: capacity grows without a copy',
1677
+ },
1678
+ {
1679
+ operation: 'close',
1680
+ args: ['munmap', 'fs.closeSync(fd)'],
1681
+ effect: 'unmap and release the descriptor',
1682
+ },
1683
+ ],
1684
+ source: 'saddle docs readme1.md line 426 (StorageRAMBridge API and internals)',
1685
+ };
1686
+ });
1687
+ }
1688
+ /* ------------------------------------------------------------------ */
1689
+ /* Section 19: sysctl drop-in and cgroups v2 slice */
1690
+ /* ------------------------------------------------------------------ */
1691
+ /**
1692
+ * sysctldropin renders the full text of /etc/sysctl.d/99-zai-memory.conf:
1693
+ * swappiness 180 (zram-first), zeroed watermark boost, watermark scale
1694
+ * 125, page-cluster 0 for swap randomness and overcommit_memory 1 so the
1695
+ * address space may exceed the box.
1696
+ */
1697
+ export function sysctldropin() {
1698
+ return [
1699
+ '# /etc/sysctl.d/99-zai-memory.conf',
1700
+ '# the memory tuning drop-in of the storage-to-RAM bridge doctrine',
1701
+ '# (saddle docs readme1.md line 415)',
1702
+ 'vm.swappiness = 180',
1703
+ 'vm.watermark_boost_factor = 0',
1704
+ 'vm.watermark_scale_factor = 125',
1705
+ 'vm.page-cluster = 0',
1706
+ 'vm.overcommit_memory = 1',
1707
+ '',
1708
+ ].join('\n');
1709
+ }
1710
+ /**
1711
+ * cgroupsv2slice renders the control files of one slice: subtree_control
1712
+ * enables memory/cpu/pids/io, memory.max bounds the RAM with memory.high
1713
+ * at 90 percent as the throttle line, swap is off, cpu.max carries the
1714
+ * percent as quota over the 100 ms period, pids.max caps the tasks,
1715
+ * io.max bounds the device bandwidth and memory.oom.group keeps the
1716
+ * blast radius inside the slice.
1717
+ */
1718
+ export function cgroupsv2slice(name, limits = {}) {
1719
+ return guard('recipes', 'cgroupsv2slice', () => {
1720
+ if (!/^[a-z0-9][a-z0-9.-]*$/.test(name)) {
1721
+ throw new tierserror(`invalid slice name "${name}"`);
1722
+ }
1723
+ const rammb = limits.rammb ?? 2048;
1724
+ const cpupercent = limits.cpupercent ?? 400;
1725
+ const pids = limits.pids ?? 256;
1726
+ const iorbps = limits.iorbps ?? 104857600;
1727
+ const dir = `/sys/fs/cgroup/${name}`;
1728
+ return {
1729
+ dir,
1730
+ files: [
1731
+ { path: `${dir}/cgroup.subtree_control`, value: '+memory +cpu +pids +io' },
1732
+ { path: `${dir}/memory.max`, value: `${rammb}M` },
1733
+ { path: `${dir}/memory.high`, value: `${Math.floor(rammb * 0.9)}M` },
1734
+ { path: `${dir}/memory.swap.max`, value: '0' },
1735
+ { path: `${dir}/memory.oom.group`, value: '1' },
1736
+ { path: `${dir}/cpu.max`, value: `${cpupercent * 1000} 100000` },
1737
+ { path: `${dir}/pids.max`, value: `${pids}` },
1738
+ { path: `${dir}/io.max`, value: `8:0 rbps=${iorbps} wbps=${iorbps}` },
1739
+ ],
1740
+ source: 'saddle docs readme1.md line 416 (cgroups v2)',
1741
+ };
1742
+ });
1743
+ }
1744
+ /** The magic byte catalog: true type over file extension. */
1745
+ export const MAGICBYTES = {
1746
+ elf: '7f454c46',
1747
+ pe: '4d5a',
1748
+ png: '89504e47',
1749
+ zip: '504b0304',
1750
+ };
1751
+ /**
1752
+ * sniffmagic reads the true type of a payload from its leading bytes:
1753
+ * ELF 7F 45 4C 46, PE 4D 5A, PNG 89 50 4E 47 and ZIP 50 4B 03 04, the
1754
+ * four signatures of the storage==compute table. The four-byte probes run
1755
+ * before the two-byte PE probe so 'MZ' never masks a longer match.
1756
+ */
1757
+ export function sniffmagic(buffer) {
1758
+ return guard('sniffer', 'sniffmagic', () => {
1759
+ const head = Buffer.from(buffer.subarray(0, 4)).toString('hex').padEnd(8, '0');
1760
+ for (const kind of ['elf', 'png', 'zip']) {
1761
+ if (head.startsWith(MAGICBYTES[kind])) {
1762
+ return { kind, signature: MAGICBYTES[kind], note: `magic match on ${kind}` };
1763
+ }
1764
+ }
1765
+ if (head.startsWith(MAGICBYTES.pe)) {
1766
+ return { kind: 'pe', signature: MAGICBYTES.pe, note: 'magic match on pe' };
1767
+ }
1768
+ return { kind: 'unknown', signature: head.slice(0, 8), note: 'no known magic prefix' };
1769
+ });
1770
+ }
1771
+ /**
1772
+ * virtualfilesystem proves the thesis in code: mountbuffer registers the
1773
+ * same bytes under an inode plus dentry exactly once, and the usage flag
1774
+ * is the only difference between storage and compute. read hands back
1775
+ * the very same Buffer reference (zero copy): the bytes never changed,
1776
+ * only the intent did.
1777
+ */
1778
+ export class virtualfilesystem {
1779
+ #inodes = new Map();
1780
+ #dentries = new Map();
1781
+ #nextino = 2;
1782
+ /** Mounts one buffer as inode+dentry with the requested usage flag. */
1783
+ mountbuffer(path, buffer, usage) {
1784
+ return guard('vfs', `mountbuffer(${path})`, () => {
1785
+ if (!path.startsWith('/') || path.includes('..')) {
1786
+ throw new tierserror(`invalid vfs path "${path}"`);
1787
+ }
1788
+ const inode = {
1789
+ ino: this.#nextino,
1790
+ mode: 0o100644,
1791
+ size: buffer.byteLength,
1792
+ ops: ['read', 'write', 'mmap'],
1793
+ };
1794
+ this.#nextino += 1;
1795
+ this.#inodes.set(inode.ino, { inode, buffer });
1796
+ const parts = path.split('/');
1797
+ const name = parts.pop();
1798
+ const dentry = {
1799
+ name,
1800
+ parent: parts.length > 1 ? parts.join('/') : null,
1801
+ inode,
1802
+ usage,
1803
+ };
1804
+ this.#dentries.set(path, dentry);
1805
+ return dentry;
1806
+ });
1807
+ }
1808
+ /** Reads one path: the same Buffer reference that was mounted. */
1809
+ read(path) {
1810
+ const dentry = this.#dentries.get(path);
1811
+ return dentry === undefined ? null : (this.#inodes.get(dentry.inode.ino)?.buffer ?? null);
1812
+ }
1813
+ /** The usage flag of one path: process or keep, the only difference. */
1814
+ usageof(path) {
1815
+ return this.#dentries.get(path)?.usage ?? null;
1816
+ }
1817
+ /** Resolves one dentry. */
1818
+ resolve(path) {
1819
+ return this.#dentries.get(path) ?? null;
1820
+ }
1821
+ /** Number of mounted inodes. */
1822
+ size() {
1823
+ return this.#inodes.size;
1824
+ }
1825
+ }
1826
+ /**
1827
+ * memoryengine is the facade of the docs: an ordered backend list (the
1828
+ * tier ladder incarnate), an L1 Map cache and the load/persist/release
1829
+ * triple. load iterates the backends and returns the first hit through
1830
+ * transformToCompute (a copy: the documented ~2x overhead, with the
1831
+ * magic identity sniffed on the way out); persist writes into every
1832
+ * configured backend; release drops the key from the in-memory Map;
1833
+ * safeload wraps load in the {success, data, error} envelope. The VFS
1834
+ * twin of every loaded buffer is one mountbuffer call away: the same
1835
+ * bytes, only the usage flag differs.
1836
+ */
1837
+ export class memoryengine {
1838
+ name;
1839
+ #backends;
1840
+ #cache = new Map();
1841
+ #vfs = new virtualfilesystem();
1842
+ #counters = { loads: 0, l1hits: 0, demotions: 0, persists: 0, releases: 0 };
1843
+ constructor(options) {
1844
+ this.name = options?.name ?? 'saddle tiers engine';
1845
+ this.#backends = options?.backends ?? [];
1846
+ }
1847
+ /** Describes the wired backends in tier order. */
1848
+ backends() {
1849
+ return this.#backends.map((backend) => ({
1850
+ id: backend.id,
1851
+ tier: backend.tier,
1852
+ planner: backend.planner,
1853
+ }));
1854
+ }
1855
+ /**
1856
+ * load returns the first hit across the ladder. The L1 cache answers
1857
+ * directly; otherwise every backend is consulted in order, the first
1858
+ * hit is cached and returned as a compute buffer. A miss on every tier
1859
+ * throws the documented error.
1860
+ */
1861
+ async load(key) {
1862
+ return guard('engine', `load(${key})`, async () => {
1863
+ this.#counters.loads += 1;
1864
+ const cached = this.#cache.get(key) ?? null;
1865
+ if (cached !== null) {
1866
+ this.#counters.l1hits += 1;
1867
+ return this.transformToCompute(cached, {
1868
+ tier: 'l1',
1869
+ latencyns: latencyfor('ram'),
1870
+ backend: 'cache',
1871
+ });
1872
+ }
1873
+ for (const backend of this.#backends) {
1874
+ const hit = await backend.get(key);
1875
+ if (hit !== null) {
1876
+ this.#cache.set(key, hit);
1877
+ if (backend.tier !== 'l1') {
1878
+ this.#counters.demotions += 1;
1879
+ }
1880
+ return this.transformToCompute(hit, {
1881
+ tier: backend.tier,
1882
+ latencyns: backend.latencyns,
1883
+ backend: backend.id,
1884
+ });
1885
+ }
1886
+ }
1887
+ throw new tierserror(`load("${key}") missed every tier: ${this.#backends.map((b) => b.id).join(', ') || 'no backends'}`);
1888
+ });
1889
+ }
1890
+ /** persist writes the payload into every configured backend. */
1891
+ async persist(key, data) {
1892
+ return guard('engine', `persist(${key})`, async () => {
1893
+ if (!Buffer.isBuffer(data)) {
1894
+ throw new tierserror('persist expects a Buffer payload');
1895
+ }
1896
+ this.#counters.persists += 1;
1897
+ this.#cache.set(key, data);
1898
+ const written = [];
1899
+ const failures = [];
1900
+ for (const backend of this.#backends) {
1901
+ try {
1902
+ await backend.set(key, data);
1903
+ written.push(backend.id);
1904
+ }
1905
+ catch (backenderror) {
1906
+ failures.push(`${backend.id}: ${backenderror.message}`);
1907
+ }
1908
+ }
1909
+ if (written.length === 0 && failures.length > 0) {
1910
+ throw new tierserror(`persist("${key}") failed on every backend: ${failures.join('; ')}`);
1911
+ }
1912
+ return written;
1913
+ });
1914
+ }
1915
+ /** release deletes the key from the in-memory Map, per the docs. */
1916
+ release(key) {
1917
+ this.#counters.releases += 1;
1918
+ return this.#cache.delete(key);
1919
+ }
1920
+ /** safeload wraps load in the {success, data, error} envelope. */
1921
+ async safeload(key) {
1922
+ try {
1923
+ const loaded = await this.load(key);
1924
+ return { success: true, data: loaded.buffer };
1925
+ }
1926
+ catch (cause) {
1927
+ return { success: false, error: cause instanceof Error ? cause.message : String(cause) };
1928
+ }
1929
+ }
1930
+ /**
1931
+ * transformToCompute copies the bytes (the documented ~2x overhead),
1932
+ * sniffs the true type from the magic prefix and flags the result for
1933
+ * processing: the compute side of the thesis.
1934
+ */
1935
+ transformToCompute(buffer, meta) {
1936
+ return guard('engine', 'transformToCompute', () => ({
1937
+ buffer: Buffer.from(buffer),
1938
+ tier: meta?.tier ?? 'l1',
1939
+ latencyns: meta?.latencyns ?? latencyfor('ram'),
1940
+ backend: meta?.backend ?? 'cache',
1941
+ kind: sniffmagic(buffer).kind,
1942
+ usage: 'process',
1943
+ }));
1944
+ }
1945
+ /**
1946
+ * transformToStorage keeps the very same Buffer reference (zero copy)
1947
+ * and flips the usage flag to keep: the storage side of the thesis. No
1948
+ * byte changes, which is the point.
1949
+ */
1950
+ transformToStorage(buffer, meta) {
1951
+ return guard('engine', 'transformToStorage', () => ({
1952
+ buffer,
1953
+ tier: meta?.tier ?? 'l3',
1954
+ latencyns: meta?.latencyns ?? latencyfor('sqlite'),
1955
+ backend: meta?.backend ?? 'storage',
1956
+ kind: sniffmagic(buffer).kind,
1957
+ usage: 'keep',
1958
+ }));
1959
+ }
1960
+ /** Mounts one loaded key into the VFS twin under the requested usage. */
1961
+ mount(key, usage) {
1962
+ return guard('engine', `mount(${key})`, () => {
1963
+ const cached = this.#cache.get(key);
1964
+ if (cached === undefined) {
1965
+ throw new tierserror(`mount("${key}") requires a cached buffer (load first)`);
1966
+ }
1967
+ return this.#vfs.mountbuffer(`/tiers/${key}`, cached, usage);
1968
+ });
1969
+ }
1970
+ /** Live counters of the engine. */
1971
+ counters() {
1972
+ return { ...this.#counters };
1973
+ }
1974
+ /** Closes disposable backends (the sqlite kv among them). */
1975
+ [Symbol.dispose]() {
1976
+ for (const backend of this.#backends) {
1977
+ const disposable = backend;
1978
+ if (typeof disposable[Symbol.dispose] === 'function') {
1979
+ try {
1980
+ disposable[Symbol.dispose]?.();
1981
+ }
1982
+ catch {
1983
+ /* catcher: one failing backend must not block the rest */
1984
+ }
1985
+ }
1986
+ }
1987
+ this.#cache.clear();
1988
+ }
1989
+ }
1990
+ /**
1991
+ * creatiersengine wires the full stack from one config: the L1 ram
1992
+ * working set, the L3 sqlite kvstore (default tmpdir, SADDLE_TIERS_DB
1993
+ * overrides), the npm chunk planner (planner backend), the GitHub backend
1994
+ * (planner without SADDLE_GITHUB_TOKEN, live with it) and, when vdr is
1995
+ * requested, the universal VDR engine whose ring demotes into the same
1996
+ * sqlite kv and whose page table persists there. The modes array states
1997
+ * honestly which backends plan and which execute.
1998
+ */
1999
+ export function creatiersengine(config = {}) {
2000
+ return guard('factory', 'creatiersengine', () => {
2001
+ const kv = config.sqlitedb === undefined
2002
+ ? null
2003
+ : new sqlitekv(config.sqlitedb === true ? undefined : config.sqlitedb);
2004
+ const sqlite = kv === null ? null : new sqlitel3backend({ kv });
2005
+ const npm = config.npmscope === undefined ? null : new npmchunkregistry({ scope: config.npmscope });
2006
+ const github = config.github === undefined ? null : new githubstorage(config.github);
2007
+ const ram = new rambufferbackend(config.ram === true || config.ram === undefined ? {} : config.ram);
2008
+ const backends = [ram];
2009
+ if (sqlite !== null) {
2010
+ backends.push(sqlite);
2011
+ }
2012
+ if (npm !== null) {
2013
+ backends.push(npm);
2014
+ }
2015
+ if (github !== null) {
2016
+ backends.push(github);
2017
+ }
2018
+ const vdr = config.vdr === undefined
2019
+ ? null
2020
+ : new universalvdrengine({
2021
+ capmb: config.vdr === true ? undefined : config.vdr.capmb,
2022
+ l3: sqlite,
2023
+ pages: kv === null ? undefined : new localpagetable({ kv }),
2024
+ });
2025
+ return {
2026
+ engine: new memoryengine({ backends, name: config.name }),
2027
+ kv,
2028
+ sqlite,
2029
+ npm,
2030
+ github,
2031
+ vdr,
2032
+ modes: backends.map((backend) => `${backend.id}: ${backend.planner ? 'planner' : 'live'} (${backend.tier})`),
2033
+ };
2034
+ });
2035
+ }
2036
+ /**
2037
+ * quotaplanner renders the upload/dispatch plan of every L4 family as
2038
+ * plan objects: the Hugging Face upload_folder dataset push with the
2039
+ * real resolve URL pattern, the Kaggle dataset-metadata.json plus create
2040
+ * call, the Terabox rclone serve/sync pair with --transfers 8, the
2041
+ * R2/Storj s3 bucket writes and the GitLab/Forgejo/Gitea pipeline
2042
+ * dispatch endpoints. Every plan carries the quota table and the saddle
2043
+ * citation; nothing is executed here.
2044
+ */
2045
+ export function quotaplanner(kind, options = {}) {
2046
+ return guard('catalog', `quotaplanner(${kind})`, () => {
2047
+ const owner = options.owner ?? 'opencode';
2048
+ const repo = options.repo ?? 'opencode-storage';
2049
+ const uuid = options.uuid ?? '00000000-0000-4000-8000-000000000000';
2050
+ switch (kind) {
2051
+ case 'hf':
2052
+ return {
2053
+ backend: 'hf',
2054
+ commands: [
2055
+ `python3 -c "from huggingface_hub import HfApi; import os; HfApi(token=os.environ['HF_TOKEN']).upload_folder(repo_id='${owner}/${repo}', repo_type='dataset', folder_path='results')"`,
2056
+ `curl -fsSL "https://huggingface.co/datasets/${owner}/${repo}/resolve/main/payload.bin" -o payload.bin`,
2057
+ ],
2058
+ envvars: { HF_TOKEN: '<write token>' },
2059
+ quotas: [
2060
+ 'free unlimited best-effort; 10 TB public + 1 TB private PRO',
2061
+ '500 GB/file via Xet; private tier 100 GB',
2062
+ ],
2063
+ source: 'saddle docs readme1.md lines 479 and 486 (hf quota and upload_folder pattern)',
2064
+ };
2065
+ case 'kaggle':
2066
+ return {
2067
+ backend: 'kaggle',
2068
+ commands: [
2069
+ `printf '{"title":"${repo}","id":"${owner}/${repo}","licenses":[{"name":"CC0-1.0"}]}' > dataset-metadata.json`,
2070
+ 'kaggle datasets create -p results --dir-mode tar',
2071
+ ],
2072
+ envvars: { KAGGLE_USERNAME: '<username>', KAGGLE_KEY: '<api key>' },
2073
+ quotas: [
2074
+ '200 GB/dataset, 50 top-level files, free CDN egress',
2075
+ 'public storage unlimited',
2076
+ ],
2077
+ source: 'saddle docs readme1.md lines 480 and 486 (kaggle quota and metadata)',
2078
+ };
2079
+ case 'terabox':
2080
+ return {
2081
+ backend: 'terabox',
2082
+ commands: [
2083
+ 'rclone serve http terabox1:opencode-storage/ --addr :8080',
2084
+ 'rclone sync terabox1:opencode-storage huggingface:opencode-storage --transfers 8',
2085
+ ],
2086
+ envvars: { RCLONE_CONFIG_TERABOX1_TYPE: 'terabox' },
2087
+ quotas: ['1 TB/account (3 TB across three), 4 GB/file free', '300 files/transfer'],
2088
+ source: 'saddle docs readme1.md lines 477 and 481 (rclone serve/sync, terabox quota)',
2089
+ };
2090
+ case 'r2':
2091
+ return {
2092
+ backend: 'r2',
2093
+ commands: [
2094
+ `rclone copy payload.bin :s3,provider=Cloudflare,endpoint=https://<accountid>.r2.cloudflarestorage.com:opencode-bucket/${uuid}`,
2095
+ ],
2096
+ envvars: { AWS_ACCESS_KEY_ID: '<r2 key id>', AWS_SECRET_ACCESS_KEY: '<r2 secret>' },
2097
+ quotas: ['10 GB free, 10 M ops/mo, egress free', '5 GB per file'],
2098
+ source: 'saddle docs readme1.md line 478 (s3/r2 backend)',
2099
+ };
2100
+ case 'storj':
2101
+ return {
2102
+ backend: 'storj',
2103
+ commands: [`rclone copy payload.bin storj:opencode-bucket/${uuid}`],
2104
+ envvars: { STORJ_ACCESS_GRANT: '<access grant>' },
2105
+ quotas: ['free S3-compatible decentralized storage', 'egress via the CDN gateway'],
2106
+ source: 'saddle docs readme1.md line 488 (storj backend)',
2107
+ };
2108
+ case 'gitlab':
2109
+ return {
2110
+ backend: 'gitlab',
2111
+ commands: [
2112
+ `curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_TOKEN" "https://gitlab.com/api/v4/projects/${options.repoid ?? '<id>'}/pipeline_schedules" -d '{"description":"tiers flush","ref":"main","cron":"0 */6 * * *"}'`,
2113
+ `curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_TOKEN" "https://gitlab.com/api/v4/projects/${options.repoid ?? '<id>'}/pipeline_schedules/<schedule-id>/play"`,
2114
+ `curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_TOKEN" "https://gitlab.com/api/v4/projects/${options.repoid ?? '<id>'}/trigger/pipeline" -d "ref=main&token=$GITLAB_TRIGGER"`,
2115
+ ],
2116
+ envvars: { GITLAB_TOKEN: '<pat>', GITLAB_TRIGGER: '<trigger token>' },
2117
+ quotas: ['400 compute min/mo', '10 GB storage + 5 GB cache (14 d), artifacts 30 d'],
2118
+ source: 'saddle docs readme1.md line 512 (gitlab dispatch and quotas)',
2119
+ };
2120
+ case 'forgejo':
2121
+ case 'gitea': {
2122
+ const host = kind === 'forgejo' ? 'https://codeberg.org' : 'https://gitea.com';
2123
+ return {
2124
+ backend: kind,
2125
+ commands: [
2126
+ `curl -fsSL -X POST -H "Authorization: token $${kind.toUpperCase()}_TOKEN" -H "Content-Type: application/json" "${host}/api/v1/repos/${owner}/${repo}/actions/workflows/${options.workflowid ?? '<workflow-id>'}/dispatches" -d '{"ref":"main"}'`,
2127
+ ],
2128
+ envvars: { [`${kind.toUpperCase()}_TOKEN`]: '<api token>' },
2129
+ quotas: [
2130
+ 'codeberg forgejo: 750 MB soft quota + 1.5 GiB LFS/packages',
2131
+ 'gitea cloud: per-instance limits',
2132
+ ],
2133
+ source: 'saddle docs readme1.md lines 512 and 898 (forgejo/gitea dispatch endpoints)',
2134
+ };
2135
+ }
2136
+ default:
2137
+ throw new tierserror(`unknown catalog kind "${String(kind)}"`);
2138
+ }
2139
+ });
2140
+ }
2141
+ /* ------------------------------------------------------------------ */
2142
+ /* Section 24: tiersreport (printable summary) */
2143
+ /* ------------------------------------------------------------------ */
2144
+ /**
2145
+ * tiersreport renders the printable summary of the whole layer: the four
2146
+ * tiers with latencies, the ladder, the free pool headline with the
2147
+ * counted families, the autoscale rule and the VDR address space. The
2148
+ * smoke suite prints it verbatim.
2149
+ */
2150
+ export function tiersreport() {
2151
+ return guard('report', 'tiersreport', () => {
2152
+ const pool = freepooltotalgb();
2153
+ const lines = ['saddle tiers: everything is VRAM', ''];
2154
+ for (const tier of Object.values(TIERS)) {
2155
+ lines.push(`${tier.id} ${tier.label.padEnd(14)} ${String(tier.latencyns).padStart(6)} ns ${tier.capacity}`);
2156
+ }
2157
+ lines.push('', 'latency ladder:');
2158
+ for (const rung of LATENCYLADDER) {
2159
+ lines.push(` ${rung.kind.padEnd(7)} ${String(rung.latencyns).padStart(6)} ns ${rung.note}`);
2160
+ }
2161
+ lines.push('', `free pool: ${pool.headline}`, ...FREEPOOL.map((entry) => ` ${entry.backend.padEnd(12)} ${entry.quota}`), '', 'autoscale: <64 MB memfs, <1 GB mmap, larger sqlite/r2', `vdr: 0x0..0xffffffffffffffff (${VDRADDRESSEB} EB), ring cap 512 MB default (SADDLE_VDR_CAP MB)`);
2162
+ return lines.join('\n');
2163
+ });
2164
+ }
2165
+ //# sourceMappingURL=tiers.js.map