sobres 1.1.0__tar.gz

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 (610) hide show
  1. sobres-1.1.0/.claude/hooks/commit-gate.sh +34 -0
  2. sobres-1.1.0/.claude/settings.json +16 -0
  3. sobres-1.1.0/.dockerignore +24 -0
  4. sobres-1.1.0/.gitattributes +2 -0
  5. sobres-1.1.0/.github/ISSUE_TEMPLATE/hosting-inquiry.yml +38 -0
  6. sobres-1.1.0/.github/dependabot.yml +27 -0
  7. sobres-1.1.0/.github/pull_request_template.md +26 -0
  8. sobres-1.1.0/.github/scripts/check_release.py +138 -0
  9. sobres-1.1.0/.github/workflows/ci.yml +353 -0
  10. sobres-1.1.0/.github/workflows/codeql.yml +35 -0
  11. sobres-1.1.0/.github/workflows/live.yml +59 -0
  12. sobres-1.1.0/.github/workflows/pages.yml +61 -0
  13. sobres-1.1.0/.github/workflows/release.yml +283 -0
  14. sobres-1.1.0/.gitignore +33 -0
  15. sobres-1.1.0/.pre-commit-config.yaml +48 -0
  16. sobres-1.1.0/.trivyignore +3 -0
  17. sobres-1.1.0/AGENTS.md +80 -0
  18. sobres-1.1.0/CHANGELOG.md +272 -0
  19. sobres-1.1.0/CLAUDE.md +238 -0
  20. sobres-1.1.0/Dockerfile +68 -0
  21. sobres-1.1.0/LICENSE +21 -0
  22. sobres-1.1.0/PKG-INFO +485 -0
  23. sobres-1.1.0/README.md +388 -0
  24. sobres-1.1.0/docker-compose.yml +23 -0
  25. sobres-1.1.0/docs/DEPLOYING.md +110 -0
  26. sobres-1.1.0/docs/RELEASING.md +172 -0
  27. sobres-1.1.0/docs/ROADMAP.md +103 -0
  28. sobres-1.1.0/docs/allocation-lp-reference.md +78 -0
  29. sobres-1.1.0/docs/foundation-completion.md +81 -0
  30. sobres-1.1.0/docs/foundation-reuse-audit.md +25 -0
  31. sobres-1.1.0/docs/integration/main-sync.md +30 -0
  32. sobres-1.1.0/docs/integration/pr10-main-sync.md +18 -0
  33. sobres-1.1.0/docs/integration/pr11-main-sync.md +16 -0
  34. sobres-1.1.0/docs/integration/pr12-main-sync.md +19 -0
  35. sobres-1.1.0/docs/integration/pr13-main-sync.md +20 -0
  36. sobres-1.1.0/docs/integration/pr14-main-sync.md +14 -0
  37. sobres-1.1.0/docs/integration/pr15-main-sync.md +15 -0
  38. sobres-1.1.0/docs/integration/pr15-multivariable-spec.md +28 -0
  39. sobres-1.1.0/docs/integration/pr16-main-sync.md +32 -0
  40. sobres-1.1.0/docs/integration/pr9-main-sync.md +19 -0
  41. sobres-1.1.0/docs/reviews/pr-7/evidence/dependency-audit.txt +3 -0
  42. sobres-1.1.0/docs/reviews/pr-7/evidence/network-tests.txt +21 -0
  43. sobres-1.1.0/docs/reviews/pr-7/evidence/offline-tests.txt +65 -0
  44. sobres-1.1.0/docs/reviews/pr-7/evidence/openspec-validation.txt +43 -0
  45. sobres-1.1.0/docs/reviews/pr-7/evidence/probes.json +43 -0
  46. sobres-1.1.0/docs/reviews/pr-7/fixes.md +94 -0
  47. sobres-1.1.0/docs/reviews/pr-7/github-review.md +57 -0
  48. sobres-1.1.0/docs/reviews/pr-7/report.md +200 -0
  49. sobres-1.1.0/docs/reviews/pr-7/reproduce.py +190 -0
  50. sobres-1.1.0/docs/reviews/pr-7/self-test.md +197 -0
  51. sobres-1.1.0/docs/reviews/pr-8/evidence/audit.log +229 -0
  52. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-csv-DEBUG.stderr +18 -0
  53. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-csv-DEBUG.stdout +15 -0
  54. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-csv-ERROR.stderr +1 -0
  55. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-csv-ERROR.stdout +15 -0
  56. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-csv-INFO.stderr +11 -0
  57. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-csv-INFO.stdout +15 -0
  58. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-csv-WARNING.stderr +1 -0
  59. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-csv-WARNING.stdout +15 -0
  60. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-json-DEBUG.stderr +18 -0
  61. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-json-DEBUG.stdout +1177 -0
  62. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-json-ERROR.stderr +1 -0
  63. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-json-ERROR.stdout +1177 -0
  64. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-json-INFO.stderr +11 -0
  65. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-json-INFO.stdout +1177 -0
  66. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-json-WARNING.stderr +1 -0
  67. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-json-WARNING.stdout +1177 -0
  68. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-table-DEBUG.stderr +18 -0
  69. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-table-DEBUG.stdout +31 -0
  70. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-table-ERROR.stderr +1 -0
  71. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-table-ERROR.stdout +31 -0
  72. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-table-INFO.stderr +11 -0
  73. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-table-INFO.stdout +31 -0
  74. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-table-WARNING.stderr +1 -0
  75. sobres-1.1.0/docs/reviews/pr-8/evidence/backtest-table-WARNING.stdout +31 -0
  76. sobres-1.1.0/docs/reviews/pr-8/evidence/build.log +18 -0
  77. sobres-1.1.0/docs/reviews/pr-8/evidence/capm.stderr +3 -0
  78. sobres-1.1.0/docs/reviews/pr-8/evidence/capm.stdout +0 -0
  79. sobres-1.1.0/docs/reviews/pr-8/evidence/cli-summary.json +332 -0
  80. sobres-1.1.0/docs/reviews/pr-8/evidence/duplicate-risk.stderr +4 -0
  81. sobres-1.1.0/docs/reviews/pr-8/evidence/duplicate-risk.stdout +0 -0
  82. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-csv-DEBUG.stderr +10 -0
  83. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-csv-DEBUG.stdout +7 -0
  84. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-csv-ERROR.stderr +1 -0
  85. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-csv-ERROR.stdout +7 -0
  86. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-csv-INFO.stderr +3 -0
  87. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-csv-INFO.stdout +7 -0
  88. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-csv-WARNING.stderr +1 -0
  89. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-csv-WARNING.stdout +7 -0
  90. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-json-DEBUG.stderr +10 -0
  91. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-json-DEBUG.stdout +90 -0
  92. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-json-ERROR.stderr +1 -0
  93. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-json-ERROR.stdout +90 -0
  94. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-json-INFO.stderr +3 -0
  95. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-json-INFO.stdout +90 -0
  96. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-json-WARNING.stderr +1 -0
  97. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-json-WARNING.stdout +90 -0
  98. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-table-DEBUG.stderr +10 -0
  99. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-table-DEBUG.stdout +19 -0
  100. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-table-ERROR.stderr +1 -0
  101. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-table-ERROR.stdout +19 -0
  102. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-table-INFO.stderr +3 -0
  103. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-table-INFO.stdout +19 -0
  104. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-table-WARNING.stderr +1 -0
  105. sobres-1.1.0/docs/reviews/pr-8/evidence/frontier-table-WARNING.stdout +19 -0
  106. sobres-1.1.0/docs/reviews/pr-8/evidence/github.json +1 -0
  107. sobres-1.1.0/docs/reviews/pr-8/evidence/independent.json +66 -0
  108. sobres-1.1.0/docs/reviews/pr-8/evidence/install-security-live.jsonl +12 -0
  109. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-csv-DEBUG.stderr +11 -0
  110. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-csv-DEBUG.stdout +3 -0
  111. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-csv-ERROR.stderr +1 -0
  112. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-csv-ERROR.stdout +3 -0
  113. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-csv-INFO.stderr +4 -0
  114. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-csv-INFO.stdout +3 -0
  115. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-csv-WARNING.stderr +2 -0
  116. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-csv-WARNING.stdout +3 -0
  117. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-json-DEBUG.stderr +11 -0
  118. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-json-DEBUG.stdout +51 -0
  119. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-json-ERROR.stderr +1 -0
  120. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-json-ERROR.stdout +51 -0
  121. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-json-INFO.stderr +4 -0
  122. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-json-INFO.stdout +51 -0
  123. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-json-WARNING.stderr +2 -0
  124. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-json-WARNING.stdout +51 -0
  125. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-table-DEBUG.stderr +11 -0
  126. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-table-DEBUG.stdout +20 -0
  127. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-table-ERROR.stderr +1 -0
  128. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-table-ERROR.stdout +20 -0
  129. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-table-INFO.stderr +4 -0
  130. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-table-INFO.stdout +20 -0
  131. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-table-WARNING.stderr +2 -0
  132. sobres-1.1.0/docs/reviews/pr-8/evidence/markowitz-table-WARNING.stdout +20 -0
  133. sobres-1.1.0/docs/reviews/pr-8/evidence/nan-risk.stderr +4 -0
  134. sobres-1.1.0/docs/reviews/pr-8/evidence/nan-risk.stdout +0 -0
  135. sobres-1.1.0/docs/reviews/pr-8/evidence/nan-riskfree.stderr +3 -0
  136. sobres-1.1.0/docs/reviews/pr-8/evidence/nan-riskfree.stdout +0 -0
  137. sobres-1.1.0/docs/reviews/pr-8/evidence/negative-seed.stderr +4 -0
  138. sobres-1.1.0/docs/reviews/pr-8/evidence/negative-seed.stdout +0 -0
  139. sobres-1.1.0/docs/reviews/pr-8/evidence/performance.log +13 -0
  140. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-csv-DEBUG.stderr +10 -0
  141. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-csv-DEBUG.stdout +16 -0
  142. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-csv-ERROR.stderr +1 -0
  143. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-csv-ERROR.stdout +16 -0
  144. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-csv-INFO.stderr +3 -0
  145. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-csv-INFO.stdout +16 -0
  146. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-csv-WARNING.stderr +1 -0
  147. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-csv-WARNING.stdout +16 -0
  148. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-json-DEBUG.stderr +10 -0
  149. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-json-DEBUG.stdout +88 -0
  150. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-json-ERROR.stderr +1 -0
  151. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-json-ERROR.stdout +88 -0
  152. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-json-INFO.stderr +3 -0
  153. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-json-INFO.stdout +88 -0
  154. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-json-WARNING.stderr +1 -0
  155. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-json-WARNING.stdout +88 -0
  156. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-table-DEBUG.stderr +10 -0
  157. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-table-DEBUG.stdout +28 -0
  158. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-table-ERROR.stderr +1 -0
  159. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-table-ERROR.stdout +28 -0
  160. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-table-INFO.stderr +3 -0
  161. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-table-INFO.stdout +28 -0
  162. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-table-WARNING.stderr +1 -0
  163. sobres-1.1.0/docs/reviews/pr-8/evidence/risk-table-WARNING.stdout +28 -0
  164. sobres-1.1.0/docs/reviews/pr-8/evidence/target-backtest-option.stderr +4 -0
  165. sobres-1.1.0/docs/reviews/pr-8/evidence/target-backtest-option.stdout +0 -0
  166. sobres-1.1.0/docs/reviews/pr-8/evidence/target-backtest.stderr +3 -0
  167. sobres-1.1.0/docs/reviews/pr-8/evidence/target-backtest.stdout +0 -0
  168. sobres-1.1.0/docs/reviews/pr-8/evidence/tests-python312.log +73 -0
  169. sobres-1.1.0/docs/reviews/pr-8/evidence/tests-python314.log +98 -0
  170. sobres-1.1.0/docs/reviews/pr-8/evidence/tty-skip.txt +12 -0
  171. sobres-1.1.0/docs/reviews/pr-8/fixes.md +134 -0
  172. sobres-1.1.0/docs/reviews/pr-8/github-review.json +55 -0
  173. sobres-1.1.0/docs/reviews/pr-8/github-review.md +53 -0
  174. sobres-1.1.0/docs/reviews/pr-8/pypi-issue.md +56 -0
  175. sobres-1.1.0/docs/reviews/pr-8/report.md +239 -0
  176. sobres-1.1.0/docs/reviews/pr-8/reproduce.py +244 -0
  177. sobres-1.1.0/docs/reviews/pr-8/self-test.md +154 -0
  178. sobres-1.1.0/docs/reviews/pr-8/verification/dependency-audit.log +11 -0
  179. sobres-1.1.0/docs/reviews/pr-8/verification/live-tests.log +10 -0
  180. sobres-1.1.0/docs/reviews/pr-8/verification/offline-tests.log +80 -0
  181. sobres-1.1.0/docs/reviews/pr-8/verification/performance.jsonl +14 -0
  182. sobres-1.1.0/docs/reviews/pr-8/verification/wheel-formats.log +49 -0
  183. sobres-1.1.0/docs/why-your-backtest-looks-too-good.md +83 -0
  184. sobres-1.1.0/frontend/index.html +23 -0
  185. sobres-1.1.0/frontend/openapi.json +7136 -0
  186. sobres-1.1.0/frontend/package-lock.json +3573 -0
  187. sobres-1.1.0/frontend/package.json +37 -0
  188. sobres-1.1.0/frontend/public/manifest.json +80 -0
  189. sobres-1.1.0/frontend/scripts/check-bundle.mjs +25 -0
  190. sobres-1.1.0/frontend/scripts/check-client.mjs +11 -0
  191. sobres-1.1.0/frontend/src/App.tsx +97 -0
  192. sobres-1.1.0/frontend/src/api/client.ts +128 -0
  193. sobres-1.1.0/frontend/src/api/schema.d.ts +6339 -0
  194. sobres-1.1.0/frontend/src/charts/BacktestCharts.tsx +62 -0
  195. sobres-1.1.0/frontend/src/charts/EChart.tsx +39 -0
  196. sobres-1.1.0/frontend/src/charts/FrontierChart.tsx +45 -0
  197. sobres-1.1.0/frontend/src/components/Disclaimer.tsx +10 -0
  198. sobres-1.1.0/frontend/src/components/JobProgress.tsx +37 -0
  199. sobres-1.1.0/frontend/src/components/Provenance.tsx +35 -0
  200. sobres-1.1.0/frontend/src/components/ResultTable.tsx +75 -0
  201. sobres-1.1.0/frontend/src/components/TokenGate.tsx +37 -0
  202. sobres-1.1.0/frontend/src/components/ui/button.tsx +18 -0
  203. sobres-1.1.0/frontend/src/components/ui/card.tsx +9 -0
  204. sobres-1.1.0/frontend/src/components/ui/switch.tsx +14 -0
  205. sobres-1.1.0/frontend/src/components/ui/tabs.tsx +16 -0
  206. sobres-1.1.0/frontend/src/components/ui/tooltip.tsx +17 -0
  207. sobres-1.1.0/frontend/src/forms/CommandForm.tsx +132 -0
  208. sobres-1.1.0/frontend/src/index.css +24 -0
  209. sobres-1.1.0/frontend/src/lib/format.ts +51 -0
  210. sobres-1.1.0/frontend/src/main.tsx +10 -0
  211. sobres-1.1.0/frontend/src/theme/ThemeProvider.tsx +96 -0
  212. sobres-1.1.0/frontend/src/theme/tokens.css +39 -0
  213. sobres-1.1.0/frontend/src/views/CommandView.tsx +72 -0
  214. sobres-1.1.0/frontend/src/views/DoctorView.tsx +69 -0
  215. sobres-1.1.0/frontend/src/views/HomeView.tsx +43 -0
  216. sobres-1.1.0/frontend/src/views/JobsView.tsx +22 -0
  217. sobres-1.1.0/frontend/src/views/PortfoliosView.tsx +98 -0
  218. sobres-1.1.0/frontend/src/views/ResultView.tsx +55 -0
  219. sobres-1.1.0/frontend/src/views/RunsView.tsx +96 -0
  220. sobres-1.1.0/frontend/src/views/SettingsView.tsx +110 -0
  221. sobres-1.1.0/frontend/tsconfig.json +22 -0
  222. sobres-1.1.0/frontend/vite.config.ts +26 -0
  223. sobres-1.1.0/openspec/changes/0000-release-engineering/proposal.md +87 -0
  224. sobres-1.1.0/openspec/changes/0000-release-engineering/specs/release-pipeline/spec.md +217 -0
  225. sobres-1.1.0/openspec/changes/0000-release-engineering/tasks.md +75 -0
  226. sobres-1.1.0/openspec/changes/0001-foundation-data-and-cli/design.md +363 -0
  227. sobres-1.1.0/openspec/changes/0001-foundation-data-and-cli/proposal.md +123 -0
  228. sobres-1.1.0/openspec/changes/0001-foundation-data-and-cli/specs/cli-shell/spec.md +121 -0
  229. sobres-1.1.0/openspec/changes/0001-foundation-data-and-cli/specs/command-registry/spec.md +101 -0
  230. sobres-1.1.0/openspec/changes/0001-foundation-data-and-cli/specs/currency/spec.md +144 -0
  231. sobres-1.1.0/openspec/changes/0001-foundation-data-and-cli/specs/market-data/spec.md +256 -0
  232. sobres-1.1.0/openspec/changes/0001-foundation-data-and-cli/specs/observability/spec.md +226 -0
  233. sobres-1.1.0/openspec/changes/0001-foundation-data-and-cli/specs/onboarding/spec.md +194 -0
  234. sobres-1.1.0/openspec/changes/0001-foundation-data-and-cli/specs/storage/spec.md +157 -0
  235. sobres-1.1.0/openspec/changes/0001-foundation-data-and-cli/specs/testing/spec.md +174 -0
  236. sobres-1.1.0/openspec/changes/0001-foundation-data-and-cli/tasks.md +248 -0
  237. sobres-1.1.0/openspec/changes/0002-portfolio-optimization/design.md +169 -0
  238. sobres-1.1.0/openspec/changes/0002-portfolio-optimization/proposal.md +107 -0
  239. sobres-1.1.0/openspec/changes/0002-portfolio-optimization/review-decisions.md +43 -0
  240. sobres-1.1.0/openspec/changes/0002-portfolio-optimization/specs/portfolio-optimization/spec.md +306 -0
  241. sobres-1.1.0/openspec/changes/0002-portfolio-optimization/tasks.md +154 -0
  242. sobres-1.1.0/openspec/changes/0003-local-persistence/design.md +25 -0
  243. sobres-1.1.0/openspec/changes/0003-local-persistence/proposal.md +108 -0
  244. sobres-1.1.0/openspec/changes/0003-local-persistence/specs/persistence/spec.md +221 -0
  245. sobres-1.1.0/openspec/changes/0003-local-persistence/tasks.md +69 -0
  246. sobres-1.1.0/openspec/changes/0004-web-ui/design.md +166 -0
  247. sobres-1.1.0/openspec/changes/0004-web-ui/proposal.md +118 -0
  248. sobres-1.1.0/openspec/changes/0004-web-ui/specs/http-api/spec.md +211 -0
  249. sobres-1.1.0/openspec/changes/0004-web-ui/specs/web-ui/spec.md +286 -0
  250. sobres-1.1.0/openspec/changes/0004-web-ui/tasks.md +91 -0
  251. sobres-1.1.0/openspec/changes/0005-docker-distribution/design.md +25 -0
  252. sobres-1.1.0/openspec/changes/0005-docker-distribution/proposal.md +113 -0
  253. sobres-1.1.0/openspec/changes/0005-docker-distribution/specs/deployment/spec.md +272 -0
  254. sobres-1.1.0/openspec/changes/0005-docker-distribution/tasks.md +80 -0
  255. sobres-1.1.0/openspec/changes/0006-landing-page/design.md +25 -0
  256. sobres-1.1.0/openspec/changes/0006-landing-page/proposal.md +115 -0
  257. sobres-1.1.0/openspec/changes/0006-landing-page/specs/landing-page/spec.md +189 -0
  258. sobres-1.1.0/openspec/changes/0006-landing-page/tasks.md +70 -0
  259. sobres-1.1.0/openspec/changes/0007-equity-factor-analysis/design.md +68 -0
  260. sobres-1.1.0/openspec/changes/0007-equity-factor-analysis/proposal.md +85 -0
  261. sobres-1.1.0/openspec/changes/0007-equity-factor-analysis/specs/equity-analysis/spec.md +79 -0
  262. sobres-1.1.0/openspec/changes/0007-equity-factor-analysis/tasks.md +46 -0
  263. sobres-1.1.0/openspec/changes/0008-goal-planning/design.md +69 -0
  264. sobres-1.1.0/openspec/changes/0008-goal-planning/proposal.md +85 -0
  265. sobres-1.1.0/openspec/changes/0008-goal-planning/specs/goal-planning/spec.md +132 -0
  266. sobres-1.1.0/openspec/changes/0008-goal-planning/tasks.md +41 -0
  267. sobres-1.1.0/openspec/changes/0009-econometrics-forecasting/design.md +246 -0
  268. sobres-1.1.0/openspec/changes/0009-econometrics-forecasting/proposal.md +132 -0
  269. sobres-1.1.0/openspec/changes/0009-econometrics-forecasting/research.md +77 -0
  270. sobres-1.1.0/openspec/changes/0009-econometrics-forecasting/specs/econometrics/spec.md +231 -0
  271. sobres-1.1.0/openspec/changes/0009-econometrics-forecasting/tasks.md +90 -0
  272. sobres-1.1.0/openspec/changes/0010-currency-and-ppp/design.md +86 -0
  273. sobres-1.1.0/openspec/changes/0010-currency-and-ppp/proposal.md +124 -0
  274. sobres-1.1.0/openspec/changes/0010-currency-and-ppp/specs/fx-analytics/spec.md +125 -0
  275. sobres-1.1.0/openspec/changes/0010-currency-and-ppp/specs/purchasing-power/spec.md +149 -0
  276. sobres-1.1.0/openspec/changes/0010-currency-and-ppp/tasks.md +60 -0
  277. sobres-1.1.0/openspec/changes/0011-rebrand-sobres/proposal.md +99 -0
  278. sobres-1.1.0/openspec/changes/0011-rebrand-sobres/specs/packaging/spec.md +54 -0
  279. sobres-1.1.0/openspec/changes/0011-rebrand-sobres/tasks.md +62 -0
  280. sobres-1.1.0/openspec/changes/0012-foundation-review-fixes/design.md +24 -0
  281. sobres-1.1.0/openspec/changes/0012-foundation-review-fixes/proposal.md +26 -0
  282. sobres-1.1.0/openspec/changes/0012-foundation-review-fixes/specs/foundation-correctness/spec.md +112 -0
  283. sobres-1.1.0/openspec/changes/0012-foundation-review-fixes/tasks.md +21 -0
  284. sobres-1.1.0/openspec/changes/0013-template-development-alignment/.openspec.yaml +2 -0
  285. sobres-1.1.0/openspec/changes/0013-template-development-alignment/alignment-audit.md +145 -0
  286. sobres-1.1.0/openspec/changes/0013-template-development-alignment/design.md +205 -0
  287. sobres-1.1.0/openspec/changes/0013-template-development-alignment/proposal.md +146 -0
  288. sobres-1.1.0/openspec/changes/0013-template-development-alignment/review-draft.md +33 -0
  289. sobres-1.1.0/openspec/changes/0013-template-development-alignment/specs/development-workflow/spec.md +116 -0
  290. sobres-1.1.0/openspec/changes/0013-template-development-alignment/specs/template-alignment/spec.md +103 -0
  291. sobres-1.1.0/openspec/changes/0013-template-development-alignment/tasks.md +132 -0
  292. sobres-1.1.0/openspec/changes/0013-template-development-alignment/tracking.md +46 -0
  293. sobres-1.1.0/openspec/changes/0013-template-development-alignment/validation.md +110 -0
  294. sobres-1.1.0/openspec/changes/0014-onboarding-defaults/design.md +34 -0
  295. sobres-1.1.0/openspec/changes/0014-onboarding-defaults/proposal.md +71 -0
  296. sobres-1.1.0/openspec/changes/0014-onboarding-defaults/specs/cli-shell/spec.md +48 -0
  297. sobres-1.1.0/openspec/changes/0014-onboarding-defaults/specs/onboarding/spec.md +37 -0
  298. sobres-1.1.0/openspec/changes/0014-onboarding-defaults/tasks.md +13 -0
  299. sobres-1.1.0/openspec/changes/0015-risk-free-proxy/design.md +29 -0
  300. sobres-1.1.0/openspec/changes/0015-risk-free-proxy/proposal.md +56 -0
  301. sobres-1.1.0/openspec/changes/0015-risk-free-proxy/specs/portfolio-optimization/spec.md +37 -0
  302. sobres-1.1.0/openspec/changes/0015-risk-free-proxy/tasks.md +7 -0
  303. sobres-1.1.0/openspec/changes/0016-broker-execution/design.md +84 -0
  304. sobres-1.1.0/openspec/changes/0016-broker-execution/proposal.md +87 -0
  305. sobres-1.1.0/openspec/changes/0016-broker-execution/specs/broker-execution/spec.md +130 -0
  306. sobres-1.1.0/openspec/changes/0016-broker-execution/tasks.md +43 -0
  307. sobres-1.1.0/openspec/changes/0017-arm-publication/design.md +42 -0
  308. sobres-1.1.0/openspec/changes/0017-arm-publication/proposal.md +55 -0
  309. sobres-1.1.0/openspec/changes/0017-arm-publication/specs/landing-page/spec.md +25 -0
  310. sobres-1.1.0/openspec/changes/0017-arm-publication/specs/release-pipeline/spec.md +52 -0
  311. sobres-1.1.0/openspec/changes/0017-arm-publication/tasks.md +50 -0
  312. sobres-1.1.0/openspec/changes/archive/.gitkeep +0 -0
  313. sobres-1.1.0/openspec/config.yaml +34 -0
  314. sobres-1.1.0/openspec/project.md +341 -0
  315. sobres-1.1.0/openspec/specs/.gitkeep +0 -0
  316. sobres-1.1.0/pyproject.toml +134 -0
  317. sobres-1.1.0/scripts/export_openapi.py +51 -0
  318. sobres-1.1.0/scripts/r_reference.py +102 -0
  319. sobres-1.1.0/scripts/record_fixtures.py +263 -0
  320. sobres-1.1.0/scripts/schema_fixture.py +75 -0
  321. sobres-1.1.0/scripts/synthesize_fixtures.py +481 -0
  322. sobres-1.1.0/scripts/synthesize_market_proxy.py +85 -0
  323. sobres-1.1.0/site/index.html +187 -0
  324. sobres-1.1.0/site/lighthouserc.json +40 -0
  325. sobres-1.1.0/site/package-lock.json +1879 -0
  326. sobres-1.1.0/site/package.json +27 -0
  327. sobres-1.1.0/site/public/og-image.png +0 -0
  328. sobres-1.1.0/site/scripts/check-bundle.mjs +23 -0
  329. sobres-1.1.0/site/scripts/check-content.mjs +38 -0
  330. sobres-1.1.0/site/scripts/generate.mjs +29 -0
  331. sobres-1.1.0/site/scripts/og_image.py +51 -0
  332. sobres-1.1.0/site/scripts/prepare-audit.mjs +12 -0
  333. sobres-1.1.0/site/scripts/record_figures.py +183 -0
  334. sobres-1.1.0/site/src/data/backtest.json +2027 -0
  335. sobres-1.1.0/site/src/data/figures-meta.json +38 -0
  336. sobres-1.1.0/site/src/data/frontier.json +673 -0
  337. sobres-1.1.0/site/src/data/terminal.txt +24 -0
  338. sobres-1.1.0/site/src/figures.ts +107 -0
  339. sobres-1.1.0/site/src/main.ts +76 -0
  340. sobres-1.1.0/site/src/motion.ts +96 -0
  341. sobres-1.1.0/site/src/style.css +80 -0
  342. sobres-1.1.0/site/src/vite-env.d.ts +5 -0
  343. sobres-1.1.0/site/tsconfig.json +17 -0
  344. sobres-1.1.0/site/vite.config.ts +23 -0
  345. sobres-1.1.0/skills/openspec-pr-review/SKILL.md +49 -0
  346. sobres-1.1.0/src/sobres/__about__.py +1 -0
  347. sobres-1.1.0/src/sobres/__init__.py +5 -0
  348. sobres-1.1.0/src/sobres/api/__init__.py +19 -0
  349. sobres-1.1.0/src/sobres/api/app.py +496 -0
  350. sobres-1.1.0/src/sobres/api/auth.py +117 -0
  351. sobres-1.1.0/src/sobres/api/jobs.py +260 -0
  352. sobres-1.1.0/src/sobres/api/static/assets/BacktestCharts-C7phgH_S.js +1 -0
  353. sobres-1.1.0/src/sobres/api/static/assets/EChart-BHrkUjbY.js +1 -0
  354. sobres-1.1.0/src/sobres/api/static/assets/FrontierChart-B3J3HsGE.js +1 -0
  355. sobres-1.1.0/src/sobres/api/static/assets/echarts-BpE1uIOo.js +39 -0
  356. sobres-1.1.0/src/sobres/api/static/assets/index-DikqmPZ6.js +68 -0
  357. sobres-1.1.0/src/sobres/api/static/assets/index-pgZOLb7x.css +1 -0
  358. sobres-1.1.0/src/sobres/api/static/assets/motion-D99TpcCF.js +17 -0
  359. sobres-1.1.0/src/sobres/api/static/index.html +25 -0
  360. sobres-1.1.0/src/sobres/api/static/manifest.json +80 -0
  361. sobres-1.1.0/src/sobres/cli/__init__.py +0 -0
  362. sobres-1.1.0/src/sobres/cli/commands/__init__.py +29 -0
  363. sobres-1.1.0/src/sobres/cli/commands/analyze.py +390 -0
  364. sobres-1.1.0/src/sobres/cli/commands/cache.py +59 -0
  365. sobres-1.1.0/src/sobres/cli/commands/commands.py +26 -0
  366. sobres-1.1.0/src/sobres/cli/commands/config.py +85 -0
  367. sobres-1.1.0/src/sobres/cli/commands/data.py +135 -0
  368. sobres-1.1.0/src/sobres/cli/commands/db.py +103 -0
  369. sobres-1.1.0/src/sobres/cli/commands/deploy.py +102 -0
  370. sobres-1.1.0/src/sobres/cli/commands/doctor.py +105 -0
  371. sobres-1.1.0/src/sobres/cli/commands/econ.py +880 -0
  372. sobres-1.1.0/src/sobres/cli/commands/fx.py +436 -0
  373. sobres-1.1.0/src/sobres/cli/commands/init.py +227 -0
  374. sobres-1.1.0/src/sobres/cli/commands/optimize.py +925 -0
  375. sobres-1.1.0/src/sobres/cli/commands/plan.py +648 -0
  376. sobres-1.1.0/src/sobres/cli/commands/portfolio.py +122 -0
  377. sobres-1.1.0/src/sobres/cli/commands/ppp.py +398 -0
  378. sobres-1.1.0/src/sobres/cli/commands/run.py +174 -0
  379. sobres-1.1.0/src/sobres/cli/commands/serve.py +263 -0
  380. sobres-1.1.0/src/sobres/cli/commands/trade.py +892 -0
  381. sobres-1.1.0/src/sobres/cli/commands/upgrade.py +68 -0
  382. sobres-1.1.0/src/sobres/cli/commands/watchlist.py +112 -0
  383. sobres-1.1.0/src/sobres/cli/context.py +237 -0
  384. sobres-1.1.0/src/sobres/cli/main.py +223 -0
  385. sobres-1.1.0/src/sobres/cli/metric_table.py +45 -0
  386. sobres-1.1.0/src/sobres/cli/render.py +130 -0
  387. sobres-1.1.0/src/sobres/cli/window.py +27 -0
  388. sobres-1.1.0/src/sobres/config.py +221 -0
  389. sobres-1.1.0/src/sobres/core/__init__.py +5 -0
  390. sobres-1.1.0/src/sobres/core/allocate.py +114 -0
  391. sobres-1.1.0/src/sobres/core/backtest.py +241 -0
  392. sobres-1.1.0/src/sobres/core/conventions.py +59 -0
  393. sobres-1.1.0/src/sobres/core/errors.py +120 -0
  394. sobres-1.1.0/src/sobres/core/factors.py +265 -0
  395. sobres-1.1.0/src/sobres/core/forecast.py +1045 -0
  396. sobres-1.1.0/src/sobres/core/fx.py +170 -0
  397. sobres-1.1.0/src/sobres/core/goals.py +408 -0
  398. sobres-1.1.0/src/sobres/core/moments.py +221 -0
  399. sobres-1.1.0/src/sobres/core/optimize.py +545 -0
  400. sobres-1.1.0/src/sobres/core/ppp.py +303 -0
  401. sobres-1.1.0/src/sobres/core/rates.py +58 -0
  402. sobres-1.1.0/src/sobres/core/regression.py +145 -0
  403. sobres-1.1.0/src/sobres/core/returns.py +116 -0
  404. sobres-1.1.0/src/sobres/core/risk.py +216 -0
  405. sobres-1.1.0/src/sobres/core/simulate.py +145 -0
  406. sobres-1.1.0/src/sobres/core/timeseries.py +308 -0
  407. sobres-1.1.0/src/sobres/core/trading.py +318 -0
  408. sobres-1.1.0/src/sobres/core/validation.py +27 -0
  409. sobres-1.1.0/src/sobres/data/__init__.py +5 -0
  410. sobres-1.1.0/src/sobres/data/align.py +62 -0
  411. sobres-1.1.0/src/sobres/data/base.py +212 -0
  412. sobres-1.1.0/src/sobres/data/brokers/__init__.py +1 -0
  413. sobres-1.1.0/src/sobres/data/brokers/alpaca.py +340 -0
  414. sobres-1.1.0/src/sobres/data/brokers/base.py +174 -0
  415. sobres-1.1.0/src/sobres/data/brokers/fake.py +218 -0
  416. sobres-1.1.0/src/sobres/data/cache.py +213 -0
  417. sobres-1.1.0/src/sobres/data/currency.py +277 -0
  418. sobres-1.1.0/src/sobres/data/ecb_provider.py +157 -0
  419. sobres-1.1.0/src/sobres/data/fixtures.py +355 -0
  420. sobres-1.1.0/src/sobres/data/fred_provider.py +196 -0
  421. sobres-1.1.0/src/sobres/data/gaps.py +83 -0
  422. sobres-1.1.0/src/sobres/data/ken_french.py +212 -0
  423. sobres-1.1.0/src/sobres/data/ppp_provider.py +346 -0
  424. sobres-1.1.0/src/sobres/data/providers.py +77 -0
  425. sobres-1.1.0/src/sobres/data/storage/__init__.py +37 -0
  426. sobres-1.1.0/src/sobres/data/storage/adapters/__init__.py +8 -0
  427. sobres-1.1.0/src/sobres/data/storage/adapters/migrations.py +61 -0
  428. sobres-1.1.0/src/sobres/data/storage/adapters/schema.py +257 -0
  429. sobres-1.1.0/src/sobres/data/storage/adapters/sqlite.py +1233 -0
  430. sobres-1.1.0/src/sobres/data/storage/base.py +507 -0
  431. sobres-1.1.0/src/sobres/data/yfinance_provider.py +319 -0
  432. sobres-1.1.0/src/sobres/deploy.py +304 -0
  433. sobres-1.1.0/src/sobres/doctor.py +532 -0
  434. sobres-1.1.0/src/sobres/observability/__init__.py +9 -0
  435. sobres-1.1.0/src/sobres/observability/logging.py +152 -0
  436. sobres-1.1.0/src/sobres/observability/redaction.py +64 -0
  437. sobres-1.1.0/src/sobres/observability/tracing.py +224 -0
  438. sobres-1.1.0/src/sobres/py.typed +0 -0
  439. sobres-1.1.0/src/sobres/registry.py +583 -0
  440. sobres-1.1.0/src/sobres/results.py +215 -0
  441. sobres-1.1.0/src/sobres/settings.py +409 -0
  442. sobres-1.1.0/src/sobres/solvers/__init__.py +1 -0
  443. sobres-1.1.0/src/sobres/solvers/base.py +38 -0
  444. sobres-1.1.0/src/sobres/solvers/scipy.py +35 -0
  445. sobres-1.1.0/tests/__init__.py +0 -0
  446. sobres-1.1.0/tests/api/__init__.py +0 -0
  447. sobres-1.1.0/tests/api/conftest.py +43 -0
  448. sobres-1.1.0/tests/api/test_api.py +469 -0
  449. sobres-1.1.0/tests/api/test_parity.py +164 -0
  450. sobres-1.1.0/tests/architecture/__init__.py +0 -0
  451. sobres-1.1.0/tests/architecture/test_deployment_files.py +97 -0
  452. sobres-1.1.0/tests/architecture/test_encoding.py +83 -0
  453. sobres-1.1.0/tests/architecture/test_layering.py +212 -0
  454. sobres-1.1.0/tests/architecture/test_scenarios.py +65 -0
  455. sobres-1.1.0/tests/architecture/test_testing_spec.py +81 -0
  456. sobres-1.1.0/tests/cli/__init__.py +0 -0
  457. sobres-1.1.0/tests/cli/test_analyze.py +154 -0
  458. sobres-1.1.0/tests/cli/test_cache_commands.py +47 -0
  459. sobres-1.1.0/tests/cli/test_config_commands.py +59 -0
  460. sobres-1.1.0/tests/cli/test_data_commands.py +137 -0
  461. sobres-1.1.0/tests/cli/test_deploy.py +380 -0
  462. sobres-1.1.0/tests/cli/test_disclaimer.py +43 -0
  463. sobres-1.1.0/tests/cli/test_doctor.py +341 -0
  464. sobres-1.1.0/tests/cli/test_econ.py +384 -0
  465. sobres-1.1.0/tests/cli/test_fx.py +356 -0
  466. sobres-1.1.0/tests/cli/test_init.py +309 -0
  467. sobres-1.1.0/tests/cli/test_main.py +129 -0
  468. sobres-1.1.0/tests/cli/test_optimization_review.py +198 -0
  469. sobres-1.1.0/tests/cli/test_optimize.py +347 -0
  470. sobres-1.1.0/tests/cli/test_persistence.py +271 -0
  471. sobres-1.1.0/tests/cli/test_plan.py +470 -0
  472. sobres-1.1.0/tests/cli/test_ppp.py +232 -0
  473. sobres-1.1.0/tests/cli/test_registry.py +333 -0
  474. sobres-1.1.0/tests/cli/test_serve.py +187 -0
  475. sobres-1.1.0/tests/cli/test_settings.py +113 -0
  476. sobres-1.1.0/tests/cli/test_trade.py +348 -0
  477. sobres-1.1.0/tests/cli/test_upgrade.py +64 -0
  478. sobres-1.1.0/tests/cli/test_usage_examples.py +26 -0
  479. sobres-1.1.0/tests/cli/test_window.py +55 -0
  480. sobres-1.1.0/tests/conftest.py +135 -0
  481. sobres-1.1.0/tests/core/__init__.py +0 -0
  482. sobres-1.1.0/tests/core/test_allocate.py +52 -0
  483. sobres-1.1.0/tests/core/test_backtest.py +168 -0
  484. sobres-1.1.0/tests/core/test_conventions.py +35 -0
  485. sobres-1.1.0/tests/core/test_factors.py +183 -0
  486. sobres-1.1.0/tests/core/test_forecast.py +318 -0
  487. sobres-1.1.0/tests/core/test_fx.py +103 -0
  488. sobres-1.1.0/tests/core/test_goals.py +216 -0
  489. sobres-1.1.0/tests/core/test_moments.py +117 -0
  490. sobres-1.1.0/tests/core/test_optimize.py +274 -0
  491. sobres-1.1.0/tests/core/test_ppp.py +135 -0
  492. sobres-1.1.0/tests/core/test_psd.py +58 -0
  493. sobres-1.1.0/tests/core/test_rates.py +25 -0
  494. sobres-1.1.0/tests/core/test_regression.py +87 -0
  495. sobres-1.1.0/tests/core/test_returns.py +88 -0
  496. sobres-1.1.0/tests/core/test_review_corrections.py +146 -0
  497. sobres-1.1.0/tests/core/test_risk.py +125 -0
  498. sobres-1.1.0/tests/core/test_simulate.py +129 -0
  499. sobres-1.1.0/tests/core/test_textbook_cases.py +58 -0
  500. sobres-1.1.0/tests/core/test_timeseries.py +147 -0
  501. sobres-1.1.0/tests/core/test_trading.py +167 -0
  502. sobres-1.1.0/tests/data/__init__.py +0 -0
  503. sobres-1.1.0/tests/data/broker_conformance.py +98 -0
  504. sobres-1.1.0/tests/data/contracts.py +60 -0
  505. sobres-1.1.0/tests/data/storage_conformance.py +390 -0
  506. sobres-1.1.0/tests/data/test_align.py +74 -0
  507. sobres-1.1.0/tests/data/test_base.py +122 -0
  508. sobres-1.1.0/tests/data/test_brokers.py +252 -0
  509. sobres-1.1.0/tests/data/test_cache.py +220 -0
  510. sobres-1.1.0/tests/data/test_currency.py +220 -0
  511. sobres-1.1.0/tests/data/test_fixture_recording.py +99 -0
  512. sobres-1.1.0/tests/data/test_fred.py +188 -0
  513. sobres-1.1.0/tests/data/test_fx.py +111 -0
  514. sobres-1.1.0/tests/data/test_gaps.py +78 -0
  515. sobres-1.1.0/tests/data/test_ken_french.py +155 -0
  516. sobres-1.1.0/tests/data/test_migrations.py +85 -0
  517. sobres-1.1.0/tests/data/test_portability.py +71 -0
  518. sobres-1.1.0/tests/data/test_ppp_provider.py +81 -0
  519. sobres-1.1.0/tests/data/test_storage_conformance.py +89 -0
  520. sobres-1.1.0/tests/data/test_storage_port.py +224 -0
  521. sobres-1.1.0/tests/data/test_yfinance.py +236 -0
  522. sobres-1.1.0/tests/fixtures/README.md +95 -0
  523. sobres-1.1.0/tests/fixtures/bis/GB.csv +121 -0
  524. sobres-1.1.0/tests/fixtures/bis/JP.csv +121 -0
  525. sobres-1.1.0/tests/fixtures/bis/MX.csv +121 -0
  526. sobres-1.1.0/tests/fixtures/bis/US.csv +121 -0
  527. sobres-1.1.0/tests/fixtures/bis/XM.csv +121 -0
  528. sobres-1.1.0/tests/fixtures/bis/meta.json +7 -0
  529. sobres-1.1.0/tests/fixtures/ecb/CHF.csv +2562 -0
  530. sobres-1.1.0/tests/fixtures/ecb/GBP.csv +2562 -0
  531. sobres-1.1.0/tests/fixtures/ecb/JPY.csv +2562 -0
  532. sobres-1.1.0/tests/fixtures/ecb/USD.csv +2562 -0
  533. sobres-1.1.0/tests/fixtures/ecb/meta.json +17 -0
  534. sobres-1.1.0/tests/fixtures/fire_calculator/golden.json +26 -0
  535. sobres-1.1.0/tests/fixtures/fred/CPIAUCSL.json +736 -0
  536. sobres-1.1.0/tests/fixtures/fred/DEXUSEU.json +15664 -0
  537. sobres-1.1.0/tests/fixtures/fred/DGS10.json +15664 -0
  538. sobres-1.1.0/tests/fixtures/fred/DTB3.json +15664 -0
  539. sobres-1.1.0/tests/fixtures/fred/meta.json +17 -0
  540. sobres-1.1.0/tests/fixtures/ken_french/F-F_Momentum_Factor.CSV +1314 -0
  541. sobres-1.1.0/tests/fixtures/ken_french/F-F_Momentum_Factor_daily.CSV +26211 -0
  542. sobres-1.1.0/tests/fixtures/ken_french/F-F_Research_Data_5_Factors_2x3.CSV +829 -0
  543. sobres-1.1.0/tests/fixtures/ken_french/F-F_Research_Data_5_Factors_2x3_daily.CSV +15883 -0
  544. sobres-1.1.0/tests/fixtures/ken_french/F-F_Research_Data_Factors.CSV +1310 -0
  545. sobres-1.1.0/tests/fixtures/ken_french/F-F_Research_Data_Factors_daily.CSV +26303 -0
  546. sobres-1.1.0/tests/fixtures/ken_french/meta.json +16 -0
  547. sobres-1.1.0/tests/fixtures/oecd/AUS.csv +15 -0
  548. sobres-1.1.0/tests/fixtures/oecd/CAN.csv +15 -0
  549. sobres-1.1.0/tests/fixtures/oecd/CHE.csv +15 -0
  550. sobres-1.1.0/tests/fixtures/oecd/DEU.csv +15 -0
  551. sobres-1.1.0/tests/fixtures/oecd/ESP.csv +15 -0
  552. sobres-1.1.0/tests/fixtures/oecd/GBR.csv +15 -0
  553. sobres-1.1.0/tests/fixtures/oecd/JPN.csv +15 -0
  554. sobres-1.1.0/tests/fixtures/oecd/MEX.csv +15 -0
  555. sobres-1.1.0/tests/fixtures/oecd/PRT.csv +15 -0
  556. sobres-1.1.0/tests/fixtures/oecd/USA.csv +15 -0
  557. sobres-1.1.0/tests/fixtures/oecd/meta.json +7 -0
  558. sobres-1.1.0/tests/fixtures/r_reference/allocation.json +42 -0
  559. sobres-1.1.0/tests/fixtures/schema/v1.sql +48 -0
  560. sobres-1.1.0/tests/fixtures/schema/v2.sql +97 -0
  561. sobres-1.1.0/tests/fixtures/synthetic/alpaca/account.json +12 -0
  562. sobres-1.1.0/tests/fixtures/synthetic/alpaca/meta.json +7 -0
  563. sobres-1.1.0/tests/fixtures/synthetic/alpaca/positions.json +1 -0
  564. sobres-1.1.0/tests/fixtures/synthetic/alpaca/quotes.json +38 -0
  565. sobres-1.1.0/tests/fixtures/synthetic/fred/GBRCPIALLMINMEI.json +736 -0
  566. sobres-1.1.0/tests/fixtures/synthetic/fred/IR3TIB01GBM156N.json +736 -0
  567. sobres-1.1.0/tests/fixtures/synthetic/yfinance/SPY.csv +2517 -0
  568. sobres-1.1.0/tests/fixtures/synthetic/yfinance/fundamentals.json +68 -0
  569. sobres-1.1.0/tests/fixtures/synthetic/yfinance/meta.json +14 -0
  570. sobres-1.1.0/tests/fixtures/worldbank/AUS.json +252 -0
  571. sobres-1.1.0/tests/fixtures/worldbank/BRA.json +252 -0
  572. sobres-1.1.0/tests/fixtures/worldbank/CAN.json +252 -0
  573. sobres-1.1.0/tests/fixtures/worldbank/CHE.json +252 -0
  574. sobres-1.1.0/tests/fixtures/worldbank/DEU.json +252 -0
  575. sobres-1.1.0/tests/fixtures/worldbank/EMU.json +252 -0
  576. sobres-1.1.0/tests/fixtures/worldbank/ESP.json +252 -0
  577. sobres-1.1.0/tests/fixtures/worldbank/GBR.json +252 -0
  578. sobres-1.1.0/tests/fixtures/worldbank/IND.json +252 -0
  579. sobres-1.1.0/tests/fixtures/worldbank/JPN.json +252 -0
  580. sobres-1.1.0/tests/fixtures/worldbank/MEX.json +252 -0
  581. sobres-1.1.0/tests/fixtures/worldbank/PRT.json +252 -0
  582. sobres-1.1.0/tests/fixtures/worldbank/USA.json +252 -0
  583. sobres-1.1.0/tests/fixtures/worldbank/meta.json +7 -0
  584. sobres-1.1.0/tests/fixtures/yfinance/AAPL.csv +2517 -0
  585. sobres-1.1.0/tests/fixtures/yfinance/GLD.csv +2517 -0
  586. sobres-1.1.0/tests/fixtures/yfinance/JNJ.csv +2517 -0
  587. sobres-1.1.0/tests/fixtures/yfinance/MSFT.csv +2517 -0
  588. sobres-1.1.0/tests/fixtures/yfinance/NVDA.csv +2517 -0
  589. sobres-1.1.0/tests/fixtures/yfinance/VOD.L.csv +2527 -0
  590. sobres-1.1.0/tests/fixtures/yfinance/XOM.csv +2517 -0
  591. sobres-1.1.0/tests/fixtures/yfinance/meta.json +57 -0
  592. sobres-1.1.0/tests/invariants/__init__.py +0 -0
  593. sobres-1.1.0/tests/invariants/test_every_command.py +693 -0
  594. sobres-1.1.0/tests/network/__init__.py +0 -0
  595. sobres-1.1.0/tests/network/test_live_providers.py +78 -0
  596. sobres-1.1.0/tests/test_cli_scaffold.py +30 -0
  597. sobres-1.1.0/tests/test_config.py +113 -0
  598. sobres-1.1.0/tests/test_errors.py +77 -0
  599. sobres-1.1.0/tests/test_logging.py +132 -0
  600. sobres-1.1.0/tests/test_observability_invariants.py +23 -0
  601. sobres-1.1.0/tests/test_packaging.py +96 -0
  602. sobres-1.1.0/tests/test_rebrand.py +49 -0
  603. sobres-1.1.0/tests/test_redaction.py +118 -0
  604. sobres-1.1.0/tests/test_release_script.py +117 -0
  605. sobres-1.1.0/tests/test_render.py +132 -0
  606. sobres-1.1.0/tests/test_settings_tier.py +11 -0
  607. sobres-1.1.0/tests/test_tracing.py +113 -0
  608. sobres-1.1.0/tests/ui/__init__.py +0 -0
  609. sobres-1.1.0/tests/ui/test_frontend_source.py +244 -0
  610. sobres-1.1.0/tests/ui/test_site_source.py +252 -0
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code PreToolUse hook: before any `git commit` runs from a Bash tool call, run the
3
+ # same gate CI runs (ruff check, ruff format --check, mypy --strict, actionlint). Exit 2 blocks the
4
+ # commit and hands the output back to the agent, so a red PR is caught before it exists.
5
+ #
6
+ # Reads the tool call as JSON on stdin; only acts when the command contains "git commit".
7
+ set -uo pipefail
8
+ payload=$(cat)
9
+ command=$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input", {}).get("command", ""))' 2>/dev/null || true)
10
+ case "$command" in
11
+ *"git commit"*) ;;
12
+ *) exit 0 ;;
13
+ esac
14
+ root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
15
+ cd "$root" || exit 0
16
+ fail=0
17
+ run() {
18
+ local name=$1; shift
19
+ if ! output=$("$@" 2>&1); then
20
+ printf '\n[commit gate] %s failed:\n%s\n' "$name" "$output" >&2
21
+ fail=1
22
+ fi
23
+ }
24
+ run "ruff check" ruff check .
25
+ run "ruff format --check" ruff format --check .
26
+ run "mypy --strict" python -m mypy
27
+ if command -v pre-commit >/dev/null 2>&1; then
28
+ run "actionlint" pre-commit run actionlint --all-files
29
+ fi
30
+ if [ "$fail" -ne 0 ]; then
31
+ printf '\n[commit gate] fix the findings above (ruff format . fixes formatting), then commit again.\n' >&2
32
+ exit 2
33
+ fi
34
+ exit 0
@@ -0,0 +1,16 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/commit-gate.sh",
10
+ "timeout": 600
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,24 @@
1
+ # Only sources reach the build; nothing from a developer's machine does.
2
+ .git
3
+ .github
4
+ .venv
5
+ venv
6
+ **/__pycache__
7
+ **/*.pyc
8
+ .pytest_cache
9
+ .ruff_cache
10
+ .mypy_cache
11
+ .coverage
12
+ htmlcov
13
+ dist
14
+ build
15
+ *.egg-info
16
+ frontend/node_modules
17
+ src/sobres/api/static
18
+ legacy_code
19
+ openspec
20
+ docs
21
+ site
22
+ tests
23
+ .env
24
+ *.toml.bak
@@ -0,0 +1,2 @@
1
+ # Recorded payload digests cover exact bytes, including vendor line endings.
2
+ tests/fixtures/** -text
@@ -0,0 +1,38 @@
1
+ name: Hosting inquiry
2
+ description: Ask about running sobres for you. This issue is public; do not include anything confidential.
3
+ title: "Hosting inquiry: "
4
+ labels: ["hosting-inquiry"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ **This issue is public.** Anyone can read it.
10
+ Do not paste API keys, account numbers, statements, holdings, or anything else you would not publish.
11
+
12
+ sobres is a single-user research tool. An inquiry is a conversation about whether
13
+ running it for you makes sense; it is not an order, a service-level agreement,
14
+ a payment contract, or a multi-tenant product, and it is not investment advice.
15
+ - type: dropdown
16
+ id: shape
17
+ attributes:
18
+ label: What are you looking for?
19
+ options:
20
+ - Help running the container myself
21
+ - Someone to run it for me
22
+ - Something else (describe below)
23
+ validations:
24
+ required: true
25
+ - type: textarea
26
+ id: context
27
+ attributes:
28
+ label: Context
29
+ description: Where would it run, for how many people, and what would you use it for? Keep it general.
30
+ validations:
31
+ required: true
32
+ - type: checkboxes
33
+ id: confirm
34
+ attributes:
35
+ label: Confirm
36
+ options:
37
+ - label: I understand this issue is public and I have not included credentials, financial records or other confidential information.
38
+ required: true
@@ -0,0 +1,27 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: "/"
5
+ schedule:
6
+ interval: weekly
7
+ commit-message:
8
+ prefix: "ci"
9
+ groups:
10
+ # One PR per week for routine action bumps rather than five.
11
+ actions:
12
+ patterns: ["*"]
13
+
14
+ - package-ecosystem: pip
15
+ directory: "/"
16
+ schedule:
17
+ interval: weekly
18
+ commit-message:
19
+ prefix: "deps"
20
+ groups:
21
+ dev-dependencies:
22
+ dependency-type: development
23
+ patterns: ["*"]
24
+ # Runtime dependency majors are a deliberate decision, not a weekly chore.
25
+ ignore:
26
+ - dependency-name: "*"
27
+ update-types: ["version-update:semver-major"]
@@ -0,0 +1,26 @@
1
+ ## What
2
+
3
+ <!-- One or two sentences. What does this change do? -->
4
+
5
+ ## OpenSpec change
6
+
7
+ <!-- Which change under openspec/changes/ does this implement? Which tasks? -->
8
+ - Change: `openspec/changes/____`
9
+ - Tasks completed: <!-- e.g. B1, B2, B3 -->
10
+
11
+ ## Scenarios covered
12
+
13
+ <!-- List the spec scenarios this PR implements, and the test proving each. -->
14
+ | Scenario | Test |
15
+ |---|---|
16
+ | | |
17
+
18
+ ## Checklist
19
+
20
+ - [ ] Every new behavior has a scenario in a spec delta
21
+ - [ ] Every scenario has a test that references it
22
+ - [ ] `pytest -m "not network"` passes offline
23
+ - [ ] `ruff check`, `ruff format --check`, and `mypy` are clean
24
+ - [ ] No business logic added to `cli/`; no I/O added to `core/`
25
+ - [ ] No bare periods-per-year literals (use `core/conventions.py`)
26
+ - [ ] Task boxes checked off in the change's `tasks.md`
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+ """Decide whether the current commit should be published.
3
+
4
+ PyPI versions are immutable: re-uploading an existing version is a hard error,
5
+ not an overwrite. So "publish on every push to main" can only mean "publish when
6
+ the version declared in ``__about__.py`` is not yet on the index".
7
+
8
+ Writes ``version``, ``target``, ``publish`` and ``reason`` as ``KEY=value`` lines on stdout,
9
+ which the workflow appends to ``$GITHUB_OUTPUT``.
10
+
11
+ stdout is therefore a machine interface: it carries ONLY ``key=value`` lines.
12
+ Every human-facing message goes to stderr. ``$GITHUB_OUTPUT`` rejects any line
13
+ that is not ``key=value``, so a stray informational print on stdout fails the
14
+ whole job -- which is exactly what happened on the first push to ``main``.
15
+ ``tests/test_release_script.py`` enforces the contract.
16
+
17
+ Run locally to see what the next push to main would do::
18
+
19
+ python .github/scripts/check_release.py
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import os
26
+ import re
27
+ import sys
28
+ import tomllib
29
+ import urllib.error
30
+ import urllib.request
31
+ from pathlib import Path
32
+
33
+ REPO_ROOT = Path(__file__).resolve().parents[2]
34
+ ABOUT = REPO_ROOT / "src" / "sobres" / "__about__.py"
35
+ PYPROJECT = REPO_ROOT / "pyproject.toml"
36
+ CHANGELOG = REPO_ROOT / "CHANGELOG.md"
37
+
38
+ INDEXES = {
39
+ "pypi": "https://pypi.org/pypi/{name}/json",
40
+ "testpypi": "https://test.pypi.org/pypi/{name}/json",
41
+ }
42
+
43
+ # PEP 440, restricted to the subset this project actually uses.
44
+ VERSION_RE = re.compile(r"^\d+\.\d+\.\d+(?:(?:a|b|rc)\d+)?(?:\.dev\d+)?$")
45
+
46
+
47
+ def note(message: str) -> None:
48
+ """Human-facing progress line. stderr, never stdout -- stdout is $GITHUB_OUTPUT."""
49
+ print(message, file=sys.stderr)
50
+
51
+
52
+ def emit(key: str, value: str) -> None:
53
+ """The only writer to stdout: one ``key=value`` line for $GITHUB_OUTPUT."""
54
+ print(f"{key}={value}")
55
+
56
+
57
+ def fail(message: str) -> None:
58
+ """Emit a GitHub Actions error annotation and exit non-zero."""
59
+ print(f"::error::{message}", file=sys.stderr)
60
+ raise SystemExit(1)
61
+
62
+
63
+ def read_version() -> str:
64
+ match = re.search(
65
+ r'^__version__\s*=\s*"([^"]+)"', ABOUT.read_text(encoding="utf-8"), re.MULTILINE
66
+ )
67
+ if not match:
68
+ fail(f"Could not find __version__ in {ABOUT.relative_to(REPO_ROOT)}")
69
+ raise AssertionError("unreachable") # pragma: no cover
70
+ return match.group(1)
71
+
72
+
73
+ def read_dist_name() -> str:
74
+ return str(tomllib.loads(PYPROJECT.read_text(encoding="utf-8"))["project"]["name"])
75
+
76
+
77
+ def released_versions(dist_name: str, target: str) -> set[str] | None:
78
+ """Return every version on the index, or ``None`` if the project is new."""
79
+ url = INDEXES[target].format(name=dist_name)
80
+ request = urllib.request.Request(url, headers={"Accept": "application/json"})
81
+ try:
82
+ with urllib.request.urlopen(request, timeout=30) as response:
83
+ return set(json.load(response)["releases"])
84
+ except urllib.error.HTTPError as exc:
85
+ if exc.code == 404:
86
+ # First ever release: the project does not exist on the index yet.
87
+ return None
88
+ fail(f"{target} returned HTTP {exc.code} for {dist_name}")
89
+ except urllib.error.URLError as exc:
90
+ # A network blip must not silently look like "nothing is published",
91
+ # which would re-publish an existing version and fail at upload.
92
+ fail(f"Could not reach {target}: {exc.reason}")
93
+ raise AssertionError("unreachable") # pragma: no cover
94
+
95
+
96
+ def main() -> None:
97
+ target = os.environ.get("TARGET", "pypi")
98
+ if target not in INDEXES:
99
+ fail(f"TARGET must be one of {sorted(INDEXES)}, got {target!r}")
100
+
101
+ version = read_version()
102
+ if not VERSION_RE.match(version):
103
+ fail(
104
+ f"Version {version!r} is not a valid release version "
105
+ f"(expected e.g. 1.2.3, 1.2.3rc1 or 1.2.3.dev0)"
106
+ )
107
+
108
+ dist_name = read_dist_name()
109
+ existing = released_versions(dist_name, target)
110
+
111
+ if existing is None:
112
+ note(f"{dist_name} is not on {target} yet -- this would be the first release.")
113
+ publish = True
114
+ elif version in existing:
115
+ note(f"{dist_name} {version} is already on {target}; nothing to publish.")
116
+ publish = False
117
+ else:
118
+ note(f"{dist_name} {version} is not on {target}; it will be published.")
119
+ publish = True
120
+
121
+ # Release discipline: a version that ships must be described. Checked only
122
+ # when actually publishing, so ordinary pushes are never blocked by it.
123
+ if publish and CHANGELOG.exists():
124
+ changelog = CHANGELOG.read_text(encoding="utf-8")
125
+ if f"## [{version}]" not in changelog and f"## {version}" not in changelog:
126
+ fail(
127
+ f"CHANGELOG.md has no section for {version}. "
128
+ f"Add a '## [{version}]' heading describing the release."
129
+ )
130
+
131
+ emit("version", version)
132
+ emit("target", target)
133
+ emit("publish", str(publish).lower())
134
+ emit("reason", "new-version" if publish else "already-published")
135
+
136
+
137
+ if __name__ == "__main__":
138
+ main()
@@ -0,0 +1,353 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+ # Callable by release.yml so a release runs the exact same gates, not a copy
8
+ # of them that can drift.
9
+ workflow_call:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ concurrency:
15
+ group: ci-${{ github.workflow }}-${{ github.ref }}
16
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
17
+
18
+ env:
19
+ FORCE_COLOR: "1"
20
+ PIP_DISABLE_PIP_VERSION_CHECK: "1"
21
+
22
+ jobs:
23
+ lint:
24
+ name: Lint and format
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v7
28
+ - uses: actions/setup-python@v7
29
+ with:
30
+ python-version: "3.12"
31
+ cache: pip
32
+ cache-dependency-path: pyproject.toml
33
+ - run: pip install -e ".[dev]"
34
+ - name: ruff check
35
+ run: ruff check --output-format=github .
36
+ - name: ruff format
37
+ run: ruff format --check --diff .
38
+
39
+ - name: Lint GitHub Actions workflows
40
+ # These workflows are code, and they fail in a way no unit test can
41
+ # catch: an invalid expression is rejected before any job starts, so the
42
+ # run reports failure with zero jobs and no log to read.
43
+ # Pinned release download rather than a third-party action or a
44
+ # curl-piped-to-bash installer.
45
+ env:
46
+ ACTIONLINT_VERSION: "1.7.7"
47
+ run: |
48
+ curl -sSfL -o actionlint.tar.gz \
49
+ "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
50
+ tar xzf actionlint.tar.gz actionlint
51
+ ./actionlint -color
52
+
53
+ typecheck:
54
+ name: Type check
55
+ runs-on: ubuntu-latest
56
+ steps:
57
+ - uses: actions/checkout@v7
58
+ - uses: actions/setup-python@v7
59
+ with:
60
+ python-version: "3.12"
61
+ cache: pip
62
+ cache-dependency-path: pyproject.toml
63
+ - run: pip install -e ".[dev]"
64
+ - name: mypy --strict
65
+ run: mypy
66
+
67
+ test:
68
+ name: Test ${{ matrix.python-version }} on ${{ matrix.os }}
69
+ runs-on: ${{ matrix.os }}
70
+ strategy:
71
+ fail-fast: false
72
+ matrix:
73
+ os: [ubuntu-latest]
74
+ python-version: ["3.11", "3.12", "3.13"]
75
+ include:
76
+ # Cross-platform coverage on the primary interpreter only. Path
77
+ # handling and the on-disk cache are the parts that break on Windows.
78
+ - os: macos-latest
79
+ python-version: "3.12"
80
+ - os: windows-latest
81
+ python-version: "3.12"
82
+ steps:
83
+ - uses: actions/checkout@v7
84
+ - uses: actions/setup-python@v7
85
+ with:
86
+ python-version: ${{ matrix.python-version }}
87
+ cache: pip
88
+ cache-dependency-path: pyproject.toml
89
+ - run: pip install -e ".[dev]"
90
+ - name: pytest
91
+ # Network-marked tests hit live providers; a vendor outage must never
92
+ # turn this repo red. Run them deliberately with `pytest -m network`.
93
+ run: pytest -m "not network" --cov --cov-report=xml --cov-report=term-missing --cov-fail-under=90
94
+
95
+ - name: Upload coverage
96
+ if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
97
+ uses: actions/upload-artifact@v7
98
+ with:
99
+ name: coverage-xml
100
+ path: coverage.xml
101
+ if-no-files-found: error
102
+
103
+ frontend:
104
+ name: Frontend (types, client drift, bundle budget)
105
+ runs-on: ubuntu-latest
106
+ steps:
107
+ - uses: actions/checkout@v7
108
+ - uses: actions/setup-python@v7
109
+ with:
110
+ python-version: "3.12"
111
+ cache: pip
112
+ cache-dependency-path: pyproject.toml
113
+ - uses: actions/setup-node@v4
114
+ with:
115
+ node-version: "22"
116
+ cache: npm
117
+ cache-dependency-path: frontend/package-lock.json
118
+ - run: pip install -e ".[dev]"
119
+ - name: The committed OpenAPI document matches the registry
120
+ # The TypeScript client is generated from this file; drift is a build
121
+ # failure here rather than a runtime one in the browser.
122
+ run: python scripts/export_openapi.py --check
123
+ - run: npm ci
124
+ working-directory: frontend
125
+ - name: The generated client matches the OpenAPI document
126
+ run: npm run check:client
127
+ working-directory: frontend
128
+ - name: Type check, build, bundle budget
129
+ # `build` runs tsc --strict, vite, then the 300 KB gzip gate.
130
+ run: npm run build
131
+ working-directory: frontend
132
+ - name: Upload the built SPA
133
+ uses: actions/upload-artifact@v7
134
+ with:
135
+ name: frontend-static
136
+ path: src/sobres/api/static/
137
+ if-no-files-found: error
138
+
139
+ build:
140
+ name: Build and verify distributions
141
+ needs: [frontend]
142
+ runs-on: ubuntu-latest
143
+ steps:
144
+ - uses: actions/checkout@v7
145
+ with:
146
+ # setuptools-scm-style tooling and changelog checks want real history.
147
+ fetch-depth: 0
148
+ - uses: actions/setup-python@v7
149
+ with:
150
+ python-version: "3.12"
151
+ cache: pip
152
+ cache-dependency-path: pyproject.toml
153
+
154
+ - name: Fetch the built SPA
155
+ # The wheel ships the frontend at sobres/api/static so `pip install
156
+ # sobres[web]` needs no Node.
157
+ uses: actions/download-artifact@v8
158
+ with:
159
+ name: frontend-static
160
+ path: src/sobres/api/static/
161
+
162
+ - run: pip install build twine
163
+ - name: Build sdist and wheel
164
+ run: python -m build
165
+
166
+ - name: The wheel carries the SPA
167
+ run: unzip -l dist/*.whl | grep -q "sobres/api/static/index.html"
168
+
169
+ - name: twine check
170
+ run: twine check --strict dist/*
171
+
172
+ - name: Install the base wheel, no extras, in a clean environment
173
+ # `pip install sobres` with no extras is what most users get, and until
174
+ # now nothing tested it: every other job installs `.[dev]` and the job
175
+ # below installs `[web]`. Registering commands imports every module under
176
+ # cli/commands/, so a single module-scope import of an optional
177
+ # dependency breaks the whole CLI, `--version` included. That shipped.
178
+ env:
179
+ SOBRES_CONFIG_FILE: /tmp/base-home/config.toml
180
+ SOBRES_DB_URL: sqlite:////tmp/base-home/sobres.db
181
+ SOBRES_FIXTURE_DIR: ${{ github.workspace }}/tests/fixtures
182
+ run: |
183
+ python -m venv /tmp/smoke-base
184
+ /tmp/smoke-base/bin/pip install --upgrade pip
185
+ /tmp/smoke-base/bin/pip install "$(ls dist/*.whl)"
186
+ # The check is only meaningful if the extra is genuinely absent.
187
+ if /tmp/smoke-base/bin/python -c "import fastapi" 2>/dev/null; then
188
+ echo "::error::fastapi is installed in the base environment; this job proves nothing"
189
+ exit 1
190
+ fi
191
+ /tmp/smoke-base/bin/sobres --version
192
+ /tmp/smoke-base/bin/sobres --help > /dev/null
193
+ /tmp/smoke-base/bin/sobres init --non-interactive --offline
194
+ /tmp/smoke-base/bin/sobres doctor --offline
195
+ /tmp/smoke-base/bin/sobres data prices AAPL MSFT --start 2020-01-01 --end 2020-03-31 \
196
+ --format csv | head -3
197
+
198
+ - name: Install the built wheel in a clean environment
199
+ # Proves the artifact users actually download works -- catches missing
200
+ # package data, a broken entry point, or an import that only resolves
201
+ # from the source tree.
202
+ run: |
203
+ python -m venv /tmp/smoke
204
+ /tmp/smoke/bin/pip install --upgrade pip
205
+ /tmp/smoke/bin/pip install "$(ls dist/*.whl)[web]"
206
+ /tmp/smoke/bin/sobres --version
207
+ /tmp/smoke/bin/sobres serve --help > /dev/null
208
+ /tmp/smoke/bin/python -c "import sobres; print(sobres.__version__)"
209
+ /tmp/smoke/bin/python -c "import yfinance; assert callable(yfinance.Ticker)"
210
+
211
+ - name: The three-command path, in the clean environment
212
+ # Onboarding is tested end to end: init, doctor, then one data command
213
+ # served from the recorded fixtures rather than the network.
214
+ env:
215
+ SOBRES_CONFIG_FILE: /tmp/smoke-home/config.toml
216
+ SOBRES_DB_URL: sqlite:////tmp/smoke-home/sobres.db
217
+ SOBRES_FIXTURE_DIR: ${{ github.workspace }}/tests/fixtures
218
+ run: |
219
+ /tmp/smoke/bin/sobres init --non-interactive --offline
220
+ /tmp/smoke/bin/sobres doctor --offline
221
+ /tmp/smoke/bin/sobres data prices AAPL MSFT --start 2020-01-01 --end 2020-03-31 --format csv \
222
+ | head -5
223
+ /tmp/smoke/bin/sobres data prices AAPL MSFT --start 2020-01-01 --end 2020-03-31 --format json \
224
+ | /tmp/smoke/bin/python -c "import json,sys; d=json.load(sys.stdin); assert d['columns']==['AAPL','MSFT'], d['columns']; print(len(d['rows']), 'rows')"
225
+
226
+ - name: Confirm the sdist builds a wheel
227
+ run: |
228
+ python -m venv /tmp/sdist
229
+ /tmp/sdist/bin/pip install --upgrade pip build
230
+ /tmp/sdist/bin/pip wheel --no-deps -w /tmp/sdist-out dist/*.tar.gz
231
+
232
+ - name: Upload distributions
233
+ uses: actions/upload-artifact@v7
234
+ with:
235
+ name: dist
236
+ path: dist/
237
+ if-no-files-found: error
238
+
239
+ docker:
240
+ name: Container image (quickstart, size budget)
241
+ runs-on: ubuntu-latest
242
+ steps:
243
+ - uses: actions/checkout@v7
244
+ - uses: docker/setup-buildx-action@v3
245
+ - name: Build the image from this checkout
246
+ uses: docker/build-push-action@v6
247
+ with:
248
+ context: .
249
+ load: true
250
+ tags: sobres:ci
251
+ cache-from: type=gha
252
+ cache-to: type=gha,mode=max
253
+
254
+ - name: The CLI is the entrypoint, as a non-root user
255
+ run: |
256
+ docker run --rm sobres:ci --help | grep -q "Usage: sobres"
257
+ docker run --rm sobres:ci --version
258
+ test "$(docker run --rm --entrypoint id sobres:ci -u)" = "1000"
259
+ test "$(docker run --rm --entrypoint id sobres:ci -g)" = "1000"
260
+
261
+ - name: A missing data volume fails loudly
262
+ run: |
263
+ mkdir -p /tmp/ro && chmod 555 /tmp/ro
264
+ set +e
265
+ out=$(docker run --rm -v /tmp/ro:/data:ro sobres:ci db info 2>&1)
266
+ code=$?
267
+ set -e
268
+ echo "$out"
269
+ test "$code" -ne 0
270
+ echo "$out" | grep -q "/data"
271
+
272
+ - name: The documented quickstart, end to end
273
+ run: |
274
+ docker volume create sobres-ci
275
+ docker run -d --name sobres-ci -p 8787:8787 -v sobres-ci:/data sobres:ci serve --host 0.0.0.0
276
+ for _ in $(seq 1 30); do
277
+ if curl -fsS http://127.0.0.1:8787/api/v1/health > /tmp/health.json; then break; fi
278
+ sleep 1
279
+ done
280
+ cat /tmp/health.json
281
+ python3 -c "import json; d=json.load(open('/tmp/health.json')); assert d['app']=='sobres' and d['ready'], d"
282
+ curl -fsS http://127.0.0.1:8787/ | grep -qi "<div id=\"root\""
283
+ docker exec sobres-ci sobres deploy health --port 8787
284
+ docker logs sobres-ci 2>&1 | grep -q "token"
285
+ # A one-off command shares the same volume, then the container is replaced and data survives.
286
+ docker run --rm -v sobres-ci:/data sobres:ci portfolio save core --tickers AAPL MSFT --weights 0.6 0.4
287
+ docker stop -t 10 sobres-ci && docker rm sobres-ci
288
+ docker run --rm -v sobres-ci:/data sobres:ci portfolio list --format json | grep -q '"core"'
289
+ docker run --rm -v sobres-ci:/data sobres:ci doctor --offline
290
+
291
+ - name: The compressed image is under the 500 MB budget
292
+ run: |
293
+ size=$(docker save sobres:ci | gzip | wc -c)
294
+ echo "compressed image: $((size / 1024 / 1024)) MB"
295
+ test "$size" -lt $((500 * 1024 * 1024))
296
+
297
+ - name: The runtime stage carries no Node or build toolchain
298
+ run: |
299
+ ! docker run --rm --entrypoint sh sobres:ci -c "command -v node || command -v npm || command -v gcc"
300
+
301
+ security:
302
+ name: Dependency audit
303
+ runs-on: ubuntu-latest
304
+ steps:
305
+ - uses: actions/checkout@v7
306
+ - uses: actions/setup-python@v7
307
+ with:
308
+ python-version: "3.12"
309
+ cache: pip
310
+ cache-dependency-path: pyproject.toml
311
+ - run: pip install -e ".[dev]" pip-audit
312
+
313
+ - name: Resolve the dependency tree
314
+ # Audit what this project actually pulls in, across every extra --
315
+ # `--exclude-editable` drops the local project (which is not on any
316
+ # index and so can never be audited), and `pip freeze` already omits
317
+ # pip/setuptools/wheel, whose advisories belong to the runner image
318
+ # rather than to this codebase. Auditing the ambient environment
319
+ # instead would fail on whatever pip version the runner happens to ship.
320
+ run: pip freeze --exclude-editable > requirements-audit.txt
321
+
322
+ - name: pip-audit
323
+ # A known vulnerability blocks merge and release by design. To ship past
324
+ # an unfixable transitive advisory, add `--ignore-vuln <ID>` here with a
325
+ # comment saying why -- see docs/RELEASING.md.
326
+ run: pip-audit --strict --progress-spinner=off -r requirements-audit.txt
327
+
328
+ # Single required status check. Branch protection points at this one job, so
329
+ # the matrix above can grow or shrink without editing protection rules.
330
+ all-green:
331
+ name: All checks passed
332
+ if: always()
333
+ needs: [lint, typecheck, test, frontend, build, docker, security]
334
+ runs-on: ubuntu-latest
335
+ steps:
336
+ # `if: always()` means this job runs even when a dependency failed, so it
337
+ # must inspect the results itself rather than rely on job ordering.
338
+ # Anything that is not `success` -- failed, cancelled, or skipped -- is a
339
+ # red gate; a skipped required job must never read as passing.
340
+ #
341
+ # The condition lives on the step, not in shell. A multi-line `${{ }}`
342
+ # interpolated into an `if` statement expands to a continuation line
343
+ # starting with `||`, which bash rejects as a syntax error -- so the gate
344
+ # would fail even when every job passed.
345
+ - name: Fail if any dependency did not succeed
346
+ if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')
347
+ run: |
348
+ echo "::error::Required jobs did not all succeed: ${{ join(needs.*.result, ', ') }}"
349
+ exit 1
350
+
351
+ - name: All required jobs succeeded
352
+ run: |
353
+ echo "Results: ${{ join(needs.*.result, ', ') }}"