downshift 0.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 (238) hide show
  1. downshift-0.1.0/.bob/custom_modes.yaml +46 -0
  2. downshift-0.1.0/.bob/rules-agent/AGENTS.md +13 -0
  3. downshift-0.1.0/.bob/rules-ask/AGENTS.md +12 -0
  4. downshift-0.1.0/.bob/rules-plan/AGENTS.md +13 -0
  5. downshift-0.1.0/.bob/skills/downshift-audit/SKILL.md +47 -0
  6. downshift-0.1.0/.bob/skills/downshift-audit/schema-reference.md +82 -0
  7. downshift-0.1.0/.bob/skills/downshift-evals/SKILL.md +73 -0
  8. downshift-0.1.0/.bob/skills/downshift-evals/format-reference.md +55 -0
  9. downshift-0.1.0/.bobignore +17 -0
  10. downshift-0.1.0/.github/workflows/ci.yml +53 -0
  11. downshift-0.1.0/.github/workflows/cost-diff.yml +20 -0
  12. downshift-0.1.0/.github/workflows/publish.yml +36 -0
  13. downshift-0.1.0/.gitignore +14 -0
  14. downshift-0.1.0/.pre-commit-config.yaml +7 -0
  15. downshift-0.1.0/AGENTS.md +71 -0
  16. downshift-0.1.0/CHANGELOG.md +44 -0
  17. downshift-0.1.0/CONTRIBUTING.md +20 -0
  18. downshift-0.1.0/LICENSE +21 -0
  19. downshift-0.1.0/PKG-INFO +218 -0
  20. downshift-0.1.0/README.md +175 -0
  21. downshift-0.1.0/action.yml +117 -0
  22. downshift-0.1.0/bob_sessions/.gitkeep +0 -0
  23. downshift-0.1.0/bob_sessions/downshift_task01_init.png +0 -0
  24. downshift-0.1.0/bob_sessions/downshift_task02_architecture.png +0 -0
  25. downshift-0.1.0/bob_sessions/downshift_task03_audit_a.png +0 -0
  26. downshift-0.1.0/bob_sessions/downshift_task03_audit_b.png +0 -0
  27. downshift-0.1.0/bob_sessions/downshift_task04_evals_a.png +0 -0
  28. downshift-0.1.0/bob_sessions/downshift_task04_evals_b.png +0 -0
  29. downshift-0.1.0/bob_sessions/downshift_task05_report_a01.png +0 -0
  30. downshift-0.1.0/bob_sessions/downshift_task05_report_a02.png +0 -0
  31. downshift-0.1.0/bob_sessions/downshift_task05_report_a03.png +0 -0
  32. downshift-0.1.0/bob_sessions/downshift_task05_report_b01.png +0 -0
  33. downshift-0.1.0/bob_sessions/downshift_task05_report_b02.png +0 -0
  34. downshift-0.1.0/bob_sessions/downshift_task05_report_b03.png +0 -0
  35. downshift-0.1.0/bob_sessions/downshift_task05_report_b04.png +0 -0
  36. downshift-0.1.0/bob_sessions/downshift_task05_report_b05.png +0 -0
  37. downshift-0.1.0/bob_sessions/downshift_task06_apply_a.png +0 -0
  38. downshift-0.1.0/bob_sessions/downshift_task06_apply_b.png +0 -0
  39. downshift-0.1.0/bob_sessions/downshift_task06_apply_c.png +0 -0
  40. downshift-0.1.0/bob_sessions/downshift_task07_apply3b.png +0 -0
  41. downshift-0.1.0/bob_sessions/downshift_task08_review.png +0 -0
  42. downshift-0.1.0/bob_sessions/downshift_task09_web01.png +0 -0
  43. downshift-0.1.0/bob_sessions/downshift_task09_web02.png +0 -0
  44. downshift-0.1.0/bob_sessions/downshift_task09_web03.png +0 -0
  45. downshift-0.1.0/bob_sessions/downshift_task10_case_study_a.png +0 -0
  46. downshift-0.1.0/bob_sessions/downshift_task10_case_study_b.png +0 -0
  47. downshift-0.1.0/bob_sessions/downshift_task10_case_study_c.png +0 -0
  48. downshift-0.1.0/bob_sessions/downshift_task10_case_study_d.png +0 -0
  49. downshift-0.1.0/bob_sessions/downshift_task11_mem0_a.png +0 -0
  50. downshift-0.1.0/bob_sessions/downshift_task11_mem0_b.png +0 -0
  51. downshift-0.1.0/bob_sessions/downshift_task11_mem0_c.png +0 -0
  52. downshift-0.1.0/bob_sessions/downshift_task11_mem0_d.png +0 -0
  53. downshift-0.1.0/bob_sessions/downshift_task11_mem0_e.png +0 -0
  54. downshift-0.1.0/docs/architecture.md +125 -0
  55. downshift-0.1.0/docs/bob.md +41 -0
  56. downshift-0.1.0/docs/case-study/mem0/downshift.audit.json +2023 -0
  57. downshift-0.1.0/docs/case-study/mem0/downshift.scan.json +1989 -0
  58. downshift-0.1.0/docs/case-study/mem0/downshift.yaml +33 -0
  59. downshift-0.1.0/docs/case-study/mem0/estimate.md +20 -0
  60. downshift-0.1.0/docs/case-study/orchestrai/downshift.audit.json +245 -0
  61. downshift-0.1.0/docs/case-study/orchestrai/downshift.scan.json +54 -0
  62. downshift-0.1.0/docs/case-study/orchestrai/downshift.yaml +24 -0
  63. downshift-0.1.0/docs/case-study/orchestrai/estimate.md +20 -0
  64. downshift-0.1.0/docs/case-study-mem0.md +130 -0
  65. downshift-0.1.0/docs/case-study.md +112 -0
  66. downshift-0.1.0/docs/images/bad_pr_cost_diff_a.png +0 -0
  67. downshift-0.1.0/docs/images/bad_pr_cost_diff_b.png +0 -0
  68. downshift-0.1.0/docs/plans/report-plan.md +228 -0
  69. downshift-0.1.0/docs/specs/report.md +171 -0
  70. downshift-0.1.0/downshift.example.yaml +33 -0
  71. downshift-0.1.0/examples/supportdesk/README.md +70 -0
  72. downshift-0.1.0/examples/supportdesk/data/refund_policy.md +19 -0
  73. downshift-0.1.0/examples/supportdesk/data/tickets.jsonl +40 -0
  74. downshift-0.1.0/examples/supportdesk/downshift.audit.json +345 -0
  75. downshift-0.1.0/examples/supportdesk/downshift.report.md +134 -0
  76. downshift-0.1.0/examples/supportdesk/downshift.scan.after.json +294 -0
  77. downshift-0.1.0/examples/supportdesk/downshift.scan.json +290 -0
  78. downshift-0.1.0/examples/supportdesk/downshift.yaml +26 -0
  79. downshift-0.1.0/examples/supportdesk/evals/supportdesk.agent_assist__draft_reply.jsonl +22 -0
  80. downshift-0.1.0/examples/supportdesk/evals/supportdesk.agent_assist__summarize_for_agent.jsonl +22 -0
  81. downshift-0.1.0/examples/supportdesk/evals/supportdesk.extract__extract_order_info.jsonl +21 -0
  82. downshift-0.1.0/examples/supportdesk/evals/supportdesk.misc_utils__lang_of.jsonl +22 -0
  83. downshift-0.1.0/examples/supportdesk/evals/supportdesk.policy__decide_refund.jsonl +26 -0
  84. downshift-0.1.0/examples/supportdesk/evals/supportdesk.triage__classify_category.jsonl +25 -0
  85. downshift-0.1.0/examples/supportdesk/evals/supportdesk.triage__detect_sentiment.jsonl +22 -0
  86. downshift-0.1.0/examples/supportdesk/evals/supportdesk.triage__tag_urgency.jsonl +22 -0
  87. downshift-0.1.0/examples/supportdesk/models.yaml +14 -0
  88. downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__draft_reply/qwen2.5-0.5b.jsonl +44 -0
  89. downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__draft_reply/qwen2.5-1.5b.jsonl +44 -0
  90. downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__draft_reply/qwen2.5-3b.jsonl +22 -0
  91. downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__draft_reply/qwen2.5-7b.jsonl +44 -0
  92. downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__summarize_for_agent/qwen2.5-0.5b.jsonl +44 -0
  93. downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__summarize_for_agent/qwen2.5-1.5b.jsonl +44 -0
  94. downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__summarize_for_agent/qwen2.5-3b.jsonl +22 -0
  95. downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__summarize_for_agent/qwen2.5-7b.jsonl +44 -0
  96. downshift-0.1.0/examples/supportdesk/results/supportdesk.extract__extract_order_info/qwen2.5-0.5b.jsonl +21 -0
  97. downshift-0.1.0/examples/supportdesk/results/supportdesk.extract__extract_order_info/qwen2.5-1.5b.jsonl +21 -0
  98. downshift-0.1.0/examples/supportdesk/results/supportdesk.extract__extract_order_info/qwen2.5-3b.jsonl +21 -0
  99. downshift-0.1.0/examples/supportdesk/results/supportdesk.extract__extract_order_info/qwen2.5-7b.jsonl +21 -0
  100. downshift-0.1.0/examples/supportdesk/results/supportdesk.misc_utils__lang_of/qwen2.5-0.5b.jsonl +22 -0
  101. downshift-0.1.0/examples/supportdesk/results/supportdesk.misc_utils__lang_of/qwen2.5-1.5b.jsonl +22 -0
  102. downshift-0.1.0/examples/supportdesk/results/supportdesk.misc_utils__lang_of/qwen2.5-3b.jsonl +22 -0
  103. downshift-0.1.0/examples/supportdesk/results/supportdesk.misc_utils__lang_of/qwen2.5-7b.jsonl +22 -0
  104. downshift-0.1.0/examples/supportdesk/results/supportdesk.policy__decide_refund/qwen2.5-0.5b.jsonl +25 -0
  105. downshift-0.1.0/examples/supportdesk/results/supportdesk.policy__decide_refund/qwen2.5-1.5b.jsonl +25 -0
  106. downshift-0.1.0/examples/supportdesk/results/supportdesk.policy__decide_refund/qwen2.5-3b.jsonl +25 -0
  107. downshift-0.1.0/examples/supportdesk/results/supportdesk.policy__decide_refund/qwen2.5-7b.jsonl +25 -0
  108. downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__classify_category/qwen2.5-0.5b.jsonl +25 -0
  109. downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__classify_category/qwen2.5-1.5b.jsonl +25 -0
  110. downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__classify_category/qwen2.5-3b.jsonl +25 -0
  111. downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__classify_category/qwen2.5-7b.jsonl +25 -0
  112. downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__detect_sentiment/qwen2.5-0.5b.jsonl +22 -0
  113. downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__detect_sentiment/qwen2.5-1.5b.jsonl +22 -0
  114. downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__detect_sentiment/qwen2.5-3b.jsonl +22 -0
  115. downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__detect_sentiment/qwen2.5-7b.jsonl +22 -0
  116. downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__tag_urgency/qwen2.5-0.5b.jsonl +22 -0
  117. downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__tag_urgency/qwen2.5-1.5b.jsonl +22 -0
  118. downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__tag_urgency/qwen2.5-3b.jsonl +22 -0
  119. downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__tag_urgency/qwen2.5-7b.jsonl +22 -0
  120. downshift-0.1.0/examples/supportdesk/supportdesk/__init__.py +1 -0
  121. downshift-0.1.0/examples/supportdesk/supportdesk/__main__.py +40 -0
  122. downshift-0.1.0/examples/supportdesk/supportdesk/agent_assist.py +56 -0
  123. downshift-0.1.0/examples/supportdesk/supportdesk/extract.py +37 -0
  124. downshift-0.1.0/examples/supportdesk/supportdesk/llm.py +35 -0
  125. downshift-0.1.0/examples/supportdesk/supportdesk/misc_utils.py +28 -0
  126. downshift-0.1.0/examples/supportdesk/supportdesk/models.py +38 -0
  127. downshift-0.1.0/examples/supportdesk/supportdesk/pipeline.py +63 -0
  128. downshift-0.1.0/examples/supportdesk/supportdesk/policy.py +65 -0
  129. downshift-0.1.0/examples/supportdesk/supportdesk/triage.py +59 -0
  130. downshift-0.1.0/pyproject.toml +90 -0
  131. downshift-0.1.0/src/downshift/__init__.py +3 -0
  132. downshift-0.1.0/src/downshift/audit.py +188 -0
  133. downshift-0.1.0/src/downshift/cli.py +930 -0
  134. downshift-0.1.0/src/downshift/config.py +348 -0
  135. downshift-0.1.0/src/downshift/cost.py +212 -0
  136. downshift-0.1.0/src/downshift/decide.py +220 -0
  137. downshift-0.1.0/src/downshift/diff.py +387 -0
  138. downshift-0.1.0/src/downshift/evalgen.py +220 -0
  139. downshift-0.1.0/src/downshift/evals.py +395 -0
  140. downshift-0.1.0/src/downshift/export.py +412 -0
  141. downshift-0.1.0/src/downshift/gitref.py +65 -0
  142. downshift-0.1.0/src/downshift/llm.py +171 -0
  143. downshift-0.1.0/src/downshift/py.typed +0 -0
  144. downshift-0.1.0/src/downshift/report.py +380 -0
  145. downshift-0.1.0/src/downshift/resolve.py +439 -0
  146. downshift-0.1.0/src/downshift/runner.py +312 -0
  147. downshift-0.1.0/src/downshift/scanner.py +334 -0
  148. downshift-0.1.0/src/downshift/schema.py +356 -0
  149. downshift-0.1.0/src/downshift/scorer.py +219 -0
  150. downshift-0.1.0/tests/cli/test_audit_commands.py +152 -0
  151. downshift-0.1.0/tests/cli/test_check_evals.py +91 -0
  152. downshift-0.1.0/tests/cli/test_cli_help.py +20 -0
  153. downshift-0.1.0/tests/cli/test_cli_scan.py +115 -0
  154. downshift-0.1.0/tests/cli/test_diff_cli.py +152 -0
  155. downshift-0.1.0/tests/cli/test_e2e_pipeline.py +79 -0
  156. downshift-0.1.0/tests/cli/test_estimate_cli.py +84 -0
  157. downshift-0.1.0/tests/cli/test_evalgen_cli.py +127 -0
  158. downshift-0.1.0/tests/cli/test_export_cli.py +91 -0
  159. downshift-0.1.0/tests/cli/test_report_cli.py +115 -0
  160. downshift-0.1.0/tests/cli/test_rescore_cli.py +169 -0
  161. downshift-0.1.0/tests/cli/test_run_cli.py +142 -0
  162. downshift-0.1.0/tests/fixtures/supportdesk_v0/README.md +8 -0
  163. downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/__init__.py +1 -0
  164. downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/__main__.py +40 -0
  165. downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/agent_assist.py +58 -0
  166. downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/extract.py +38 -0
  167. downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/llm.py +39 -0
  168. downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/misc_utils.py +26 -0
  169. downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/pipeline.py +63 -0
  170. downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/policy.py +66 -0
  171. downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/triage.py +55 -0
  172. downshift-0.1.0/tests/integration/test_evalgen_ollama.py +26 -0
  173. downshift-0.1.0/tests/integration/test_ollama.py +17 -0
  174. downshift-0.1.0/tests/integration/test_run_ollama.py +29 -0
  175. downshift-0.1.0/tests/snapshots/supportdesk_report.md +134 -0
  176. downshift-0.1.0/tests/unit/test_audit.py +252 -0
  177. downshift-0.1.0/tests/unit/test_config.py +221 -0
  178. downshift-0.1.0/tests/unit/test_cost.py +237 -0
  179. downshift-0.1.0/tests/unit/test_decide.py +358 -0
  180. downshift-0.1.0/tests/unit/test_diff.py +222 -0
  181. downshift-0.1.0/tests/unit/test_evalgen.py +207 -0
  182. downshift-0.1.0/tests/unit/test_evals.py +448 -0
  183. downshift-0.1.0/tests/unit/test_export.py +212 -0
  184. downshift-0.1.0/tests/unit/test_llm.py +158 -0
  185. downshift-0.1.0/tests/unit/test_report.py +568 -0
  186. downshift-0.1.0/tests/unit/test_report_snapshot.py +41 -0
  187. downshift-0.1.0/tests/unit/test_rescore.py +185 -0
  188. downshift-0.1.0/tests/unit/test_resolve.py +397 -0
  189. downshift-0.1.0/tests/unit/test_runner.py +311 -0
  190. downshift-0.1.0/tests/unit/test_scanner.py +265 -0
  191. downshift-0.1.0/tests/unit/test_scanner_sdk_detection.py +104 -0
  192. downshift-0.1.0/tests/unit/test_schema.py +221 -0
  193. downshift-0.1.0/tests/unit/test_scorer.py +298 -0
  194. downshift-0.1.0/web/.gitignore +5 -0
  195. downshift-0.1.0/web/SPEC.md +796 -0
  196. downshift-0.1.0/web/app/audit/page.tsx +117 -0
  197. downshift-0.1.0/web/app/callsites/[slug]/CallSiteDetail.tsx +229 -0
  198. downshift-0.1.0/web/app/callsites/[slug]/page.tsx +163 -0
  199. downshift-0.1.0/web/app/callsites/page.tsx +82 -0
  200. downshift-0.1.0/web/app/case-study/page.tsx +158 -0
  201. downshift-0.1.0/web/app/docs/page.tsx +131 -0
  202. downshift-0.1.0/web/app/globals.css +87 -0
  203. downshift-0.1.0/web/app/how-it-works/page.tsx +94 -0
  204. downshift-0.1.0/web/app/layout.tsx +37 -0
  205. downshift-0.1.0/web/app/not-found.tsx +14 -0
  206. downshift-0.1.0/web/app/page.tsx +226 -0
  207. downshift-0.1.0/web/components/Badge.tsx +47 -0
  208. downshift-0.1.0/web/components/Button.tsx +31 -0
  209. downshift-0.1.0/web/components/CodeBlock.tsx +17 -0
  210. downshift-0.1.0/web/components/Disclaimer.tsx +10 -0
  211. downshift-0.1.0/web/components/Footer.tsx +33 -0
  212. downshift-0.1.0/web/components/Nav.tsx +68 -0
  213. downshift-0.1.0/web/components/Panel.tsx +41 -0
  214. downshift-0.1.0/web/components/StatCard.tsx +45 -0
  215. downshift-0.1.0/web/components/Table.tsx +32 -0
  216. downshift-0.1.0/web/components/ThemeToggle.tsx +50 -0
  217. downshift-0.1.0/web/eslint.config.mjs +13 -0
  218. downshift-0.1.0/web/lib/__tests__/content.test.ts +37 -0
  219. downshift-0.1.0/web/lib/__tests__/data.test.ts +51 -0
  220. downshift-0.1.0/web/lib/__tests__/format.test.ts +86 -0
  221. downshift-0.1.0/web/lib/__tests__/select.test.ts +142 -0
  222. downshift-0.1.0/web/lib/content.ts +315 -0
  223. downshift-0.1.0/web/lib/data.ts +28 -0
  224. downshift-0.1.0/web/lib/format.ts +62 -0
  225. downshift-0.1.0/web/lib/select.ts +53 -0
  226. downshift-0.1.0/web/lib/types.ts +140 -0
  227. downshift-0.1.0/web/next-env.d.ts +6 -0
  228. downshift-0.1.0/web/next.config.ts +11 -0
  229. downshift-0.1.0/web/package-lock.json +7213 -0
  230. downshift-0.1.0/web/package.json +33 -0
  231. downshift-0.1.0/web/postcss.config.mjs +2 -0
  232. downshift-0.1.0/web/public/data/audit.json +205 -0
  233. downshift-0.1.0/web/public/data/callsites.json +1067 -0
  234. downshift-0.1.0/web/public/data/evals_summary.json +2824 -0
  235. downshift-0.1.0/web/public/data/report.md +134 -0
  236. downshift-0.1.0/web/public/data/summary.json +75 -0
  237. downshift-0.1.0/web/tsconfig.json +21 -0
  238. downshift-0.1.0/web/vitest.config.ts +12 -0
@@ -0,0 +1,46 @@
1
+ customModes:
2
+ - slug: downshift-auditor
3
+ name: Downshift Auditor
4
+ description: Audits LLM call sites from downshift scan and writes downshift.audit.json.
5
+ roleDefinition: >-
6
+ You are the Downshift Auditor. You read the output of `downshift scan` and close the
7
+ gaps static analysis cannot: models and prompts only known at runtime, and shared LLM
8
+ helpers that serve several features. You also describe each call site precisely enough
9
+ that evals can be written for it. You are careful, you never guess, and you read only
10
+ the files you need.
11
+ whenToUse: >-
12
+ Use after running `downshift scan`, to resolve unresolved call sites, split shared LLM
13
+ helpers into one call site per feature, and add purpose, output contract, difficulty
14
+ and grading.
15
+ customInstructions: >-
16
+ Load the downshift-audit skill first and follow it exactly. Never edit source code.
17
+ The only file you may write is downshift.audit.json. Do not run commands.
18
+ groups:
19
+ - read
20
+ - - edit
21
+ - fileRegex: "(^|/)downshift\\.audit\\.json$"
22
+ description: Only the audit output file
23
+ - skill
24
+ - todo
25
+ - slug: downshift-eval-writer
26
+ name: Downshift Eval Writer
27
+ description: Writes eval sets (JSONL test cases) for audited LLM call sites.
28
+ roleDefinition: >-
29
+ You are the Downshift Eval Writer. For one audited LLM call site you write 20 to 25
30
+ test cases whose expected answers are provably correct from the prompt, the output
31
+ contract and the refund policy. You prefer fewer, unambiguous cases over clever ones,
32
+ and you never guess a label.
33
+ whenToUse: >-
34
+ Use after downshift.audit.json exists, to write one eval file per call site in the
35
+ evals folder next to the audit.
36
+ customInstructions: >-
37
+ Load the downshift-evals skill first and follow it exactly. Never edit source code or
38
+ the audit. The only files you may write are .jsonl files in an evals folder. Do not run
39
+ commands.
40
+ groups:
41
+ - read
42
+ - - edit
43
+ - fileRegex: "(^|/)evals/[^/]+\\.jsonl$"
44
+ description: Only eval set files
45
+ - skill
46
+ - todo
@@ -0,0 +1,13 @@
1
+ # AGENTS.md: Agent (coding) mode
2
+
3
+ ## Non-obvious coding rules
4
+
5
+ - **Scanner unit tests use `scan_source(textwrap.dedent(src), "app.py")`**, not `scan_module` or temp files.
6
+ - **Extend the `_MISSING` sentinel pattern** when adding optional fields to `_field()` in `schema.py`. `None` is a valid value for nullable fields, so it cannot mean "not provided".
7
+ - **Call `_reject_unknown()` at the start of every `from_dict`** in `schema.py`; omitting it silently accepts unknown fields.
8
+ - **`frozenset`, not `Enum`**, for valid-value sets. Add new values to the existing constants (`MODEL_SOURCES`, `PRODUCERS`, ...).
9
+ - **Bob-produced call sites** set `found_by="bob"` on each `CallSite` and `generated_by="bob"` on the `ScanResult`. Every file Bob writes must load cleanly with `ScanResult.load`.
10
+ - **`API_PATTERNS`** in `scanner.py` controls which call chains are detected. Add an LLM API by appending a `(suffix_tuple, api_name, needs_model_kwarg)` entry.
11
+ - **Call site IDs are stable keys** used in `downshift.yaml` `volume.per_call_site`; changing the ID format is a breaking change.
12
+ - **Config dataclasses are `frozen=True`**: construct a new instance instead of mutating.
13
+ - **Run checks before finishing**: `ruff format . && ruff check . && mypy src && python -m pytest`. Never bare `pytest`.
@@ -0,0 +1,12 @@
1
+ # AGENTS.md: Ask mode
2
+
3
+ ## Non-obvious documentation context
4
+
5
+ - **`schema.py` is the shared contract** between the AST scanner and the Bob auditor. Both produce files in the same format.
6
+ - **Implemented vs stub commands**: check `cli.py`. Stubs call `_not_implemented()` and exit with code 1.
7
+ - **`examples/supportdesk/`** is the demo target and integration fixture, referenced in tests as `SUPPORTDESK`. Its data is synthetic.
8
+ - **Integration tests need local Ollama** and are excluded from the default run; use `python -m pytest -m integration`.
9
+ - **`downshift.example.yaml`** is the config template users copy to `downshift.yaml`.
10
+ - **`.bob/` ships with the product**: `custom_modes.yaml` defines the Downshift Auditor mode and `skills/downshift-audit/` holds its workflow. Install guide: `docs/bob.md`.
11
+ - **`bob_sessions/`** holds screenshots of Bob tasks (hackathon evidence), not code.
12
+ - **Architecture overview**: `docs/architecture.md`.
@@ -0,0 +1,13 @@
1
+ # AGENTS.md: Plan mode
2
+
3
+ ## Non-obvious architectural constraints
4
+
5
+ - **`schema.py` is forward-only**: `schema_version` is checked on load and an unsupported version raises. No migration path.
6
+ - **`Resolver` is stateful within one `_scan_modules` call** (it holds a `ModuleIndex`); do not share one across `scan_path` invocations.
7
+ - **Cross-module resolution is one-pass**: `ModuleIndex` is built once before resolution. Circular imports are not followed.
8
+ - **Caller attribution is name-based**: `_attach_callers` matches function names as strings.
9
+ - **`ScanResult.write()` overwrites silently**: no merging, no locking.
10
+ - **`via`** is not set by the AST scanner. The Bob auditor sets it when it splits a shared helper (e.g. `supportdesk/llm.py::ask`) into one logical call site per feature; `via` holds the helper's call site id.
11
+ - **Bob auditor output is a separate file**, e.g. `examples/supportdesk/downshift.audit.json`. It never overwrites the scan file; `downshift compare` reads both.
12
+ - **`.bob/custom_modes.yaml` and `.bob/skills/` are shipped product files** (the Downshift Auditor mode and its skill), not scratch space.
13
+ - **Plans stay small**: one job per task, touch only the files named in the request.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: downshift-audit
3
+ description: Audit LLM call sites from a downshift scan file. Resolve models and prompts the static scanner missed, split shared LLM helpers into one call site per feature, add purpose, output_contract, difficulty and grading, and write downshift.audit.json.
4
+ ---
5
+
6
+ # Downshift audit
7
+
8
+ Input: a scan file written by `downshift scan`, given as an @mention (for example `examples/supportdesk/downshift.scan.json`).
9
+ Output: `downshift.audit.json` in the same folder as the scan file.
10
+ Field reference: `schema-reference.md` in this skill folder. Read it before writing.
11
+
12
+ ## Steps
13
+
14
+ 1. Read the scan file and `schema-reference.md`. Do not re-scan the repo.
15
+ 2. Build a worklist. A call site needs inspection if any of these is true:
16
+ - `model.value` is null, or `model.source` is `kwargs`, `env`, `dynamic` or `missing`
17
+ - `messages` is null, or any message has `resolved: false`
18
+ - it has 2 or more `callers` and its prompt is built by the callers
19
+ 3. For each worklist call site, read only its own file and the files of its `callers`. Read nothing else.
20
+ 4. Resolve models. Set `model.value` to the real model string, `model.source` to `manual`, keep `expression` and `env_var`, set `defined_in` to the file where the model string lives, set `found_by` to `bob`, and add a note starting with `Bob:` that says how you resolved it.
21
+ 5. Resolve prompts. Set `messages` to the full list as it is sent to the model. Write runtime values as `{placeholder}` with a plain name: `{ticket['body']}` becomes `{body}`, an expression like `{json.dumps(order_info)}` becomes `{order_info_json}`, and a module constant is written out as its value. Set `resolved: true` on each message. Add a `Bob:` note.
22
+ 6. Split shared helpers. If one helper serves several features with different prompts, replace the helper entry with one entry per calling feature:
23
+ - `id`: the caller's id in the form `path/to/file.py::qualname`
24
+ - `file`, `line`, `end_line`, `function`: the caller's call to the helper
25
+ - `via`: the helper's id from the scan file
26
+ - `api`, `model`, `is_async`, `temperature`, `max_tokens`: from the helper call, as this caller uses it
27
+ - `messages`: the prompt this caller sends
28
+ - `callers`: empty list. `found_by`: `bob`. Add a `Bob:` note naming the helper.
29
+ The helper does not stay in the output as its own entry.
30
+ 7. Enrich every call site, including ones that were already resolved: `purpose`, `output_contract`, `difficulty`, `grading` (rules below).
31
+ 8. Write `downshift.audit.json`. Copy every top-level key and value from the scan file unchanged, except set `generated_by` to `bob` and replace `call_sites` with your list. Include every CallSite field on every entry. Sort call sites by `id`.
32
+ 9. Finish with a short table: id, model, what changed (resolved model, resolved prompt, split from helper, metadata only). Do not run commands. The user validates with `downshift validate`.
33
+
34
+ ## Enrichment rules
35
+
36
+ - `purpose`: one sentence naming the product feature. Example: "Classify a support ticket into one of five categories."
37
+ - `output_contract`: concrete, checkable rules taken from the prompt and from the code that parses the output. Exact JSON keys and allowed values, allowed labels, length limits, forbidden content. Evals will test exactly these rules.
38
+ - `difficulty`: `easy` for a single label or short extraction; `medium` for structured JSON or tightly constrained short text; `hard` for open-ended writing or policy reasoning.
39
+ - `grading`: `exact` for a single label or short fixed answer; `json_fields` when `output_format` is `json`; `judge` for free text.
40
+
41
+ ## Hard rules
42
+
43
+ - Never edit source code or any file other than `downshift.audit.json`.
44
+ - Never invent. If something cannot be determined from the code, leave it null, keep the original `source`, and add a `Bob:` note saying what is missing.
45
+ - Use only the fields in `schema-reference.md`. Unknown fields fail validation.
46
+ - Keep the ids of call sites you do not split exactly as they are. They are keys in `downshift.yaml`.
47
+ - Call sites that were already fully resolved keep `found_by: "ast"`. Only add the enrichment fields.
@@ -0,0 +1,82 @@
1
+ # downshift.audit.json field reference
2
+
3
+ Same format as the scan file. Top-level keys: copy them from the scan file; set `generated_by` to `bob`.
4
+
5
+ ## CallSite (every field required on every entry)
6
+
7
+ | Field | Type | Notes |
8
+ |---|---|---|
9
+ | id | string | `path/to/file.py::qualname`, duplicates get `#2` |
10
+ | file | string | relative POSIX path |
11
+ | line | int | >= 1 |
12
+ | end_line | int or null | |
13
+ | function | string | qualified name, `<module>` for top level |
14
+ | api | string | copy from the scan file, never invent new values |
15
+ | model | ModelRef | below |
16
+ | is_async | bool | |
17
+ | messages | list of PromptMessage, or null | |
18
+ | output_format | string | `text` or `json` |
19
+ | temperature | number or null | |
20
+ | max_tokens | int or null | |
21
+ | via | string or null | id of the shared helper, only on split entries |
22
+ | callers | list of strings | call site ids |
23
+ | notes | list of strings | Bob notes start with `Bob:` |
24
+ | purpose | string or null | |
25
+ | output_contract | string or null | |
26
+ | difficulty | string or null | `easy`, `medium`, `hard` |
27
+ | grading | string or null | `exact`, `json_fields`, `judge` |
28
+ | found_by | string | `ast`, `bob`, `manual` |
29
+
30
+ ## ModelRef
31
+
32
+ | Field | Type | Notes |
33
+ |---|---|---|
34
+ | value | string or null | the model name |
35
+ | source | string | `literal`, `constant`, `env_default`, `env`, `dict_lookup`, `parameter_default`, `kwargs`, `missing`, `dynamic`, `manual` (use `manual` when Bob resolves it) |
36
+ | expression | string | source text of the model argument, keep as scanned |
37
+ | env_var | string or null | |
38
+ | defined_in | string or null | file where the model string lives |
39
+
40
+ ## PromptMessage
41
+
42
+ | Field | Type | Notes |
43
+ |---|---|---|
44
+ | role | string | `system`, `user`, `assistant` |
45
+ | content | string | template text, runtime values as `{placeholder}` |
46
+ | resolved | bool | true once Bob has resolved it |
47
+
48
+ ## Example: one entry split from a shared helper
49
+
50
+ ```json
51
+ {
52
+ "id": "app/billing.py::summarize_invoice",
53
+ "file": "app/billing.py",
54
+ "line": 42,
55
+ "end_line": 45,
56
+ "function": "summarize_invoice",
57
+ "api": "openai.chat.completions",
58
+ "model": {
59
+ "value": "gpt-4o-mini",
60
+ "source": "manual",
61
+ "expression": "MODEL",
62
+ "env_var": null,
63
+ "defined_in": "app/llm.py"
64
+ },
65
+ "is_async": false,
66
+ "messages": [
67
+ {"role": "system", "content": "Summarize the invoice in one sentence.", "resolved": true},
68
+ {"role": "user", "content": "{invoice_text}", "resolved": true}
69
+ ],
70
+ "output_format": "text",
71
+ "temperature": 0.0,
72
+ "max_tokens": null,
73
+ "via": "app/llm.py::complete",
74
+ "callers": [],
75
+ "notes": ["Bob: split from shared helper app/llm.py::complete; this caller builds its own prompt."],
76
+ "purpose": "Summarize an invoice for the billing dashboard.",
77
+ "output_contract": "One sentence, under 30 words, mentions the total amount, no speculation.",
78
+ "difficulty": "easy",
79
+ "grading": "judge",
80
+ "found_by": "bob"
81
+ }
82
+ ```
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: downshift-evals
3
+ description: Write eval sets for audited LLM call sites. One JSONL file of 20 to 25 test cases per call site, with inputs matching the prompt placeholders and expected answers that follow the output contract and the refund policy.
4
+ ---
5
+
6
+ # Downshift evals
7
+
8
+ Input: `downshift.audit.json` and the call site(s) you are assigned.
9
+ Output: one file per call site in the `evals/` folder next to the audit, for example `examples/supportdesk/evals/supportdesk.triage__classify_category.jsonl`.
10
+ Format reference: `format-reference.md` in this skill folder. Read it before writing.
11
+
12
+ ## Steps
13
+
14
+ 1. Read `format-reference.md`, your call site's entry in the audit (`messages`, `output_contract`, `grading`), `data/refund_policy.md` and `data/tickets.jsonl`. Read nothing else.
15
+ 2. Plan 20 to 25 cases in a todo list before writing: which label or rule each case covers, and whether its input comes from `tickets.jsonl` or is new.
16
+ 3. Write the file. One JSON object per line, no trailing commas, no comments.
17
+ 4. Finish with a short table: file, case count, cases per label (or per rule). Do not run commands. The user checks with `downshift check-evals`.
18
+
19
+ ## Rules for every call site
20
+
21
+ - Inputs: about half adapted from `tickets.jsonl` (use each ticket at most once per file), the rest new. New tickets look like real customer mail: same store, order IDs like `ORD-10xxx`, dates in 2026.
22
+ - `ticket_text` is always `subject + "\n\n" + body`, exactly as the app builds it.
23
+ - Cover every allowed label or rule at least 3 times when the site has a label set.
24
+ - Include these edge cases where they apply: very short text, non-English text (es, fr, de, hi, pt), mixed language, sarcasm, typos or all caps, missing order ID, dates a day or two either side of a policy limit.
25
+ - Only write cases a careful human would label the same way. If a case could reasonably have two answers, drop it. Never put a date exactly on a policy limit (exactly 14, 30 or 60 days); use 1 or 2 days either side.
26
+ - `notes` on every case: one line saying what the case tests.
27
+ - `id`: short prefix plus number, for example `cat-01`. Unique within the file.
28
+ - `grading` on every case equals the call site's `grading` in the audit.
29
+
30
+ ## Per call site
31
+
32
+ ### triage.py::classify_category (exact)
33
+ Labels: billing, shipping, technical, account, refund, other.
34
+ - refund: the customer asks for money back or a return for money, whatever the reason.
35
+ - billing: charges, duplicate charges, invoices, payment methods, with no refund or return ask.
36
+ - shipping: delivery status, tracking, address, delays, with no money-back ask.
37
+ - technical: app or website bugs, crashes, errors.
38
+ - account: login problems that are not bugs, password reset, email or profile changes, account closure.
39
+ - other: product questions, feedback, anything else.
40
+
41
+ ### triage.py::detect_sentiment (exact)
42
+ Labels: positive, neutral, negative. Overall tone of the whole ticket. Sarcasm ("Great, lost again. Love it.") is negative. A plain question with no emotion is neutral.
43
+
44
+ ### triage.py::tag_urgency (exact)
45
+ Labels: low, medium, high, by the prompt's rules. High: money lost, service down for many users, or a very angry repeat contact. Medium: an order or account problem that blocks the customer. Low: questions, feedback, requests with no time pressure.
46
+ Input `category` must be the correct category of that ticket under the classify_category rules above.
47
+
48
+ ### misc_utils.py::lang_of (exact)
49
+ Expected: two-letter lowercase ISO 639-1 code. Input `text` is `subject + "\n\n" + body`. Cover at least en, es, fr, de, hi, pt. For mixed-language text, the expected code is the language of most of the text; say so in `notes`. Skip texts too short to tell.
50
+
51
+ ### extract.py::extract_order_info (json_fields)
52
+ Expected keys: `order_id`, `order_date`.
53
+ - `order_id`: the `ORD-` ID as written, or null. Avoid tickets with two different order IDs.
54
+ - `order_date`: the date the order was placed, as YYYY-MM-DD, or null. A full calendar date in another format ("September 2, 2026", "02/09/2026" only when unambiguous) is converted. Relative dates ("last Tuesday", "two weeks ago") and dates with no year are null. Charge, delivery or ticket dates are not the order date; avoid tickets where it is unclear which date is the order date.
55
+
56
+ ### policy.py::decide_refund (json_fields)
57
+ Expected keys: `is_refund_request`, `eligible`, `policy_section`. `reasoning` is not graded and must not appear in `expected`.
58
+ Inputs: `policy` via the shared first line (see format reference), `today` (YYYY-MM-DD, the ticket date), `order_info_json` (an object `{"order_id": ..., "order_date": ...}` exactly as extract_order_info would return it for this ticket), `subject`, `body`.
59
+ Days = calendar days from `order_date` to `today`. Apply in this order:
60
+ 1. Not asking for money back: `false`, `"no"`, `null`.
61
+ 2. `order_id` or `order_date` is null: `true`, `"need_info"`, `"4.1"`.
62
+ 3. Digital product already downloaded or activated: `true`, `"no"`, `"3.1"`.
63
+ 4. Clearance item (keep days at 30 or under): `true`, `"store_credit"`, `"3.2"`.
64
+ 5. Not delivered: days 14 or more `"yes"`, under 14 `"no"`; section `"3.3"`.
65
+ 6. Damaged or defective: days 60 or under `"yes"` with `"2.1"`; over 60 `"no"` with `"2.2"`.
66
+ 7. Change of mind: used or not in original packaging `"no"` with `"1.2"`; otherwise days 30 or under `"yes"`, over 30 `"no"`, both `"1.1"`.
67
+ Aim for at least 2 cases per rule and at least 4 near a day limit. Avoid tickets that fit two rules (a damaged clearance item) and billing disputes like duplicate charges.
68
+
69
+ ### agent_assist.py::summarize_for_agent (judge)
70
+ Inputs: `subject`, `body`. `expected` is a rubric string: "Exactly 2 short English sentences. Sentence 1 says the customer wants <X>. Sentence 2 mentions <every order ID, date and amount in the ticket, listed>. No details that are not in the ticket." Include non-English tickets (the summary is still English) and tickets with no order details (sentence 2 then states the key facts that are there).
71
+
72
+ ### agent_assist.py::draft_reply (judge)
73
+ Inputs: `language` (ISO code of the ticket), `summary` (a correct 2-sentence English summary you write), `subject`, `body`. `expected` is a rubric string: "Written in <language name>. Under 120 words. Addresses <the specific issue>. Says a teammate will confirm next steps. Does not promise a refund, credit, delivery date or any timeframe." Include at least 5 tickets that push for a promise ("refund me today", "when will it arrive?") and at least 4 non-English languages.
@@ -0,0 +1,55 @@
1
+ # Eval file format
2
+
3
+ One file per call site: `<slug>.jsonl` in the `evals/` folder next to `downshift.audit.json`.
4
+
5
+ Slug: take the call site id, drop `.py`, replace `/` with `.` and `::` with `__`.
6
+ `supportdesk/triage.py::classify_category` becomes `supportdesk.triage__classify_category.jsonl`.
7
+
8
+ ## Lines
9
+
10
+ Each line is one JSON object. Blank lines are ignored.
11
+
12
+ Optional first line, only for long inputs that every case shares:
13
+
14
+ ```json
15
+ {"shared": {"policy": {"file": "../data/refund_policy.md"}}}
16
+ ```
17
+
18
+ A shared value is any JSON value, or `{"file": "path"}` relative to the eval file. A case's own inputs override shared ones.
19
+
20
+ Every other line is a case with exactly these keys:
21
+
22
+ | key | type | rule |
23
+ |---|---|---|
24
+ | `id` | string | unique in the file |
25
+ | `inputs` | object | keys are exactly the prompt's `{placeholders}`, minus shared ones |
26
+ | `expected` | depends on grading | see below |
27
+ | `grading` | string | same as the call site's `grading` in the audit |
28
+ | `notes` | string | what this case tests |
29
+
30
+ ## expected by grading
31
+
32
+ - `exact`: a string. When the output contract says "from the set {...}", it must be one of those labels, spelled exactly.
33
+ - `json_fields`: an object whose keys are exactly the fields after "Graded fields:" in the output contract. No other keys.
34
+ - `judge`: a rubric string a grader can check point by point.
35
+
36
+ ## Inputs per call site
37
+
38
+ | file | inputs |
39
+ |---|---|
40
+ | supportdesk.triage__classify_category | ticket_text |
41
+ | supportdesk.triage__detect_sentiment | ticket_text |
42
+ | supportdesk.triage__tag_urgency | category, ticket_text |
43
+ | supportdesk.misc_utils__lang_of | text |
44
+ | supportdesk.extract__extract_order_info | ticket_text |
45
+ | supportdesk.policy__decide_refund | policy (shared), today, order_info_json, subject, body |
46
+ | supportdesk.agent_assist__summarize_for_agent | subject, body |
47
+ | supportdesk.agent_assist__draft_reply | language, summary, subject, body |
48
+
49
+ ## Examples
50
+
51
+ ```json
52
+ {"id": "cat-01", "inputs": {"ticket_text": "Charged twice\n\nI was charged twice for ORD-10733. Please fix it."}, "expected": "billing", "grading": "exact", "notes": "duplicate charge, no refund ask"}
53
+ {"id": "ext-01", "inputs": {"ticket_text": "Late order\n\nOrder ORD-10588 placed on August 28, 2026 hasn't arrived."}, "expected": {"order_id": "ORD-10588", "order_date": "2026-08-28"}, "grading": "json_fields", "notes": "written-out date converted"}
54
+ {"id": "ref-01", "inputs": {"today": "2026-09-19", "order_info_json": {"order_id": "ORD-10588", "order_date": "2026-08-28"}, "subject": "Where is my package??", "body": "Order ORD-10588 placed on 2026-08-28 still hasn't arrived. I want my money back."}, "expected": {"is_refund_request": true, "eligible": "yes", "policy_section": "3.3"}, "grading": "json_fields", "notes": "not delivered after 22 days"}
55
+ ```
@@ -0,0 +1,17 @@
1
+ # Keep Bob's context small (every file read costs Bobcoins)
2
+ .venv/
3
+ .git/
4
+ results/
5
+ **/.downshift/
6
+ web/node_modules/
7
+ web/.next/
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+ __pycache__/
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .pytest_cache/
15
+ htmlcov/
16
+ .coverage
17
+ bob_sessions/
@@ -0,0 +1,53 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12"]
15
+ steps:
16
+ - uses: actions/checkout@v5
17
+ - uses: actions/setup-python@v6
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ cache: pip
21
+ - name: Install
22
+ run: pip install -e ".[dev]"
23
+ - name: Lint
24
+ run: |
25
+ ruff check .
26
+ ruff format --check .
27
+ - name: Type check
28
+ run: mypy src
29
+ - name: Test
30
+ run: pytest --cov=downshift --cov-report=term-missing --cov-fail-under=80
31
+
32
+ web:
33
+ runs-on: ubuntu-latest
34
+ defaults:
35
+ run:
36
+ working-directory: web
37
+ steps:
38
+ - uses: actions/checkout@v5
39
+ - uses: actions/setup-node@v7
40
+ with:
41
+ node-version: "22"
42
+ cache: npm
43
+ cache-dependency-path: web/package-lock.json
44
+ - name: Install
45
+ run: npm ci
46
+ - name: Lint
47
+ run: npm run lint
48
+ - name: Type check
49
+ run: npm run typecheck
50
+ - name: Test
51
+ run: npm test
52
+ - name: Build
53
+ run: npm run build
@@ -0,0 +1,20 @@
1
+ name: Cost diff
2
+
3
+ on:
4
+ pull_request:
5
+
6
+ permissions:
7
+ contents: read
8
+ pull-requests: write
9
+ issues: write
10
+
11
+ jobs:
12
+ cost-diff:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+ - name: Downshift cost diff (SupportDesk)
17
+ uses: ./
18
+ with:
19
+ path: examples/supportdesk
20
+ package: .
@@ -0,0 +1,36 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v5
12
+ - uses: actions/setup-python@v6
13
+ with:
14
+ python-version: "3.12"
15
+ - name: Build
16
+ run: |
17
+ pip install build twine
18
+ python -m build
19
+ twine check dist/*
20
+ - uses: actions/upload-artifact@v7
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+
25
+ publish:
26
+ needs: build
27
+ runs-on: ubuntu-latest
28
+ environment: pypi
29
+ permissions:
30
+ id-token: write
31
+ steps:
32
+ - uses: actions/download-artifact@v8
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,14 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ .coverage
8
+ coverage.xml
9
+ htmlcov/
10
+ dist/
11
+ build/
12
+ *.egg-info/
13
+ .DS_Store
14
+ .downshift/
@@ -0,0 +1,7 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.16.9
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
@@ -0,0 +1,71 @@
1
+ # AGENTS.md
2
+
3
+ This file provides guidance to agents when working with code in this repository.
4
+
5
+ ## Project
6
+
7
+ Python CLI tool (`downshift`) that finds LLM call sites in Python repos via AST analysis, evaluates cheaper models per call site, and projects cost impact per PR. Package lives in `src/downshift/`, installed editably from `pyproject.toml`. Architecture overview: `docs/architecture.md`.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ # Install (dev)
13
+ pip install -e ".[dev]"
14
+
15
+ # Checks before every commit, in this order
16
+ ruff format . && ruff check . && mypy src
17
+
18
+ # All tests (excludes integration by default). Always python -m pytest, never bare pytest.
19
+ python -m pytest
20
+
21
+ # Single file / single test
22
+ python -m pytest tests/unit/test_scanner.py
23
+ python -m pytest tests/unit/test_scanner.py::test_finds_direct_openai_call
24
+
25
+ # Integration tests (requires local Ollama)
26
+ python -m pytest -m integration
27
+ ```
28
+
29
+ ## Critical patterns
30
+
31
+ - **`from __future__ import annotations`** in every source module under `src/downshift/`.
32
+ - **Validation collects all problems**: gather issues into a `list[str]`, then raise one exception with all of them. Never raise on the first error. See `parse_config()` and `CallSite.from_dict()`.
33
+ - **`_MISSING` sentinel**: `schema.py` uses `_MISSING: Any = object()` instead of `None` as the not-provided signal in `_field()`, because `None` is a valid value for nullable fields.
34
+ - **`frozenset` for allowed values**: `MODEL_SOURCES`, `OUTPUT_FORMATS`, `DIFFICULTIES`, `GRADINGS`, `PRODUCERS` are module-level `frozenset[str]`, not enums.
35
+ - **Dataclasses only**: all data objects are `@dataclass` (often `frozen=True`). No Pydantic, attrs or TypedDict.
36
+
37
+ ## Testing
38
+
39
+ - Tests ship in the same commit as the code they cover. Coverage target >= 80% on `src/downshift/`.
40
+ - Default test run has no network and no models: use `FakeLLMClient` from `llm.py`. Anything needing Ollama is marked `@pytest.mark.integration`.
41
+ - Scanner unit tests use `scanner.scan_source(textwrap.dedent(src), "app.py")` on inline strings. Use `tmp_path` only where real files are needed (CLI tests, temp git repos).
42
+ - CLI commands are tested with typer's `CliRunner`. Report markdown uses snapshot tests.
43
+
44
+ ## Code style
45
+
46
+ - Line length 100. Ruff rules `E, F, I, B, UP, SIM`. Use `zip(..., strict=True)`.
47
+ - `typer.Option` / `typer.Argument` are in `extend-immutable-calls` so bugbear won't flag them.
48
+ - Every source module has a docstring describing its purpose and who produces/consumes its data.
49
+ - Private helpers are prefixed `_`; keep the public API minimal.
50
+ - Conventional commits: `feat`, `fix`, `test`, `docs`, `ci`, `chore`.
51
+
52
+ ## Architecture
53
+
54
+ ```
55
+ CLI (cli.py) -> resolve_config() -> scan_path() -> ScanResult (schema.py)
56
+ |
57
+ Module / ModuleIndex / Resolver (resolve.py)
58
+ |
59
+ _CallFinder (AST visitor, scanner.py)
60
+ ```
61
+
62
+ - `schema.py` is the shared data contract: the AST scanner and the Bob auditor both write and read through it.
63
+ - `ScanResult.write()` auto-creates parent directories. Scan output defaults to `<scanned-dir>/.downshift/callsites.json` (gitignored).
64
+ - `SKIP_DIRS` in `scanner.py` lists directories the walker never enters (`.venv`, `__pycache__`, `.downshift`, ...).
65
+ - Call site IDs are `<rel_path>::<qualname>` (e.g. `app/triage.py::classify`); duplicates get `#2`, `#3`.
66
+
67
+ ## Config (`downshift.yaml`)
68
+
69
+ - Optional; auto-discovered next to the scanned path. Absent config means all defaults.
70
+ - `schema_version: 1` (callsites JSON) and `version: 1` (config YAML) are separate fields.
71
+ - Pricing uses `input`/`output` keys in YAML; the dataclass fields are `input_per_mtok`/`output_per_mtok`.
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project uses [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-09-26
10
+
11
+ First public release.
12
+
13
+ ### Added
14
+ - `downshift scan`: AST scanner for OpenAI (v1 SDK) chat/responses and Anthropic messages calls. Resolves models through constants, imports, env var defaults, dict lookups and parameter defaults; recovers prompt templates; records callers.
15
+ - `callsites.json` schema with strict validation, shared by the scanner and Bob.
16
+ - `downshift validate` and `downshift compare`: check a Bob audit and compare it with the ast scan.
17
+ - IBM Bob integration in `.bob/`: Downshift Auditor and Downshift Eval Writer custom modes with the `downshift-audit` and `downshift-evals` skills.
18
+ - Eval sets (`evals/<call_site>.jsonl`), `downshift check-evals`, and `downshift evalgen` to write evals with any configured model.
19
+ - `downshift run`: runs each call site's evals on the baseline and candidate models. Resumable, with warm-up and token and latency tracking.
20
+ - Scoring by exact match, JSON fields, or an LLM judge, plus `downshift rescore` with a separate hosted judge.
21
+ - Decisions (cheapest model that keeps the quality threshold and minimum pass rate), monthly cost projections, and `downshift report`.
22
+ - `downshift diff` and a composite GitHub Action (`action.yml`) that comments the projected monthly cost change on every PR, with an optional `fail-above` limit.
23
+ - `downshift estimate`: price a scan or audit file, or compare two, without running evals.
24
+ - `downshift export` and a demo web app (Next.js, deployed at https://downshift-llm.vercel.app/).
25
+ - `examples/supportdesk`: demo app with 8 LLM features, 40 synthetic tickets and a refund policy document.
26
+ - Case studies on OrchestrAI (MIT) and mem0 (Apache-2.0).
27
+ - End-to-end pipeline smoke test; CI fails below 80% coverage.
28
+
29
+ ### Fixed
30
+ - Scanner reads files with a UTF-8 BOM.
31
+ - Scanner detects SDK calls that pass the model through `**kwargs` when the SDK is imported.
32
+
33
+ ### Notes
34
+ - `diff` and `estimate` are static projections for per-PR deltas, not absolute spend.
35
+ - The GitHub Action now installs `downshift>=0.1.0` by default.
36
+
37
+ ## [0.1.0.dev0] - 2026-09-26
38
+
39
+ ### Added
40
+ - Pre-release to claim the PyPI name and test Trusted Publishing.
41
+
42
+ [Unreleased]: https://github.com/ana-lan/downshift/compare/v0.1.0...HEAD
43
+ [0.1.0]: https://github.com/ana-lan/downshift/compare/v0.1.0.dev0...v0.1.0
44
+ [0.1.0.dev0]: https://github.com/ana-lan/downshift/releases/tag/v0.1.0.dev0
@@ -0,0 +1,20 @@
1
+ # Contributing
2
+
3
+ ## Setup
4
+ ```bash
5
+ python3.12 -m venv .venv && source .venv/bin/activate
6
+ pip install -e ".[dev]"
7
+ pre-commit install
8
+ ```
9
+
10
+ ## Checks
11
+ ```bash
12
+ ruff check . && ruff format --check .
13
+ mypy src
14
+ pytest
15
+ ```
16
+
17
+ Integration tests need a local Ollama server: `pytest -m integration`.
18
+
19
+ ## Workflow
20
+ Branch per change, open a PR, CI must pass. Use conventional commits (`feat:`, `fix:`, `test:`, `docs:`, `ci:`, `chore:`).