skilldrop-cli 0.16.29 → 0.16.31

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 (231) hide show
  1. package/bin/skilldrop.js +160 -33
  2. package/catalogue.json +10 -4
  3. package/model-routing.json +24 -0
  4. package/package.json +1 -1
  5. package/packs/ai-engineering/skills/mcp-pairing/SKILL.md +93 -0
  6. package/packs/ai-engineering/skills/mcp-pairing/evals/eval_queries.json +13 -0
  7. package/packs/ai-engineering/skills/mcp-pairing/evals/evals.json +76 -0
  8. package/packs/ai-engineering/skills/mcp-pairing/evals/files/2/mcp.json +32 -0
  9. package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/.mcp.json +47 -0
  10. package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/docs/pr-loop.md +12 -0
  11. package/packs/ai-engineering/skills/mcp-pairing/evals/files/5/docs/oncall-triage.md +18 -0
  12. package/packs/ai-engineering/skills/mcp-pairing/examples/build-loop-github-jira-datadog.md +69 -0
  13. package/packs/ai-engineering/skills/mcp-pairing/manifest.json +17 -0
  14. package/packs/ai-engineering/skills/mcp-pairing/reference.md +136 -0
  15. package/packs/ai-engineering/skills/mcp-pairing/templates/mcp-pairing.md +43 -0
  16. package/packs/dev-team/loops/build/LOOP.md +4 -2
  17. package/packs/dev-team/loops/build/loop.json +2 -2
  18. package/packs/dev-team/loops/release/LOOP.md +7 -5
  19. package/packs/dev-team/loops/release/loop.json +2 -2
  20. package/packs/dev-team/skills/codebase-tour/SKILL.md +115 -0
  21. package/packs/dev-team/skills/codebase-tour/evals/eval_queries.json +13 -0
  22. package/packs/dev-team/skills/codebase-tour/evals/evals.json +77 -0
  23. package/packs/dev-team/skills/codebase-tour/evals/files/1/.github/workflows/ci.yml +23 -0
  24. package/packs/dev-team/skills/codebase-tour/evals/files/1/README.md +23 -0
  25. package/packs/dev-team/skills/codebase-tour/evals/files/1/openapi.yaml +21 -0
  26. package/packs/dev-team/skills/codebase-tour/evals/files/1/package-lock.json +7 -0
  27. package/packs/dev-team/skills/codebase-tour/evals/files/1/package.json +29 -0
  28. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/config.ts +20 -0
  29. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/filter.ts +24 -0
  30. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/repository.ts +15 -0
  31. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/errors.ts +9 -0
  32. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/generated/api-types.ts +9 -0
  33. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/handlers/shipments.ts +38 -0
  34. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/routes.ts +7 -0
  35. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/jobs/export-shipments.ts +22 -0
  36. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/server.ts +18 -0
  37. package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/domain/filter.test.ts +16 -0
  38. package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/http/shipments.test.ts +18 -0
  39. package/packs/dev-team/skills/codebase-tour/evals/files/1/tsconfig.json +10 -0
  40. package/packs/dev-team/skills/codebase-tour/evals/files/1/vitest.config.ts +5 -0
  41. package/packs/dev-team/skills/codebase-tour/evals/files/2/.github/workflows/ci.yml +16 -0
  42. package/packs/dev-team/skills/codebase-tour/evals/files/2/README.md +16 -0
  43. package/packs/dev-team/skills/codebase-tour/evals/files/2/noxfile.py +14 -0
  44. package/packs/dev-team/skills/codebase-tour/evals/files/2/pyproject.toml +22 -0
  45. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/__init__.py +3 -0
  46. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/_version.py +4 -0
  47. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/cli.py +32 -0
  48. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/detect.py +15 -0
  49. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/errors.py +2 -0
  50. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/money.py +14 -0
  51. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/__init__.py +8 -0
  52. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/acme.py +18 -0
  53. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/base.py +24 -0
  54. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/harbor.py +13 -0
  55. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/fixtures/acme_sample.csv +3 -0
  56. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_acme.py +23 -0
  57. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_detect.py +13 -0
  58. package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +28 -0
  59. package/packs/dev-team/skills/codebase-tour/evals/files/3/Makefile +16 -0
  60. package/packs/dev-team/skills/codebase-tour/evals/files/3/README.md +16 -0
  61. package/packs/dev-team/skills/codebase-tour/evals/files/3/cmd/tidepool/main.go +24 -0
  62. package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +5 -0
  63. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/config/config.go +38 -0
  64. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher.go +52 -0
  65. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher_test.go +42 -0
  66. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/sender.go +32 -0
  67. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/replay/replay.go +31 -0
  68. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/db.go +35 -0
  69. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql +11 -0
  70. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql.go +30 -0
  71. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/schema.sql +6 -0
  72. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/store_integration_test.go +21 -0
  73. package/packs/dev-team/skills/codebase-tour/evals/files/3/sqlc.yaml +9 -0
  74. package/packs/dev-team/skills/codebase-tour/evals/files/4/.env.example +4 -0
  75. package/packs/dev-team/skills/codebase-tour/evals/files/4/.github/workflows/ci.yml +16 -0
  76. package/packs/dev-team/skills/codebase-tour/evals/files/4/Makefile +16 -0
  77. package/packs/dev-team/skills/codebase-tour/evals/files/4/README.md +23 -0
  78. package/packs/dev-team/skills/codebase-tour/evals/files/4/data/rates.csv +4 -0
  79. package/packs/dev-team/skills/codebase-tour/evals/files/4/pyproject.toml +19 -0
  80. package/packs/dev-team/skills/codebase-tour/evals/files/4/scripts/gen_rates.py +29 -0
  81. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/__init__.py +0 -0
  82. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/__init__.py +0 -0
  83. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/preview.py +14 -0
  84. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/routes.py +36 -0
  85. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/config.py +15 -0
  86. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/discounts.py +8 -0
  87. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/__init__.py +0 -0
  88. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/rates_table.py +8 -0
  89. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/main.py +8 -0
  90. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/shipping.py +18 -0
  91. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/tax.py +7 -0
  92. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/conftest.py +3 -0
  93. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_discounts.py +11 -0
  94. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_tax.py +13 -0
  95. package/packs/dev-team/skills/codebase-tour/evals/files/5/.env.example +3 -0
  96. package/packs/dev-team/skills/codebase-tour/evals/files/5/.github/workflows/ci.yml +13 -0
  97. package/packs/dev-team/skills/codebase-tour/evals/files/5/README.md +18 -0
  98. package/packs/dev-team/skills/codebase-tour/evals/files/5/package.json +13 -0
  99. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/openapi.json +30 -0
  100. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/package.json +15 -0
  101. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/db.js +19 -0
  102. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/flags.js +8 -0
  103. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/generated/schema.js +32 -0
  104. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/middleware/validate.js +20 -0
  105. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/routes/bookings.js +15 -0
  106. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/server.js +15 -0
  107. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/bookings.js +23 -0
  108. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/pricing.js +9 -0
  109. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/test/pricing.test.js +15 -0
  110. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/package.json +9 -0
  111. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/bookingForm.js +11 -0
  112. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/format.js +7 -0
  113. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/test/format.test.js +12 -0
  114. package/packs/dev-team/skills/codebase-tour/evals/files/5/scripts/gen-schema.mjs +27 -0
  115. package/packs/dev-team/skills/codebase-tour/examples/refund-window.md +158 -0
  116. package/packs/dev-team/skills/codebase-tour/manifest.json +47 -0
  117. package/packs/dev-team/skills/codebase-tour/scripts/repo_facts.py +403 -0
  118. package/packs/dev-team/skills/codebase-tour/templates/codebase-tour.md +85 -0
  119. package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +101 -0
  120. package/packs/dev-team/skills/dependency-upgrade-review/evals/eval_queries.json +11 -0
  121. package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +77 -0
  122. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/.github/workflows/ci.yml +20 -0
  123. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/pr.diff +87 -0
  124. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/quillmark-CHANGELOG.md +24 -0
  125. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/comment.js +6 -0
  126. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/note.js +8 -0
  127. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/test/note.test.js +12 -0
  128. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/ci.yml +23 -0
  129. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/release.yml +23 -0
  130. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/pr.diff +32 -0
  131. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/release-notes.md +20 -0
  132. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/.github/workflows/ci.yml +18 -0
  133. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/Dockerfile +8 -0
  134. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/PR.md +36 -0
  135. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/pr.diff +47 -0
  136. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/client.py +24 -0
  137. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/reader.py +32 -0
  138. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/tests/test_reader.py +15 -0
  139. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/upstream/quillstream-CHANGELOG.md +37 -0
  140. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/ci.yml +22 -0
  141. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/release.yml +25 -0
  142. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.nvmrc +1 -0
  143. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-412.md +16 -0
  144. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-413.md +15 -0
  145. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/package.json +21 -0
  146. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-412.diff +26 -0
  147. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-413.diff +84 -0
  148. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/scripts/release.mjs +18 -0
  149. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/release-notary-CHANGELOG.md +16 -0
  150. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/setup-toolchain-RELEASES.md +22 -0
  151. package/packs/dev-team/skills/dependency-upgrade-review/examples/inputs/ledgerline.diff +38 -0
  152. package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +91 -0
  153. package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +50 -0
  154. package/packs/dev-team/skills/dependency-upgrade-review/reference.md +99 -0
  155. package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +718 -0
  156. package/packs/dev-team/skills/refactor-plan/SKILL.md +168 -0
  157. package/packs/dev-team/skills/refactor-plan/evals/eval_queries.json +46 -0
  158. package/packs/dev-team/skills/refactor-plan/evals/evals.json +106 -0
  159. package/packs/dev-team/skills/refactor-plan/evals/files/1/debt-register-row.md +5 -0
  160. package/packs/dev-team/skills/refactor-plan/evals/files/1/in-flight-prs.md +7 -0
  161. package/packs/dev-team/skills/refactor-plan/evals/files/1/order_service.py +74 -0
  162. package/packs/dev-team/skills/refactor-plan/evals/files/1/test-coverage.txt +14 -0
  163. package/packs/dev-team/skills/refactor-plan/evals/files/3/imports.txt +22 -0
  164. package/packs/dev-team/skills/refactor-plan/evals/files/3/register.md +3 -0
  165. package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/open-prs.md +6 -0
  166. package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/tech-debt-register.md +7 -0
  167. package/packs/dev-team/skills/refactor-plan/evals/files/4/reports/monthly_revenue.py +44 -0
  168. package/packs/dev-team/skills/refactor-plan/evals/files/4/src/billing/invoice.py +233 -0
  169. package/packs/dev-team/skills/refactor-plan/evals/files/4/src/partners/statement.py +29 -0
  170. package/packs/dev-team/skills/refactor-plan/evals/files/4/tests/billing/test_payments.py +13 -0
  171. package/packs/dev-team/skills/refactor-plan/evals/files/5/ci-notes.md +5 -0
  172. package/packs/dev-team/skills/refactor-plan/evals/files/5/open-prs.txt +4 -0
  173. package/packs/dev-team/skills/refactor-plan/evals/files/5/scripts/nightly-digest.js +36 -0
  174. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/auth/passwordReset.ts +16 -0
  175. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/index.ts +2 -0
  176. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/legacyMailer.ts +149 -0
  177. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/orders/onOrderShipped.ts +15 -0
  178. package/packs/dev-team/skills/refactor-plan/evals/files/5/tests/notify/legacyMailer.test.ts +7 -0
  179. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/DunningJob.kt +29 -0
  180. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/InvoiceService.kt +157 -0
  181. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/pdf/InvoicePdfRenderer.kt +36 -0
  182. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/test/kotlin/billing/InvoiceServiceTest.kt +40 -0
  183. package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/adr-007-persistence.md +26 -0
  184. package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/open-prs.md +9 -0
  185. package/packs/dev-team/skills/refactor-plan/evals/files/7/notes/open-work.md +19 -0
  186. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/CheckoutPanel.tsx +207 -0
  187. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/MiniCart.tsx +33 -0
  188. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/__tests__/CheckoutPanel.test.tsx +13 -0
  189. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/pages/OrderReview.tsx +24 -0
  190. package/packs/dev-team/skills/refactor-plan/examples/search-ranker.md +104 -0
  191. package/packs/dev-team/skills/refactor-plan/manifest.json +36 -0
  192. package/packs/dev-team/skills/refactor-plan/reference.md +149 -0
  193. package/packs/dev-team/skills/refactor-plan/templates/refactor-plan.md +82 -0
  194. package/packs/dev-team/skills/rollout-plan/SKILL.md +157 -0
  195. package/packs/dev-team/skills/rollout-plan/evals/eval_queries.json +12 -0
  196. package/packs/dev-team/skills/rollout-plan/evals/evals.json +109 -0
  197. package/packs/dev-team/skills/rollout-plan/evals/files/2/metrics-snapshot.txt +11 -0
  198. package/packs/dev-team/skills/rollout-plan/evals/files/2/pr-description.md +28 -0
  199. package/packs/dev-team/skills/rollout-plan/evals/files/4/PR_DESCRIPTION.md +24 -0
  200. package/packs/dev-team/skills/rollout-plan/evals/files/4/metrics_snapshot.txt +7 -0
  201. package/packs/dev-team/skills/rollout-plan/evals/files/5/docs/tax-engine-v2.md +24 -0
  202. package/packs/dev-team/skills/rollout-plan/evals/files/6/flags/smart_reminders_v2.yaml +14 -0
  203. package/packs/dev-team/skills/rollout-plan/evals/files/6/notes/push-pipeline.md +10 -0
  204. package/packs/dev-team/skills/rollout-plan/evals/files/7/docs/ratelimiter-v2.md +21 -0
  205. package/packs/dev-team/skills/rollout-plan/evals/files/7/ops/traffic-summary.txt +9 -0
  206. package/packs/dev-team/skills/rollout-plan/examples/sync-engine-workspaces.md +102 -0
  207. package/packs/dev-team/skills/rollout-plan/manifest.json +35 -0
  208. package/packs/dev-team/skills/rollout-plan/reference.md +100 -0
  209. package/packs/dev-team/skills/rollout-plan/scripts/canary_math.py +261 -0
  210. package/packs/dev-team/skills/rollout-plan/templates/rollout-plan.md +50 -0
  211. package/packs/product-manager/loops/discover/LOOP.md +5 -4
  212. package/packs/product-manager/loops/discover/loop.json +2 -2
  213. package/packs/product-manager/skills/feedback-synthesis/SKILL.md +182 -0
  214. package/packs/product-manager/skills/feedback-synthesis/evals/eval_queries.json +46 -0
  215. package/packs/product-manager/skills/feedback-synthesis/evals/evals.json +76 -0
  216. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/interview-notes.md +47 -0
  217. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/nps-verbatims.csv +13 -0
  218. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/support-tickets.csv +36 -0
  219. package/packs/product-manager/skills/feedback-synthesis/evals/files/3/churn-exit-survey.csv +11 -0
  220. package/packs/product-manager/skills/feedback-synthesis/evals/files/3/sales-call-notes.md +55 -0
  221. package/packs/product-manager/skills/feedback-synthesis/evals/files/4/csat_comments_q3.csv +17 -0
  222. package/packs/product-manager/skills/feedback-synthesis/evals/files/4/support_tickets_q3.csv +49 -0
  223. package/packs/product-manager/skills/feedback-synthesis/evals/files/5/app_reviews_last_60_days.csv +32 -0
  224. package/packs/product-manager/skills/feedback-synthesis/evals/files/5/interview_notes_round1.md +48 -0
  225. package/packs/product-manager/skills/feedback-synthesis/examples/booking-app-feedback.md +139 -0
  226. package/packs/product-manager/skills/feedback-synthesis/examples/inputs/booking-app-coded.csv +17 -0
  227. package/packs/product-manager/skills/feedback-synthesis/manifest.json +53 -0
  228. package/packs/product-manager/skills/feedback-synthesis/reference.md +89 -0
  229. package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +355 -0
  230. package/packs/product-manager/skills/feedback-synthesis/templates/coding-sheet.csv +6 -0
  231. package/packs/product-manager/skills/feedback-synthesis/templates/synthesis.md +42 -0
@@ -0,0 +1,77 @@
1
+ {
2
+ "skill_name": "codebase-tour",
3
+ "evals": [
4
+ {
5
+ "id": 1,
6
+ "prompt": "I'm new to this repo (it's the current directory) and my first ticket is to add a `carrier` filter to GET /shipments, so dispatchers can see one carrier's shipments at a time. Get me oriented before I start: how to run it, where the change goes, and anything that will bite me.",
7
+ "assertions": [
8
+ "Opens with a short list of the few things that matter most for this change, before any section, and does not open with a directory listing",
9
+ "Every command given is cited to package.json or .github/workflows/ci.yml and marked as either run or only read; `npm run dev` is not given as a way to run the server",
10
+ "Says where the README and the code disagree: there is no `dev` script (package.json has `start:watch`), there is no `src/routes/` (routes are registered in src/http/routes.ts), and config comes from env vars in src/config.ts, not config/default.json",
11
+ "Says the change goes in `parseListQuery` (src/http/handlers/shipments.ts) and `buildShipmentFilter` (src/domain/shipments/filter.ts), and that openapi.yaml needs the new query parameter",
12
+ "Names src/generated/api-types.ts as generated from openapi.yaml by `npm run gen:types`, says not to edit it by hand, and notes that CI fails if it drifts (.github/workflows/ci.yml)",
13
+ "Names src/jobs/export-shipments.ts as a second copy of the shipment filtering that the change does not reach on its own, notes it has no tests, and raises whether it needs the carrier filter too",
14
+ "Names tests/http/shipments.test.ts and tests/domain/filter.test.ts as the tests for the area, with a way to run just those through the repo's vitest script, and notes DATABASE_URL is required to start the server (src/config.ts)",
15
+ "Does not invent commands, files, scripts or tools that are not in the repo"
16
+ ]
17
+ },
18
+ {
19
+ "id": 2,
20
+ "prompt": "Never touched this codebase before. I need to add support for Northwind Credit Union's CSV export (header: Transaction Date, Merchant, Amount; dates like 2026-03-02). Where do I start, and how do I run the tests? The repo is the folder you're in.",
21
+ "assertions": [
22
+ "Says the README is wrong that a new parser module is picked up automatically: the PARSERS registry in src/ledgerline/parsers/__init__.py has to list it",
23
+ "Names HEADER_SIGNATURES in src/ledgerline/detect.py as a second place the new bank must be added, or detection fails for the new header",
24
+ "Gives the test command from noxfile.py or CI (`nox -s tests`, `nox -s lint tests`), says `python setup.py test` from the README cannot work because there is no setup.py, and notes the tests session skips tests marked `slow`",
25
+ "Gives a way to run a single test file through the runner the repo already uses (nox posargs or pytest), labelled as derived from the repo's config rather than presented as a defined command",
26
+ "Points to AcmeParser as the parser to copy and flags that HarborParser has no tests and does not use parse_amount or raise ParseError like the others",
27
+ "Identifies src/ledgerline/_version.py as generated by setuptools-scm and not to be edited",
28
+ "Each convention it states (ParseError naming the column, money.parse_amount for amounts, CSV samples in tests/fixtures/) is cited to a file in the repo",
29
+ "Does not invent commands, files, scripts or tools that are not in the repo"
30
+ ]
31
+ },
32
+ {
33
+ "id": 3,
34
+ "prompt": "I've got a ticket to switch webhook delivery retries in this repo to exponential backoff. I don't really know Go and I've never seen this code. Write me an orientation to docs/backoff-tour.md and don't change anything else.",
35
+ "assertions": [
36
+ "Writes docs/backoff-tour.md and modifies no other file in the repo",
37
+ "Says the README is wrong about config.yaml: there is no such file, and retry settings are the env vars TIDEPOOL_MAX_ATTEMPTS and TIDEPOOL_RETRY_DELAY read in internal/config/config.go",
38
+ "Says the README is wrong that `make test` includes the database tests: they sit behind the `integration` build tag and run with `make test-integration`, which needs Postgres and TIDEPOOL_DATABASE_URL",
39
+ "Names `Deliver` in internal/dispatch/dispatcher.go as where the change goes, and the injectable `sleep` field used by internal/dispatch/dispatcher_test.go as the way to test the delays",
40
+ "Names internal/replay/replay.go as a second retry loop with its own fixed delay that the change must also update or deliberately leave, and notes it has no tests",
41
+ "Names internal/store/query.sql.go as generated by sqlc from query.sql, regenerated with `make generate`, and not to be edited by hand",
42
+ "Every command is cited to the Makefile or .github/workflows/ci.yml and marked as run or only read; it does not claim to have run the integration tests",
43
+ "Does not invent commands, files, scripts or tools that are not in the repo"
44
+ ]
45
+ },
46
+ {
47
+ "id": 4,
48
+ "held_out": "2026-10-20",
49
+ "prompt": "Hi! I joined the checkout team on Monday and I've been handed ticket SHIP-214: \"Raise the EU free-shipping threshold from EUR 50 to EUR 60. UK and US stay as they are.\" I'm new to this repo and haven't used FastAPI much. Before I start, can you get me oriented: how do I install, run and test this thing, and where exactly would I make the change?",
50
+ "assertions": [
51
+ "Gives install/run/test commands taken from the Makefile and .github/workflows/ci.yml (`make install` / `pip install -e \".[dev]\"`, `make run` / `uvicorn shipcalc.main:app`, `make test` / pytest) and explicitly says the README's `pip install -r requirements.txt`, `flask run` and `python -m unittest discover` are stale (there is no requirements.txt and the app is FastAPI, not Flask).",
52
+ "Says the EU threshold's source of truth is data/rates.csv (line 2, `free_threshold` column), that src/shipcalc/generated/rates_table.py is generated (DO NOT EDIT header on line 1) and must not be hand-edited, and that the change is: edit the CSV, then run `make gen`; it notes that CI's `make check-generated` step fails if the generated file is out of sync with the CSV.",
53
+ "Finds the second, hard-coded copy of the threshold in src/shipcalc/api/preview.py line 5 (`FREE_SHIPPING_MIN`, used by the /preview/banner route) and says it must also change to 60, or the cart banner and the actual quote will disagree.",
54
+ "Traces the quote path with file:line citations: the /quote handler in src/shipcalc/api/routes.py (shipping_cost call at line 24) -> shipping_cost in src/shipcalc/shipping.py (line 13) -> the threshold comparison against RATES[region][\"free_threshold\"] at shipping.py line 15.",
55
+ "Flags the feature flag SHIPCALC_FF_FREE_SHIPPING read in src/shipcalc/shipping.py line 10 (free shipping applies only when it is exactly \"on\", and it is not in .env.example or the Makefile run target), so a local manual check of the new threshold will show no change unless the flag is set; also notes SHIPCALC_API_KEY is required at startup (src/shipcalc/config.py line 12).",
56
+ "Points out that shipping.py and preview.py have no tests while discounts and tax do (tests/unit/test_discounts.py, tests/unit/test_tax.py), and recommends adding boundary tests (e.g. 59.99 vs 60.00 for EU, plus UK/US unchanged) that follow the existing style: pytest functions, Decimal money values, and the flag set via monkeypatch or the environment.",
57
+ "Ends with one concrete first step whose result can be checked (e.g. run `make test` and see it pass before changing anything, or edit the CSV, run `make gen` and confirm `git diff` touches only the EU line of rates_table.py), and keeps the tour focused on this ticket instead of walking through tax and discounts in detail.",
58
+ "Invents no commands, files, functions or settings that are not in the repo (e.g. no requirements.txt, no `python -m shipcalc` or `flask run` as a working command, no existing test_shipping.py, no config value for the threshold outside rates.csv/preview.py)."
59
+ ]
60
+ },
61
+ {
62
+ "id": 5,
63
+ "held_out": "2026-10-20",
64
+ "prompt": "First week here, and my ticket is CAB-112: \"Let guests book stays of up to 21 nights (currently capped at 14).\" I'm new to this codebase. Can you walk me through the code for the booking flow, tell me where the change goes, and how I run and test it? I really don't want to break anything on my first PR.",
65
+ "assertions": [
66
+ "Gives commands from the root package.json and .github/workflows/ci.yml: `npm install`, `npm test` (which runs `node --test` in each workspace), `npm run gen` / `npm run gen:check`, and `npm run dev:api` (which reads ../../.env, so .env must be copied from .env.example first). It says the README's `yarn install`, `cd api && yarn start`, `yarn test` and the `api/config.js` reference are stale: the repo uses npm workspaces under packages/ and there is no api/ directory or config.js.",
67
+ "Identifies packages/api/src/generated/schema.js as generated (DO NOT EDIT on line 1, written by scripts/gen-schema.mjs) and says to change the nights `maximum` in packages/api/openapi.json (line 24) and run `npm run gen`, noting that CI's `npm run gen:check` fails if schema.js is hand-edited or stale.",
68
+ "Lists every copy of the 14-night limit that must move to 21: packages/api/openapi.json line 24 (via the regenerated schema.js), packages/api/src/services/bookings.js line 6 (`MAX_NIGHTS`), and packages/web/src/bookingForm.js line 2 (`MAX_NIGHTS`, the client-side check). It warns that changing only one of them gives a confusing 400 or 422, or a form that blocks valid stays. The README line also needs updating.",
69
+ "Traces the POST /bookings request path with file:line citations: packages/api/src/server.js mounts the router -> packages/api/src/routes/bookings.js line 7 runs `validate('BookingRequest')` -> packages/api/src/middleware/validate.js line 15 enforces `maximum` from the generated schema (400) -> createBooking / maxNightsFor in packages/api/src/services/bookings.js enforces MAX_NIGHTS (422).",
70
+ "Flags the `long-stays` feature flag (FEATURE_FLAGS env var parsed in packages/api/src/flags.js; checked at services/bookings.js line 10, giving members LONG_STAY_MAX_NIGHTS = 28). It notes the schema maximum is checked first, so members can never actually reach 28. It asks the ticket owner how the 21-night limit should interact with that flag instead of deciding alone.",
71
+ "Notes that services/bookings.js and web/src/bookingForm.js have no tests while pricing and format do (packages/api/test/pricing.test.js, packages/web/test/format.test.js). It warns that a new test importing services/bookings.js will throw unless DATABASE_URL is set, because packages/api/src/db.js lines 2-4 check it at import time, and suggests setting it (plus FEATURE_FLAGS if needed) before a dynamic import, using node:test and node:assert/strict as the existing tests do.",
72
+ "Ends with one concrete first step that can be checked (e.g. run `npm install && npm test` and see 5 passing tests, or change openapi.json, run `npm run gen` and confirm `npm run gen:check` passes and the diff shows only the nights maximum), scoped to this ticket.",
73
+ "Invents no commands, files, functions or settings that are not in the repo (e.g. no yarn scripts, no api/config.js, no `npm run lint` or `npm run build`, no existing bookings test, no .env file claimed to exist)."
74
+ ]
75
+ }
76
+ ]
77
+ }
@@ -0,0 +1,23 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ubuntu-latest
6
+ services:
7
+ postgres:
8
+ image: postgres:16
9
+ env:
10
+ POSTGRES_PASSWORD: example-only
11
+ ports: ["5432:5432"]
12
+ env:
13
+ DATABASE_URL: postgres://postgres:example-only@localhost:5432/postgres
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-node@v4
17
+ with:
18
+ node-version: 20
19
+ - run: npm ci
20
+ - run: npm run gen:types && git diff --exit-code src/generated
21
+ - run: npm run lint
22
+ - run: npm run typecheck
23
+ - run: npm test -- --coverage
@@ -0,0 +1,23 @@
1
+ # shipwright-api
2
+
3
+ The shipments API for the Harbourline dispatch console.
4
+
5
+ ## Getting started
6
+
7
+ ```
8
+ npm install
9
+ npm run dev
10
+ ```
11
+
12
+ The server listens on port 3000.
13
+
14
+ ## Layout
15
+
16
+ - `src/routes/` holds one file per resource.
17
+ - Configuration lives in `config/default.json`.
18
+
19
+ ## Tests
20
+
21
+ ```
22
+ npm test
23
+ ```
@@ -0,0 +1,21 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: shipwright-api
4
+ version: 2.4.0
5
+ paths:
6
+ /shipments:
7
+ get:
8
+ summary: List shipments
9
+ parameters:
10
+ - name: status
11
+ in: query
12
+ schema:
13
+ type: string
14
+ enum: [pending, in_transit, delivered]
15
+ - name: limit
16
+ in: query
17
+ schema:
18
+ type: integer
19
+ responses:
20
+ "200":
21
+ description: A page of shipments
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "shipwright-api",
3
+ "version": "2.4.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {}
7
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "shipwright-api",
3
+ "version": "2.4.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "dist/server.js",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.json",
9
+ "start": "node dist/server.js",
10
+ "start:watch": "tsx watch src/server.ts",
11
+ "lint": "eslint src tests",
12
+ "typecheck": "tsc --noEmit",
13
+ "test": "vitest run",
14
+ "gen:types": "openapi-typescript openapi.yaml -o src/generated/api-types.ts"
15
+ },
16
+ "dependencies": {
17
+ "express": "^4.19.2",
18
+ "pg": "^8.11.3"
19
+ },
20
+ "devDependencies": {
21
+ "@types/express": "^4.17.21",
22
+ "eslint": "^8.57.0",
23
+ "openapi-typescript": "^6.7.4",
24
+ "supertest": "^6.3.4",
25
+ "tsx": "^4.7.1",
26
+ "typescript": "^5.4.2",
27
+ "vitest": "^1.4.0"
28
+ }
29
+ }
@@ -0,0 +1,20 @@
1
+ export interface Config {
2
+ port: number;
3
+ databaseUrl: string;
4
+ maxPageSize: number;
5
+ legacySort: boolean;
6
+ }
7
+
8
+ export function loadConfig(): Config {
9
+ const databaseUrl = process.env.DATABASE_URL;
10
+ if (!databaseUrl) {
11
+ throw new Error("DATABASE_URL is required");
12
+ }
13
+ return {
14
+ port: Number(process.env.PORT ?? 3000),
15
+ databaseUrl,
16
+ maxPageSize: Number(process.env.MAX_PAGE_SIZE ?? 100),
17
+ // Old consoles sort by created_at ascending; remove once v1 console is retired.
18
+ legacySort: process.env.SHIPMENTS_LEGACY_SORT === "1",
19
+ };
20
+ }
@@ -0,0 +1,24 @@
1
+ import type { Config } from "../../config.js";
2
+ import type { ListShipmentsQuery } from "../../generated/api-types.js";
3
+
4
+ export interface ShipmentFilter {
5
+ where: string[];
6
+ params: unknown[];
7
+ orderBy: string;
8
+ limit: number;
9
+ }
10
+
11
+ export function buildShipmentFilter(query: ListShipmentsQuery, config: Config): ShipmentFilter {
12
+ const where: string[] = [];
13
+ const params: unknown[] = [];
14
+ if (query.status) {
15
+ params.push(query.status);
16
+ where.push(`status = $${params.length}`);
17
+ }
18
+ return {
19
+ where,
20
+ params,
21
+ orderBy: config.legacySort ? "created_at ASC" : "created_at DESC",
22
+ limit: query.limit ?? 50,
23
+ };
24
+ }
@@ -0,0 +1,15 @@
1
+ import pg from "pg";
2
+ import type { Config } from "../../config.js";
3
+ import type { ShipmentFilter } from "./filter.js";
4
+
5
+ export async function queryShipments(config: Config, filter: ShipmentFilter) {
6
+ const client = new pg.Client({ connectionString: config.databaseUrl });
7
+ await client.connect();
8
+ try {
9
+ const where = filter.where.length ? "WHERE " + filter.where.join(" AND ") : "";
10
+ const sql = `SELECT id, status, carrier, created_at FROM shipments ${where} ORDER BY ${filter.orderBy} LIMIT ${filter.limit}`;
11
+ return (await client.query(sql, filter.params)).rows;
12
+ } finally {
13
+ await client.end();
14
+ }
15
+ }
@@ -0,0 +1,9 @@
1
+ export class HttpError extends Error {
2
+ constructor(public status: number, public code: string, message: string) {
3
+ super(message);
4
+ }
5
+ }
6
+
7
+ export function badRequest(code: string, message: string): HttpError {
8
+ return new HttpError(400, code, message);
9
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * This file was auto-generated by openapi-typescript.
3
+ * Do not make direct changes to the file.
4
+ */
5
+
6
+ export interface ListShipmentsQuery {
7
+ status?: "pending" | "in_transit" | "delivered";
8
+ limit?: number;
9
+ }
@@ -0,0 +1,38 @@
1
+ import type { Request, Response, NextFunction } from "express";
2
+ import type { Config } from "../../config.js";
3
+ import type { ListShipmentsQuery } from "../../generated/api-types.js";
4
+ import { badRequest } from "../../errors.js";
5
+ import { buildShipmentFilter } from "../../domain/shipments/filter.js";
6
+ import { queryShipments } from "../../domain/shipments/repository.js";
7
+
8
+ const STATUSES = ["pending", "in_transit", "delivered"];
9
+
10
+ export function parseListQuery(raw: Record<string, unknown>, config: Config): ListShipmentsQuery {
11
+ const query: ListShipmentsQuery = {};
12
+ if (raw.status !== undefined) {
13
+ if (typeof raw.status !== "string" || !STATUSES.includes(raw.status)) {
14
+ throw badRequest("invalid_status", "status must be one of " + STATUSES.join(", "));
15
+ }
16
+ query.status = raw.status as ListShipmentsQuery["status"];
17
+ }
18
+ if (raw.limit !== undefined) {
19
+ const limit = Number(raw.limit);
20
+ if (!Number.isInteger(limit) || limit < 1 || limit > config.maxPageSize) {
21
+ throw badRequest("invalid_limit", "limit must be 1.." + config.maxPageSize);
22
+ }
23
+ query.limit = limit;
24
+ }
25
+ return query;
26
+ }
27
+
28
+ export function listShipments(config: Config) {
29
+ return async (req: Request, res: Response, next: NextFunction) => {
30
+ try {
31
+ const query = parseListQuery(req.query, config);
32
+ const filter = buildShipmentFilter(query, config);
33
+ res.json({ items: await queryShipments(config, filter) });
34
+ } catch (err) {
35
+ next(err);
36
+ }
37
+ };
38
+ }
@@ -0,0 +1,7 @@
1
+ import type { Express } from "express";
2
+ import type { Config } from "../config.js";
3
+ import { listShipments } from "./handlers/shipments.js";
4
+
5
+ export function registerRoutes(app: Express, config: Config): void {
6
+ app.get("/shipments", listShipments(config));
7
+ }
@@ -0,0 +1,22 @@
1
+ // Nightly CSV export for the finance team. Runs from the scheduler, not the API.
2
+ import pg from "pg";
3
+ import { loadConfig } from "../config.js";
4
+
5
+ export async function exportShipments(status?: string): Promise<string> {
6
+ const config = loadConfig();
7
+ const client = new pg.Client({ connectionString: config.databaseUrl });
8
+ await client.connect();
9
+ try {
10
+ // Same filtering as the list endpoint, kept here so the job has no HTTP dependency.
11
+ const params: unknown[] = [];
12
+ let where = "";
13
+ if (status) {
14
+ params.push(status);
15
+ where = "WHERE status = $1";
16
+ }
17
+ const rows = (await client.query(`SELECT id, status, carrier FROM shipments ${where} ORDER BY created_at DESC`, params)).rows;
18
+ return rows.map((r) => [r.id, r.status, r.carrier].join(",")).join("\n");
19
+ } finally {
20
+ await client.end();
21
+ }
22
+ }
@@ -0,0 +1,18 @@
1
+ import express from "express";
2
+ import { loadConfig } from "./config.js";
3
+ import { registerRoutes } from "./http/routes.js";
4
+ import { HttpError } from "./errors.js";
5
+
6
+ const config = loadConfig();
7
+ const app = express();
8
+ registerRoutes(app, config);
9
+
10
+ app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
11
+ if (err instanceof HttpError) {
12
+ res.status(err.status).json({ error: err.code, message: err.message });
13
+ return;
14
+ }
15
+ res.status(500).json({ error: "internal" });
16
+ });
17
+
18
+ app.listen(config.port);
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildShipmentFilter } from "../../src/domain/shipments/filter.js";
3
+
4
+ const config = { port: 0, databaseUrl: "postgres://unused", maxPageSize: 100, legacySort: false };
5
+
6
+ describe("buildShipmentFilter", () => {
7
+ it("adds a status clause with a positional parameter", () => {
8
+ const f = buildShipmentFilter({ status: "pending" }, config);
9
+ expect(f.where).toEqual(["status = $1"]);
10
+ expect(f.params).toEqual(["pending"]);
11
+ });
12
+
13
+ it("sorts ascending when legacySort is on", () => {
14
+ expect(buildShipmentFilter({}, { ...config, legacySort: true }).orderBy).toBe("created_at ASC");
15
+ });
16
+ });
@@ -0,0 +1,18 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseListQuery } from "../../src/http/handlers/shipments.js";
3
+
4
+ const config = { port: 0, databaseUrl: "postgres://unused", maxPageSize: 100, legacySort: false };
5
+
6
+ describe("parseListQuery", () => {
7
+ it("accepts a known status", () => {
8
+ expect(parseListQuery({ status: "delivered" }, config)).toEqual({ status: "delivered" });
9
+ });
10
+
11
+ it("rejects an unknown status with invalid_status", () => {
12
+ expect(() => parseListQuery({ status: "lost" }, config)).toThrowError(/status must be one of/);
13
+ });
14
+
15
+ it("rejects a limit above maxPageSize", () => {
16
+ expect(() => parseListQuery({ limit: "500" }, config)).toThrowError(/limit must be/);
17
+ });
18
+ });
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "strict": true
8
+ },
9
+ "include": ["src"]
10
+ }
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: { include: ["tests/**/*.test.ts"] },
5
+ });
@@ -0,0 +1,16 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+ jobs:
4
+ check:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - uses: actions/checkout@v4
8
+ with:
9
+ fetch-depth: 0
10
+ - uses: actions/setup-python@v5
11
+ with:
12
+ python-version: |
13
+ 3.10
14
+ 3.12
15
+ - run: pip install nox
16
+ - run: nox -s lint tests
@@ -0,0 +1,16 @@
1
+ # ledgerline
2
+
3
+ Turns bank-statement CSV exports into one ledger format.
4
+
5
+ ## Install
6
+
7
+ pip install -e .
8
+
9
+ ## Running the tests
10
+
11
+ python setup.py test
12
+
13
+ ## Adding a bank
14
+
15
+ Drop a new module in `src/ledgerline/parsers/` and it is picked up automatically.
16
+ Each parser subclasses `BaseParser` and implements `parse_row`.
@@ -0,0 +1,14 @@
1
+ import nox
2
+
3
+
4
+ @nox.session(python=["3.10", "3.12"])
5
+ def tests(session):
6
+ session.install("-e", ".[dev]")
7
+ session.run("pytest", "-m", "not slow", *session.posargs)
8
+
9
+
10
+ @nox.session
11
+ def lint(session):
12
+ session.install("ruff")
13
+ session.run("ruff", "check", "src", "tests")
14
+ session.run("ruff", "format", "--check", "src", "tests")
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "setuptools-scm>=8"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ledgerline"
7
+ dynamic = ["version"]
8
+ requires-python = ">=3.10"
9
+ dependencies = []
10
+
11
+ [project.optional-dependencies]
12
+ dev = ["pytest>=8", "ruff>=0.4", "nox>=2024.3"]
13
+
14
+ [project.scripts]
15
+ ledgerline = "ledgerline.cli:main"
16
+
17
+ [tool.setuptools_scm]
18
+ version_file = "src/ledgerline/_version.py"
19
+
20
+ [tool.pytest.ini_options]
21
+ testpaths = ["tests"]
22
+ markers = ["slow: reads the large sample statements"]
@@ -0,0 +1,3 @@
1
+ from ledgerline._version import __version__
2
+
3
+ __all__ = ["__version__"]
@@ -0,0 +1,4 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+ __version__ = version = "1.6.2.dev4"
4
+ __version_tuple__ = version_tuple = (1, 6, 2, "dev4")
@@ -0,0 +1,32 @@
1
+ import argparse
2
+ import csv
3
+ import os
4
+ import sys
5
+
6
+ from ledgerline.detect import detect_bank
7
+ from ledgerline.errors import ParseError
8
+ from ledgerline.parsers import PARSERS
9
+
10
+
11
+ def main(argv=None):
12
+ ap = argparse.ArgumentParser(prog="ledgerline")
13
+ ap.add_argument("statement")
14
+ ap.add_argument("--bank", help="skip detection and use this parser")
15
+ args = ap.parse_args(argv)
16
+ currency = os.environ.get("LEDGERLINE_CURRENCY", "GBP")
17
+ with open(args.statement, newline="", encoding="utf-8-sig") as fh:
18
+ rows = list(csv.reader(fh))
19
+ bank = args.bank or detect_bank(rows[0])
20
+ parser = PARSERS[bank](currency=currency)
21
+ writer = csv.writer(sys.stdout)
22
+ for n, row in enumerate(rows[1:], start=2):
23
+ try:
24
+ writer.writerow(parser.parse_row(row).as_csv())
25
+ except ParseError as e:
26
+ print(f"line {n}: {e}", file=sys.stderr)
27
+ return 1
28
+ return 0
29
+
30
+
31
+ if __name__ == "__main__":
32
+ sys.exit(main())
@@ -0,0 +1,15 @@
1
+ from ledgerline.errors import ParseError
2
+
3
+ # Header row of each bank's export -> parser name in PARSERS.
4
+ HEADER_SIGNATURES = {
5
+ ("Date", "Description", "Amount", "Balance"): "acme",
6
+ ("Posted", "Payee", "Debit", "Credit"): "harbor",
7
+ }
8
+
9
+
10
+ def detect_bank(header):
11
+ key = tuple(cell.strip() for cell in header)
12
+ try:
13
+ return HEADER_SIGNATURES[key]
14
+ except KeyError:
15
+ raise ParseError(f"unrecognised header {list(key)}; pass --bank")
@@ -0,0 +1,2 @@
1
+ class ParseError(ValueError):
2
+ """A statement row that cannot be turned into an Entry. The message names the column."""
@@ -0,0 +1,14 @@
1
+ from decimal import Decimal, InvalidOperation
2
+
3
+ from ledgerline.errors import ParseError
4
+
5
+
6
+ def parse_amount(text: str) -> Decimal:
7
+ """Parse '1,234.50' or '(12.00)' into a Decimal. Parentheses mean a debit."""
8
+ cleaned = text.strip().replace(",", "")
9
+ negative = cleaned.startswith("(") and cleaned.endswith(")")
10
+ try:
11
+ value = Decimal(cleaned.strip("()"))
12
+ except InvalidOperation:
13
+ raise ParseError(f"amount {text!r} is not a number")
14
+ return -value if negative else value
@@ -0,0 +1,8 @@
1
+ from ledgerline.parsers.acme import AcmeParser
2
+ from ledgerline.parsers.harbor import HarborParser
3
+
4
+ # Explicit registry: a parser module is not used until it is listed here.
5
+ PARSERS = {
6
+ "acme": AcmeParser,
7
+ "harbor": HarborParser,
8
+ }
@@ -0,0 +1,18 @@
1
+ from datetime import datetime
2
+
3
+ from ledgerline.errors import ParseError
4
+ from ledgerline.money import parse_amount
5
+ from ledgerline.parsers.base import BaseParser, Entry
6
+
7
+
8
+ class AcmeParser(BaseParser):
9
+ date_format = "%d/%m/%Y"
10
+
11
+ def parse_row(self, row):
12
+ if len(row) != 4:
13
+ raise ParseError(f"expected 4 columns, got {len(row)}")
14
+ try:
15
+ on = datetime.strptime(row[0], self.date_format).date()
16
+ except ValueError:
17
+ raise ParseError(f"Date {row[0]!r} is not DD/MM/YYYY")
18
+ return Entry(on=on, payee=row[1].strip(), amount=parse_amount(row[2]), currency=self.currency)
@@ -0,0 +1,24 @@
1
+ from dataclasses import dataclass
2
+ from datetime import date
3
+ from decimal import Decimal
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Entry:
8
+ on: date
9
+ payee: str
10
+ amount: Decimal
11
+ currency: str
12
+
13
+ def as_csv(self):
14
+ return [self.on.isoformat(), self.payee, f"{self.amount:.2f}", self.currency]
15
+
16
+
17
+ class BaseParser:
18
+ date_format = "%Y-%m-%d"
19
+
20
+ def __init__(self, currency: str):
21
+ self.currency = currency
22
+
23
+ def parse_row(self, row) -> Entry:
24
+ raise NotImplementedError