skilldrop-cli 0.16.28 → 0.16.30

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 (186) hide show
  1. package/bin/skilldrop.js +160 -33
  2. package/catalogue.json +7 -3
  3. package/model-routing.json +16 -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/data-analytics/loops/measure/LOOP.md +118 -0
  17. package/packs/data-analytics/loops/measure/evals/eval_queries.json +9 -0
  18. package/packs/data-analytics/loops/measure/evals/evals.json +28 -0
  19. package/packs/data-analytics/loops/measure/loop.json +41 -0
  20. package/packs/dev-team/loops/build/LOOP.md +4 -2
  21. package/packs/dev-team/loops/build/loop.json +2 -2
  22. package/packs/dev-team/skills/codebase-tour/SKILL.md +115 -0
  23. package/packs/dev-team/skills/codebase-tour/evals/eval_queries.json +13 -0
  24. package/packs/dev-team/skills/codebase-tour/evals/evals.json +77 -0
  25. package/packs/dev-team/skills/codebase-tour/evals/files/1/.github/workflows/ci.yml +23 -0
  26. package/packs/dev-team/skills/codebase-tour/evals/files/1/README.md +23 -0
  27. package/packs/dev-team/skills/codebase-tour/evals/files/1/openapi.yaml +21 -0
  28. package/packs/dev-team/skills/codebase-tour/evals/files/1/package-lock.json +7 -0
  29. package/packs/dev-team/skills/codebase-tour/evals/files/1/package.json +29 -0
  30. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/config.ts +20 -0
  31. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/filter.ts +24 -0
  32. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/repository.ts +15 -0
  33. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/errors.ts +9 -0
  34. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/generated/api-types.ts +9 -0
  35. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/handlers/shipments.ts +38 -0
  36. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/routes.ts +7 -0
  37. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/jobs/export-shipments.ts +22 -0
  38. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/server.ts +18 -0
  39. package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/domain/filter.test.ts +16 -0
  40. package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/http/shipments.test.ts +18 -0
  41. package/packs/dev-team/skills/codebase-tour/evals/files/1/tsconfig.json +10 -0
  42. package/packs/dev-team/skills/codebase-tour/evals/files/1/vitest.config.ts +5 -0
  43. package/packs/dev-team/skills/codebase-tour/evals/files/2/.github/workflows/ci.yml +16 -0
  44. package/packs/dev-team/skills/codebase-tour/evals/files/2/README.md +16 -0
  45. package/packs/dev-team/skills/codebase-tour/evals/files/2/noxfile.py +14 -0
  46. package/packs/dev-team/skills/codebase-tour/evals/files/2/pyproject.toml +22 -0
  47. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/__init__.py +3 -0
  48. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/_version.py +4 -0
  49. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/cli.py +32 -0
  50. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/detect.py +15 -0
  51. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/errors.py +2 -0
  52. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/money.py +14 -0
  53. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/__init__.py +8 -0
  54. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/acme.py +18 -0
  55. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/base.py +24 -0
  56. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/harbor.py +13 -0
  57. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/fixtures/acme_sample.csv +3 -0
  58. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_acme.py +23 -0
  59. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_detect.py +13 -0
  60. package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +28 -0
  61. package/packs/dev-team/skills/codebase-tour/evals/files/3/Makefile +16 -0
  62. package/packs/dev-team/skills/codebase-tour/evals/files/3/README.md +16 -0
  63. package/packs/dev-team/skills/codebase-tour/evals/files/3/cmd/tidepool/main.go +24 -0
  64. package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +5 -0
  65. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/config/config.go +38 -0
  66. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher.go +52 -0
  67. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher_test.go +42 -0
  68. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/sender.go +32 -0
  69. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/replay/replay.go +31 -0
  70. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/db.go +35 -0
  71. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql +11 -0
  72. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql.go +30 -0
  73. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/schema.sql +6 -0
  74. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/store_integration_test.go +21 -0
  75. package/packs/dev-team/skills/codebase-tour/evals/files/3/sqlc.yaml +9 -0
  76. package/packs/dev-team/skills/codebase-tour/evals/files/4/.env.example +4 -0
  77. package/packs/dev-team/skills/codebase-tour/evals/files/4/.github/workflows/ci.yml +16 -0
  78. package/packs/dev-team/skills/codebase-tour/evals/files/4/Makefile +16 -0
  79. package/packs/dev-team/skills/codebase-tour/evals/files/4/README.md +23 -0
  80. package/packs/dev-team/skills/codebase-tour/evals/files/4/data/rates.csv +4 -0
  81. package/packs/dev-team/skills/codebase-tour/evals/files/4/pyproject.toml +19 -0
  82. package/packs/dev-team/skills/codebase-tour/evals/files/4/scripts/gen_rates.py +29 -0
  83. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/__init__.py +0 -0
  84. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/__init__.py +0 -0
  85. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/preview.py +14 -0
  86. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/routes.py +36 -0
  87. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/config.py +15 -0
  88. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/discounts.py +8 -0
  89. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/__init__.py +0 -0
  90. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/rates_table.py +8 -0
  91. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/main.py +8 -0
  92. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/shipping.py +18 -0
  93. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/tax.py +7 -0
  94. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/conftest.py +3 -0
  95. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_discounts.py +11 -0
  96. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_tax.py +13 -0
  97. package/packs/dev-team/skills/codebase-tour/evals/files/5/.env.example +3 -0
  98. package/packs/dev-team/skills/codebase-tour/evals/files/5/.github/workflows/ci.yml +13 -0
  99. package/packs/dev-team/skills/codebase-tour/evals/files/5/README.md +18 -0
  100. package/packs/dev-team/skills/codebase-tour/evals/files/5/package.json +13 -0
  101. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/openapi.json +30 -0
  102. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/package.json +15 -0
  103. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/db.js +19 -0
  104. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/flags.js +8 -0
  105. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/generated/schema.js +32 -0
  106. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/middleware/validate.js +20 -0
  107. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/routes/bookings.js +15 -0
  108. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/server.js +15 -0
  109. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/bookings.js +23 -0
  110. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/pricing.js +9 -0
  111. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/test/pricing.test.js +15 -0
  112. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/package.json +9 -0
  113. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/bookingForm.js +11 -0
  114. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/format.js +7 -0
  115. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/test/format.test.js +12 -0
  116. package/packs/dev-team/skills/codebase-tour/evals/files/5/scripts/gen-schema.mjs +27 -0
  117. package/packs/dev-team/skills/codebase-tour/examples/refund-window.md +158 -0
  118. package/packs/dev-team/skills/codebase-tour/manifest.json +47 -0
  119. package/packs/dev-team/skills/codebase-tour/scripts/repo_facts.py +403 -0
  120. package/packs/dev-team/skills/codebase-tour/templates/codebase-tour.md +85 -0
  121. package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +101 -0
  122. package/packs/dev-team/skills/dependency-upgrade-review/evals/eval_queries.json +11 -0
  123. package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +77 -0
  124. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/.github/workflows/ci.yml +20 -0
  125. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/pr.diff +87 -0
  126. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/quillmark-CHANGELOG.md +24 -0
  127. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/comment.js +6 -0
  128. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/note.js +8 -0
  129. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/test/note.test.js +12 -0
  130. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/ci.yml +23 -0
  131. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/release.yml +23 -0
  132. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/pr.diff +32 -0
  133. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/release-notes.md +20 -0
  134. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/.github/workflows/ci.yml +18 -0
  135. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/Dockerfile +8 -0
  136. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/PR.md +36 -0
  137. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/pr.diff +47 -0
  138. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/client.py +24 -0
  139. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/reader.py +32 -0
  140. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/tests/test_reader.py +15 -0
  141. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/upstream/quillstream-CHANGELOG.md +37 -0
  142. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/ci.yml +22 -0
  143. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/release.yml +25 -0
  144. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.nvmrc +1 -0
  145. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-412.md +16 -0
  146. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-413.md +15 -0
  147. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/package.json +21 -0
  148. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-412.diff +26 -0
  149. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-413.diff +84 -0
  150. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/scripts/release.mjs +18 -0
  151. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/release-notary-CHANGELOG.md +16 -0
  152. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/setup-toolchain-RELEASES.md +22 -0
  153. package/packs/dev-team/skills/dependency-upgrade-review/examples/inputs/ledgerline.diff +38 -0
  154. package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +91 -0
  155. package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +50 -0
  156. package/packs/dev-team/skills/dependency-upgrade-review/reference.md +99 -0
  157. package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +718 -0
  158. package/packs/grc/loops/comply/LOOP.md +133 -0
  159. package/packs/grc/loops/comply/evals/eval_queries.json +9 -0
  160. package/packs/grc/loops/comply/evals/evals.json +30 -0
  161. package/packs/grc/loops/comply/loop.json +54 -0
  162. package/packs/product-manager/loops/discover/LOOP.md +5 -4
  163. package/packs/product-manager/loops/discover/loop.json +2 -2
  164. package/packs/product-manager/skills/feedback-synthesis/SKILL.md +182 -0
  165. package/packs/product-manager/skills/feedback-synthesis/evals/eval_queries.json +46 -0
  166. package/packs/product-manager/skills/feedback-synthesis/evals/evals.json +76 -0
  167. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/interview-notes.md +47 -0
  168. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/nps-verbatims.csv +13 -0
  169. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/support-tickets.csv +36 -0
  170. package/packs/product-manager/skills/feedback-synthesis/evals/files/3/churn-exit-survey.csv +11 -0
  171. package/packs/product-manager/skills/feedback-synthesis/evals/files/3/sales-call-notes.md +55 -0
  172. package/packs/product-manager/skills/feedback-synthesis/evals/files/4/csat_comments_q3.csv +17 -0
  173. package/packs/product-manager/skills/feedback-synthesis/evals/files/4/support_tickets_q3.csv +49 -0
  174. package/packs/product-manager/skills/feedback-synthesis/evals/files/5/app_reviews_last_60_days.csv +32 -0
  175. package/packs/product-manager/skills/feedback-synthesis/evals/files/5/interview_notes_round1.md +48 -0
  176. package/packs/product-manager/skills/feedback-synthesis/examples/booking-app-feedback.md +139 -0
  177. package/packs/product-manager/skills/feedback-synthesis/examples/inputs/booking-app-coded.csv +17 -0
  178. package/packs/product-manager/skills/feedback-synthesis/manifest.json +53 -0
  179. package/packs/product-manager/skills/feedback-synthesis/reference.md +89 -0
  180. package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +355 -0
  181. package/packs/product-manager/skills/feedback-synthesis/templates/coding-sheet.csv +6 -0
  182. package/packs/product-manager/skills/feedback-synthesis/templates/synthesis.md +42 -0
  183. package/packs/research/loops/research/LOOP.md +123 -0
  184. package/packs/research/loops/research/evals/eval_queries.json +9 -0
  185. package/packs/research/loops/research/evals/evals.json +39 -0
  186. package/packs/research/loops/research/loop.json +35 -0
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: codebase-tour
3
+ description: Get a newcomer (a new engineer, or the agent itself) oriented in an unfamiliar repository before they make one specific change, with a short tour scoped to that change. It gives the install, run and test commands taken from CI, package scripts and the Makefile; the path the change touches, traced through the code with file:line citations; where the change goes; the conventions the code actually follows; the traps (generated files, a second copy of the logic, untested code, env vars, flags); and a first step that can be checked. Use when someone says "I'm new to this repo", "get me oriented", "where would I make this change", "walk me through the code for X", "onboard me before I start this ticket", or "how do I run and test this".
4
+ ---
5
+
6
+ # codebase-tour
7
+
8
+ Orients someone in a repository they have never seen, for **one change they are about to make**. The tour is not a map of the system. It follows the path the change will touch, and it ends at the file and function where the work starts.
9
+
10
+ Two failures make a plain orientation worse than none. The first is a **confident wrong command**: `make dev` copied from a README into a repo whose Makefile has no `dev` target. The second is the **trap nobody mentions**: the generated file someone hand-edits, the second copy of the logic that stays unchanged, the test suite that skips the area. This skill is built around two rules. **Every claim cites the file and line it was read from. The README gives leads to check; the code gives the facts.**
11
+
12
+ Boundaries: a full as-is architecture write-up is `reverse-architecture`. A diagram is `interactive-diagram` or `architecture-diagrams`. A standing agent-policy file for the repo is `agents-md-generator`. Writing the change itself is `feature-implement-loop`, which takes this tour's "where the change goes" and first step as its input. Reviewing the finished diff is `pre-merge-review`.
13
+
14
+ ## How to respond
15
+
16
+ **Four rules come before the steps and outrank them:**
17
+
18
+ - **Answer what was asked, first.** Open the reply with the 3–5 things this person most needs to know for this change. A directory listing is never first.
19
+ - **Use only what you read.** Every statement carries `path:line` (or a path, for a whole file) from this repository. Anything not checked is tagged `[inferred]` with the reason. A fact the repo doesn't give goes under "Open questions". It is not filled in from what the framework usually does.
20
+ - **Deliver from what you have.** Read the repo, state any assumption in one line, and produce the tour. Ask nothing when the repo and the change are both in hand.
21
+ - **Write for someone who has never heard of this skill.** No skill names, no paths or scripts from this folder, no internal terms, and nothing about how the run was set up. A next step is one plain sentence at the end.
22
+ - **For this skill:** where the README and the code disagree, say so in one line with both citations, and follow the code.
23
+
24
+ 1. **Pin the change in code terms.** Restate it in one sentence using the words the code will use. ✅ *"Reject `POST /orders/{order_id}/refunds` when the event ended more than 14 days ago."* ❌ *"Tighten up refunds."* If no change is named, tour the main path from the primary entry point, tag that scope `[assumption]`, and replace "Where the change goes" with one line asking for the change.
25
+
26
+ 2. **Inventory what the repo says about itself.** Run the fact lister. It is read-only: it prints commands, test files, generated-file markers, env reads and README claims that don't resolve, each with `path:line`.
27
+
28
+ ```bash
29
+ # Claude Code
30
+ python3 "${CLAUDE_SKILL_DIR}/scripts/repo_facts.py" <repo-root>
31
+ # Other IDEs (from the skill folder)
32
+ python3 scripts/repo_facts.py <repo-root>
33
+ ```
34
+
35
+ If no script can run, read these by hand in order: CI workflows (`.github/workflows/`, `.gitlab-ci.yml`), then package scripts, `Makefile`, `justfile`, `tox.ini`, `noxfile.py` and `pyproject.toml`, then `README` and `CONTRIBUTING`. CI ranks first because its commands are the ones that have to pass.
36
+
37
+ 3. **Take commands only from those files, and say which ones ran.** Quote each command with its source. ✅ `make test` (`.github/workflows/ci.yml:21`, which runs `pytest -q -m "not e2e"` per `Makefile:8`). ❌ `make dev` (named in the README; the Makefile has no `dev` target). Prefer CI's form of a command over the script's bare form, and list anything CI needs that a laptop lacks (a database service, an env var set in the workflow). Give each command one status:
38
+ - `ran: passed` or `ran: failed` with the first error line. Run only commands that stay inside the repo: tests, lint, typecheck or build, and only when the toolchain and dependencies are already installed.
39
+ - `read, not run` for everything else. That covers dependency installs, anything that needs a database, a network service or credentials, deploys, migrations, publishing, and generators that rewrite tracked files. Run these only when the user asks.
40
+
41
+ A command to run one test file or one test is **derived**. It goes through the repo's own script or runner, plus that runner's filter argument, and it is labelled `derived from <source>`. ✅ `pytest -q -m "not e2e" tests/rules/test_refunds.py` (derived from `Makefile:8`). ❌ `python -m unittest` in a repo that runs pytest. A runner the repo never calls is invented. Leave it out.
42
+
43
+ 4. **Trace the path the change touches.** Search for the change's nouns: the route string, the CLI flag, the field or column name, the error code. Work outward to the entry point (`main`, the server bootstrap, the console script, the job scheduler). Then write the path forward as numbered hops. Each hop is `path:line` `function`, plus one line on what it does there. Stop at the far edge of the change, usually the database call or the outbound request. Keep it to 10 hops or fewer. A hop you couldn't trace is `[inferred]`, with what you'd need to confirm it.
44
+
45
+ 5. **Say where the change goes, and why there.** Name the files and functions. Justify each one with the pattern its nearest sibling already follows. ✅ *"Every other refund rule lives in `check_refundable` (`tallyhall/rules/refunds.py:9`), which the route calls at `tallyhall/api/refunds.py:22`. The window check goes in `check_refundable` too."* List **everything that has to change together**: a spec and the types generated from it, a registry the new code must be added to, a second copy of the logic.
46
+
47
+ 6. **Read the conventions from the code nearest the change, not from the README.** Cover naming, how errors are raised and returned, test layout and naming, how config and env vars are read, and logging, but only the ones this change will hit. Each gets a citation from code. ✅ *"Errors: raise `RefundRejected(code, message)`; the route turns it into a 422 (`tallyhall/rules/refunds.py:4`, `tallyhall/api/refunds.py:25`)."* Where a file on the path breaks its own convention, say so. That file is the one a newcomer copies by mistake. Every README or `CONTRIBUTING` claim the code contradicts (a command, a path, where config lives, how a plugin is registered) goes under "Where the README is wrong", with both citations.
48
+
49
+ 7. **Hunt the traps.** Check each one and report what you found. Write "none found" when a check comes up empty, so the person knows it was done.
50
+ - **Generated files**: the marker line, the source they come from, the command that regenerates them, and whether CI checks for drift.
51
+ - **Duplicated logic**: search for the change's identifiers, SQL, literals and constants outside the main path. A second copy is either updated in the same change or left alone on purpose, and the tour says which is the open question.
52
+ - **Untested code on the path**: name each function on the path that no test calls.
53
+ - **Tests the default command skips**: markers (`-m "not slow"`), build tags, include globs.
54
+ - **Env vars and feature flags the path reads**: the name, the default, and the line that reads it.
55
+ - **Vendored code** on the path, and where it comes from upstream.
56
+
57
+ 8. **Find the tests for the area, and how to run just those.** Name the test files that exercise the path, the command for the whole suite, and the derived command for only these files. When the area has no tests, say so plainly: ✅ *"Nothing tests `tallyhall/admin/bulk_refund.py`. The first step adds one."*
58
+
59
+ 9. **Name the first step: small, local, and verifiable.** It says what to do in which file, the command to run, and the result that shows it worked. A failing test next to an existing one is the usual best first step. ✅ *"Add `test_rejects_refund_after_window` to `tests/rules/test_refunds.py`, with an order whose event ended 15 days ago; run `pytest -q -m "not e2e" tests/rules/test_refunds.py`; it fails until `check_refundable` checks the window."*
60
+
61
+ 10. **List open questions for a maintainer.** Five at most. Each one can be answered in a sentence and points at the evidence that raised it. ✅ *"Should the support bulk-refund tool (`tallyhall/admin/bulk_refund.py:27`) apply the 14-day window too, or is it the override?"*
62
+
63
+ 11. **Write it with [`templates/codebase-tour.md`](templates/codebase-tour.md), under the caps.** The whole tour is ≤ ~120 lines and each section is ≤ ~15 lines, about one screen. The scope line names what was **deliberately skipped**: the areas not on the path, the services not read. That way the person knows it is a scoped tour and not a survey. If a section would overflow, cut the hops and conventions furthest from the change first.
64
+
65
+ 12. **Reply.** Lead with the 3–5 bullets, then the tour. Put the tour in the reply. Write it to a file only when the user names a path, and change nothing else in the repository.
66
+
67
+ **Non-interactive runs** (subagent, CI, headless): an unnamed change becomes a main-path tour tagged `[assumption]` (step 1). Unclear scope boundaries become `[assumption]` lines at the top. A repository that cannot be read emits `BLOCKED: need read access to the repository — every claim in the tour cites a file in it`, followed by one plain sentence saying the tour follows once the files can be read. A tour is never written from a description of a repo.
68
+
69
+ ## Useful references in this skill
70
+
71
+ - [`templates/codebase-tour.md`](templates/codebase-tour.md): the tour skeleton, with section order and line budgets
72
+ - [`scripts/repo_facts.py`](scripts/repo_facts.py): read-only, stdlib-only fact lister (commands with provenance, README commands and paths that don't resolve, tests, generated markers, env reads)
73
+ - [`examples/refund-window.md`](examples/refund-window.md): worked input → output for a Python service, including a README that disagrees with the code and a second copy of a rule
74
+
75
+ ## Quality bar
76
+
77
+ - **Every claim has a citation or an `[inferred]` tag.** Run a spot check: pick any three statements in the tour, and each must lead to a file and line that says it.
78
+ - **Zero invented commands.** Every command traces to CI, a package script, a Makefile, a tox or nox session or pyproject, or it is a labelled derived filter of a runner the repo already calls.
79
+ - **Ran and read are never blurred.** Every command carries `ran: passed`, `ran: failed`, or `read, not run`.
80
+ - **README-versus-code conflicts are named**, each with both citations and the code taken as the fact.
81
+ - **Scoped, not surveyed.** The path stops at the change's far edge, the tour fits the caps, and the scope line says what was skipped.
82
+ - **The trap checks all report**, including "none found". A second copy of the logic, when one exists, is named with its location.
83
+ - **The tests for the area are named, with a command for just those**, or the tour says plainly that there are none.
84
+ - **It ends at a place to work.** Files and functions, then a first step with a command and an expected result, then at most five open questions.
85
+ - **The reply leads with the 3–5 things that matter**, not a tree.
86
+
87
+ ## When to use this skill
88
+
89
+ - ✅ "I'm new to this repo and need to add X. Get me oriented."
90
+ - ✅ A ticket is picked up in an unfamiliar codebase and the first hour is going on finding where things live
91
+ - ✅ An agent is about to implement a change in a repo it has not read, and needs the commands and traps before writing code
92
+ - ✅ "How do I run and test this, and where would this change go?"
93
+ - ✅ Handing a scoped piece of work to a contractor or a new teammate with a page that is true
94
+
95
+ ## When NOT to use this skill
96
+
97
+ - ❌ A full as-is architecture write-up or diagram of the whole system: `reverse-architecture`
98
+ - ❌ Drawing a diagram of a flow or a system: `interactive-diagram` or `architecture-diagrams`
99
+ - ❌ Writing a standing `AGENTS.md` or agent-policy file for the repo: `agents-md-generator`
100
+ - ❌ Writing the code for the change: `feature-implement-loop`
101
+ - ❌ Reviewing a diff that already exists: `pre-merge-review`
102
+
103
+ ## Anti-patterns to avoid
104
+
105
+ - ❌ **Copying the README's commands.** A README goes stale first. Check every command it names against the scripts and CI, and report the ones that don't exist.
106
+ - ❌ **Ecosystem-default commands.** `pytest` for a repo that runs `nox -s tests`, `make test` in a repo with no Makefile. The repo says what it runs.
107
+ - ❌ **"Run the tests" without saying whether you did.** A reader can't tell a command that passed from one copied out of a file.
108
+ - ❌ **The architecture survey.** Twelve services described, and the one function the change touches never named. Follow the path; skip the rest and say so.
109
+ - ❌ **A directory tree as the opening.** The person can run `ls`. Open with what changes their next hour.
110
+ - ❌ **Conventions from the style guide.** `CONTRIBUTING.md` says "use custom error classes"; the handler on the path returns `{ error: string }`. Report what the code does, and flag the gap.
111
+ - ❌ **Missing the second copy.** The change lands in the API route, and the admin tool with its own copy of the same rule keeps the old behaviour. Search for the identifiers before naming where the change goes.
112
+ - ❌ **Editing a generated file in the first step.** Name the source and the regenerate command instead.
113
+ - ❌ **Running something with side effects to "check it works".** Deploys, migrations, installs that hit the network, and generators that rewrite tracked files stay `read, not run` unless the user asks.
114
+ - ❌ **A first step that can't be checked.** "Get familiar with the handler" is not a step. Name a file, a command and an expected result.
115
+ - ❌ **Showing the machinery.** No skill names, script names or caps in the reply or the tour.
@@ -0,0 +1,13 @@
1
+ [
2
+ { "query": "I'm new to this repo and need to add rate limiting to the login endpoint. Get me oriented.", "should_trigger": true },
3
+ { "query": "Before I start this ticket, walk me through the code that handles invoice exports", "should_trigger": true },
4
+ { "query": "Where would I make this change? I've never seen this codebase", "should_trigger": true },
5
+ { "query": "How do I run and test this project, and where does the payment retry logic live?", "should_trigger": true },
6
+ { "query": "Onboard me to this repo so I can fix the timezone bug in the scheduler", "should_trigger": true },
7
+
8
+ { "query": "Reverse-engineer the architecture of this whole system and draw the as-is diagram", "should_trigger": false },
9
+ { "query": "Write an AGENTS.md for this repo so agents stop running the wrong commands", "should_trigger": false },
10
+ { "query": "Implement the carrier filter on GET /shipments with tests, here are the acceptance criteria", "should_trigger": false },
11
+ { "query": "Review my diff before I merge it", "should_trigger": false },
12
+ { "query": "Make an interactive diagram of how a request flows through our services", "should_trigger": false }
13
+ ]
@@ -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")