skilldrop-cli 0.16.35 → 0.16.36

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 (212) hide show
  1. package/README.md +1 -0
  2. package/agents/README.md +4 -1
  3. package/agents/docs-reviewer.md +47 -0
  4. package/agents/finding-adjudicator.md +49 -0
  5. package/agents/test-reviewer.md +52 -0
  6. package/catalogue.json +17 -7
  7. package/model-routing.json +40 -0
  8. package/package.json +1 -1
  9. package/packs/ai-engineering/skills/agent-config-review/SKILL.md +116 -0
  10. package/packs/ai-engineering/skills/agent-config-review/evals/eval_queries.json +12 -0
  11. package/packs/ai-engineering/skills/agent-config-review/evals/evals.json +75 -0
  12. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/agents/db-migrator.md +8 -0
  13. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/agents/reviewer.md +9 -0
  14. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/settings.json +48 -0
  15. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/settings.local.json +5 -0
  16. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.mcp.json +22 -0
  17. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/package.json +12 -0
  18. package/packs/ai-engineering/skills/agent-config-review/evals/files/2/.cursor/mcp.json +17 -0
  19. package/packs/ai-engineering/skills/agent-config-review/evals/files/2/config.toml +21 -0
  20. package/packs/ai-engineering/skills/agent-config-review/evals/files/4/my-user-settings.json +11 -0
  21. package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.claude/agents/release-notes.md +13 -0
  22. package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.claude/settings.json +39 -0
  23. package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.mcp.json +18 -0
  24. package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/package.json +14 -0
  25. package/packs/ai-engineering/skills/agent-config-review/evals/files/5/.github/workflows/codex-triage.yml +29 -0
  26. package/packs/ai-engineering/skills/agent-config-review/evals/files/5/AGENTS.md +16 -0
  27. package/packs/ai-engineering/skills/agent-config-review/evals/files/5/codex/config.toml +34 -0
  28. package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/.claude/settings.json +22 -0
  29. package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/.mcp.json +9 -0
  30. package/packs/ai-engineering/skills/agent-config-review/examples/python-repo-tighten.md +81 -0
  31. package/packs/ai-engineering/skills/agent-config-review/manifest.json +60 -0
  32. package/packs/ai-engineering/skills/agent-config-review/reference.md +205 -0
  33. package/packs/ai-engineering/skills/agent-config-review/scripts/agent_config_scan.py +989 -0
  34. package/packs/ai-engineering/skills/agent-incident-response/SKILL.md +175 -0
  35. package/packs/ai-engineering/skills/agent-incident-response/evals/eval_queries.json +14 -0
  36. package/packs/ai-engineering/skills/agent-incident-response/evals/evals.json +107 -0
  37. package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/agent-session-excerpt.txt +11 -0
  38. package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/ci-build-log.txt +24 -0
  39. package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/incident-notes.md +19 -0
  40. package/packs/ai-engineering/skills/agent-incident-response/evals/files/2/mcp-config.json +20 -0
  41. package/packs/ai-engineering/skills/agent-incident-response/evals/files/2/session-transcript.txt +8 -0
  42. package/packs/ai-engineering/skills/agent-incident-response/evals/files/4/agent-session.log +70 -0
  43. package/packs/ai-engineering/skills/agent-incident-response/evals/files/4/infra/staging-db.tf +25 -0
  44. package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/jobs/nightly-triage/job.yaml +19 -0
  45. package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/jobs/nightly-triage/run.log +13 -0
  46. package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/usage-export-2026-10-20.csv +13 -0
  47. package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/.mcp.json +20 -0
  48. package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/git-log.txt +12 -0
  49. package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/notesync-excerpt.txt +30 -0
  50. package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/notesync.log +15 -0
  51. package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/ssh-config.txt +16 -0
  52. package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/.github/workflows/ci.yml +28 -0
  53. package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/SECURITY.md +8 -0
  54. package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/issue-479.md +35 -0
  55. package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/package.json +15 -0
  56. package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/pr-482.diff +20 -0
  57. package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/run-log.txt +30 -0
  58. package/packs/ai-engineering/skills/agent-incident-response/examples/injected-issue-comment.md +57 -0
  59. package/packs/ai-engineering/skills/agent-incident-response/manifest.json +55 -0
  60. package/packs/ai-engineering/skills/agent-incident-response/reference.md +152 -0
  61. package/packs/ai-engineering/skills/agent-incident-response/scripts/find_secret_exposure.py +239 -0
  62. package/packs/ai-engineering/skills/agent-incident-response/scripts/usage_split.py +194 -0
  63. package/packs/converters/skills/file-to-markdown/SKILL.md +2 -1
  64. package/packs/converters/skills/file-to-markdown/evals/eval_queries.json +4 -0
  65. package/packs/converters/skills/file-to-markdown/manifest.json +2 -2
  66. package/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +253 -3
  67. package/packs/converters/skills/md-to-confluence/SKILL.md +139 -0
  68. package/packs/converters/skills/md-to-confluence/evals/eval_queries.json +42 -0
  69. package/packs/converters/skills/md-to-confluence/evals/evals.json +89 -0
  70. package/packs/converters/skills/md-to-confluence/evals/files/1/img/latency-dashboard.png +0 -0
  71. package/packs/converters/skills/md-to-confluence/evals/files/1/payments-runbook.md +60 -0
  72. package/packs/converters/skills/md-to-confluence/evals/files/2/deploy-checklist.md +33 -0
  73. package/packs/converters/skills/md-to-confluence/evals/files/3/img/after/screen.png +0 -0
  74. package/packs/converters/skills/md-to-confluence/evals/files/3/img/before/screen.png +0 -0
  75. package/packs/converters/skills/md-to-confluence/evals/files/3/release-notes-4.2.md +49 -0
  76. package/packs/converters/skills/md-to-confluence/evals/files/4/docs/img/topology.png +0 -0
  77. package/packs/converters/skills/md-to-confluence/evals/files/4/docs/ingest-pipeline-design.md +65 -0
  78. package/packs/converters/skills/md-to-confluence/evals/files/5/support/troubleshooting-sso.md +57 -0
  79. package/packs/converters/skills/md-to-confluence/examples/inputs/img/queue-depth.png +0 -0
  80. package/packs/converters/skills/md-to-confluence/examples/inputs/queue-runbook.fixed.md +21 -0
  81. package/packs/converters/skills/md-to-confluence/examples/inputs/queue-runbook.md +21 -0
  82. package/packs/converters/skills/md-to-confluence/examples/queue-runbook.md +57 -0
  83. package/packs/converters/skills/md-to-confluence/manifest.json +52 -0
  84. package/packs/converters/skills/md-to-confluence/reference.md +92 -0
  85. package/packs/converters/skills/md-to-confluence/scripts/md_to_confluence.py +821 -0
  86. package/packs/converters/skills/md-to-pptx/SKILL.md +131 -0
  87. package/packs/converters/skills/md-to-pptx/evals/eval_queries.json +12 -0
  88. package/packs/converters/skills/md-to-pptx/evals/evals.json +84 -0
  89. package/packs/converters/skills/md-to-pptx/evals/files/1/img/defect-burndown.png +0 -0
  90. package/packs/converters/skills/md-to-pptx/evals/files/1/status-report.md +75 -0
  91. package/packs/converters/skills/md-to-pptx/evals/files/3/brand.json +12 -0
  92. package/packs/converters/skills/md-to-pptx/evals/files/3/night-shift-induction.md +53 -0
  93. package/packs/converters/skills/md-to-pptx/evals/files/4/charts/ontime.png +0 -0
  94. package/packs/converters/skills/md-to-pptx/evals/files/4/qbr-q3.md +85 -0
  95. package/packs/converters/skills/md-to-pptx/evals/files/5/vendor-sync-notes.md +28 -0
  96. package/packs/converters/skills/md-to-pptx/examples/support-weekly-input.md +48 -0
  97. package/packs/converters/skills/md-to-pptx/examples/support-weekly.md +75 -0
  98. package/packs/converters/skills/md-to-pptx/examples/tickets-by-week.png +0 -0
  99. package/packs/converters/skills/md-to-pptx/manifest.json +49 -0
  100. package/packs/converters/skills/md-to-pptx/reference.md +139 -0
  101. package/packs/converters/skills/md-to-pptx/scripts/md_to_pptx.py +1841 -0
  102. package/packs/dev-team/loops/modernize/LOOP.md +124 -0
  103. package/packs/dev-team/loops/modernize/evals/eval_queries.json +9 -0
  104. package/packs/dev-team/loops/modernize/evals/evals.json +27 -0
  105. package/packs/dev-team/loops/modernize/loop.json +60 -0
  106. package/packs/dev-team/skills/behaviour-equivalence/SKILL.md +155 -0
  107. package/packs/dev-team/skills/behaviour-equivalence/evals/eval_queries.json +11 -0
  108. package/packs/dev-team/skills/behaviour-equivalence/evals/evals.json +76 -0
  109. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/cases.jsonl +12 -0
  110. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/new.jsonl +12 -0
  111. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/notes.md +15 -0
  112. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/old.jsonl +12 -0
  113. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/old_rerun.jsonl +12 -0
  114. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/pricing_v1.py +45 -0
  115. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/pricing_v2.py +65 -0
  116. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/rules.json +10 -0
  117. package/packs/dev-team/skills/behaviour-equivalence/evals/files/3/diff-summary.txt +16 -0
  118. package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/diff.txt +13 -0
  119. package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/fee_calc_py2.py +16 -0
  120. package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/fee_calc_py3.py +24 -0
  121. package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/input.csv +13 -0
  122. package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/new_output.csv +13 -0
  123. package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/old_output.csv +13 -0
  124. package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/TEAM_NOTES.md +19 -0
  125. package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/normalisation.yaml +12 -0
  126. package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/new_run.jsonl +8 -0
  127. package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/old_run1.jsonl +8 -0
  128. package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/old_run2.jsonl +8 -0
  129. package/packs/dev-team/skills/behaviour-equivalence/examples/delivery-date.md +78 -0
  130. package/packs/dev-team/skills/behaviour-equivalence/manifest.json +33 -0
  131. package/packs/dev-team/skills/behaviour-equivalence/reference.md +97 -0
  132. package/packs/dev-team/skills/behaviour-equivalence/scripts/golden_diff.py +524 -0
  133. package/packs/dev-team/skills/behaviour-equivalence/templates/accepted.json +10 -0
  134. package/packs/dev-team/skills/behaviour-equivalence/templates/rules.json +7 -0
  135. package/packs/frontend/pack.json +55 -0
  136. package/packs/frontend/skills/rendering-strategy/SKILL.md +149 -0
  137. package/packs/frontend/skills/rendering-strategy/evals/eval_queries.json +38 -0
  138. package/packs/frontend/skills/rendering-strategy/evals/evals.json +77 -0
  139. package/packs/frontend/skills/rendering-strategy/evals/files/1/fernhill-app.md +40 -0
  140. package/packs/frontend/skills/rendering-strategy/evals/files/3/routes.md +33 -0
  141. package/packs/frontend/skills/rendering-strategy/evals/files/4/build-notes.txt +5 -0
  142. package/packs/frontend/skills/rendering-strategy/evals/files/4/nuxt.config.ts +19 -0
  143. package/packs/frontend/skills/rendering-strategy/evals/files/4/pages/recipes/[slug].vue +37 -0
  144. package/packs/frontend/skills/rendering-strategy/evals/files/4/routes.md +15 -0
  145. package/packs/frontend/skills/rendering-strategy/evals/files/5/routes.txt +18 -0
  146. package/packs/frontend/skills/rendering-strategy/evals/files/5/src/hooks.server.ts +14 -0
  147. package/packs/frontend/skills/rendering-strategy/evals/files/5/src/routes/(marketing)/pricing/+page.server.ts +10 -0
  148. package/packs/frontend/skills/rendering-strategy/evals/files/5/src/routes/+layout.server.ts +10 -0
  149. package/packs/frontend/skills/rendering-strategy/evals/files/5/svelte.config.js +10 -0
  150. package/packs/frontend/skills/rendering-strategy/evals/files/5/traffic.md +18 -0
  151. package/packs/frontend/skills/rendering-strategy/examples/saas-site.md +42 -0
  152. package/packs/frontend/skills/rendering-strategy/manifest.json +50 -0
  153. package/packs/frontend/skills/rendering-strategy/reference.md +116 -0
  154. package/packs/frontend/skills/rendering-strategy/scripts/render_load.py +181 -0
  155. package/packs/frontend/skills/web-performance-review/SKILL.md +106 -0
  156. package/packs/frontend/skills/web-performance-review/evals/eval_queries.json +38 -0
  157. package/packs/frontend/skills/web-performance-review/evals/evals.json +76 -0
  158. package/packs/frontend/skills/web-performance-review/evals/files/1/psi.json +1065 -0
  159. package/packs/frontend/skills/web-performance-review/evals/files/2/after.json +359 -0
  160. package/packs/frontend/skills/web-performance-review/evals/files/2/before.json +375 -0
  161. package/packs/frontend/skills/web-performance-review/evals/files/4/psi-pdp-mobile-2026-10-19.json +171 -0
  162. package/packs/frontend/skills/web-performance-review/evals/files/4/rum-inp-attribution-oct14-19.csv +6 -0
  163. package/packs/frontend/skills/web-performance-review/evals/files/4/rum-inp-daily.csv +15 -0
  164. package/packs/frontend/skills/web-performance-review/evals/files/5/crux-home-phone.json +35 -0
  165. package/packs/frontend/skills/web-performance-review/evals/files/5/lighthouse-home-mobile.json +173 -0
  166. package/packs/frontend/skills/web-performance-review/examples/quayside-article-lhr.json +480 -0
  167. package/packs/frontend/skills/web-performance-review/examples/quayside-article.md +103 -0
  168. package/packs/frontend/skills/web-performance-review/manifest.json +48 -0
  169. package/packs/frontend/skills/web-performance-review/reference.md +105 -0
  170. package/packs/frontend/skills/web-performance-review/scripts/lh_summary.py +743 -0
  171. package/packs/product-manager/skills/lean-canvas/SKILL.md +162 -0
  172. package/packs/product-manager/skills/lean-canvas/evals/eval_queries.json +42 -0
  173. package/packs/product-manager/skills/lean-canvas/evals/evals.json +82 -0
  174. package/packs/product-manager/skills/lean-canvas/evals/files/1/pawpass-notes.md +54 -0
  175. package/packs/product-manager/skills/lean-canvas/evals/files/3/lunchloop-pitch.md +30 -0
  176. package/packs/product-manager/skills/lean-canvas/evals/files/4/clubkit-notes.md +44 -0
  177. package/packs/product-manager/skills/lean-canvas/evals/files/5/cost-model.csv +5 -0
  178. package/packs/product-manager/skills/lean-canvas/evals/files/5/proposal.md +40 -0
  179. package/packs/product-manager/skills/lean-canvas/evals/files/5/stakeholder-emails.txt +18 -0
  180. package/packs/product-manager/skills/lean-canvas/examples/bike-repair.md +72 -0
  181. package/packs/product-manager/skills/lean-canvas/manifest.json +34 -0
  182. package/packs/product-manager/skills/lean-canvas/reference.md +102 -0
  183. package/packs/product-manager/skills/lean-canvas/scripts/unit_economics.py +211 -0
  184. package/packs/solution-architect/skills/api-contract-draft/SKILL.md +2 -2
  185. package/packs/solution-architect/skills/api-contract-draft/manifest.json +28 -6
  186. package/packs/solution-architect/skills/event-contract-draft/SKILL.md +167 -0
  187. package/packs/solution-architect/skills/event-contract-draft/evals/eval_queries.json +38 -0
  188. package/packs/solution-architect/skills/event-contract-draft/evals/evals.json +76 -0
  189. package/packs/solution-architect/skills/event-contract-draft/evals/files/1/order-events-brief.md +45 -0
  190. package/packs/solution-architect/skills/event-contract-draft/evals/files/4/NOTES.md +23 -0
  191. package/packs/solution-architect/skills/event-contract-draft/evals/files/4/schemas/shipment-update.json +40 -0
  192. package/packs/solution-architect/skills/event-contract-draft/evals/files/5/invoice.example.json +25 -0
  193. package/packs/solution-architect/skills/event-contract-draft/examples/order-service.asyncapi.json +246 -0
  194. package/packs/solution-architect/skills/event-contract-draft/examples/order-service.md +92 -0
  195. package/packs/solution-architect/skills/event-contract-draft/manifest.json +25 -0
  196. package/packs/solution-architect/skills/event-contract-draft/reference.md +113 -0
  197. package/packs/solution-architect/skills/event-contract-draft/scripts/check_asyncapi.py +502 -0
  198. package/packs/solution-architect/skills/event-contract-draft/templates/asyncapi-skeleton.json +144 -0
  199. package/packs/sre-oncall/skills/slo-spec/SKILL.md +161 -0
  200. package/packs/sre-oncall/skills/slo-spec/evals/eval_queries.json +38 -0
  201. package/packs/sre-oncall/skills/slo-spec/evals/evals.json +75 -0
  202. package/packs/sre-oncall/skills/slo-spec/evals/files/1/order-service.md +51 -0
  203. package/packs/sre-oncall/skills/slo-spec/evals/files/3/search-slo-status.md +30 -0
  204. package/packs/sre-oncall/skills/slo-spec/evals/files/4/alerts/payments-api.rules.yaml +16 -0
  205. package/packs/sre-oncall/skills/slo-spec/evals/files/4/docs/dependencies.md +8 -0
  206. package/packs/sre-oncall/skills/slo-spec/evals/files/4/metrics/summary-90d.md +51 -0
  207. package/packs/sre-oncall/skills/slo-spec/evals/files/5/ops-notes.md +34 -0
  208. package/packs/sre-oncall/skills/slo-spec/examples/ticket-booking.md +88 -0
  209. package/packs/sre-oncall/skills/slo-spec/manifest.json +34 -0
  210. package/packs/sre-oncall/skills/slo-spec/reference.md +113 -0
  211. package/packs/sre-oncall/skills/slo-spec/scripts/slo_math.py +449 -0
  212. package/packs/sre-oncall/skills/slo-spec/templates/slo-spec.md +35 -0
@@ -0,0 +1,45 @@
1
+ """Legacy quote service (Python 2-era code, ported to 3 in 2021). Being retired."""
2
+ import random
3
+ from datetime import datetime, timezone
4
+ from decimal import ROUND_HALF_UP, Decimal
5
+
6
+ CATALOGUE = {
7
+ "TEA-001": (450, "standard"),
8
+ "MUG-002": (899, "standard"),
9
+ "BOOK-05": (1299, "zero"),
10
+ "CHILD-SEAT-01": (12500, "reduced"),
11
+ "BOOSTER-02": (2250, "reduced"),
12
+ "PRAM-03": (31090, "reduced"),
13
+ }
14
+ VAT = {"standard": Decimal("0.20"), "reduced": Decimal("0.05"), "zero": Decimal("0")}
15
+
16
+
17
+ def quote(request):
18
+ items = request.get("items") or []
19
+ if not items:
20
+ return 422, {"error": "empty_basket"}
21
+ qty_by_sku = {}
22
+ for item in items:
23
+ if item["sku"] not in CATALOGUE:
24
+ return 422, {"error": "unknown_sku", "sku": item["sku"]}
25
+ if item["qty"] < 1:
26
+ return 422, {"error": "invalid_qty", "sku": item["sku"]}
27
+ qty_by_sku[item["sku"]] = qty_by_sku.get(item["sku"], 0) + item["qty"]
28
+ lines = []
29
+ for sku in set(qty_by_sku): # set: line order is whatever the hash gives
30
+ price, band = CATALOGUE[sku]
31
+ line_total = price * qty_by_sku[sku]
32
+ vat = int((Decimal(line_total) * VAT[band]).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
33
+ lines.append({"sku": sku, "qty": qty_by_sku[sku], "unit_price_minor": price,
34
+ "line_total_minor": line_total, "vat_band": band, "vat_minor": vat})
35
+ subtotal = sum(l["line_total_minor"] for l in lines)
36
+ vat_total = sum(l["vat_minor"] for l in lines)
37
+ return 200, {
38
+ "quote_id": "Q-%08x" % random.getrandbits(32),
39
+ "quoted_at": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
40
+ "currency": request.get("currency", "GBP"),
41
+ "lines": lines,
42
+ "subtotal_minor": subtotal,
43
+ "vat_minor": vat_total,
44
+ "total_minor": subtotal + vat_total,
45
+ }
@@ -0,0 +1,65 @@
1
+ """Quote service rewrite. Same contract as pricing_v1, catalogue now loaded from config."""
2
+ import secrets
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timezone
5
+ from decimal import Decimal
6
+
7
+ CATALOGUE = {
8
+ "TEA-001": (450, "standard"),
9
+ "MUG-002": (899, "standard"),
10
+ "BOOK-05": (1299, "zero"),
11
+ "CHILD-SEAT-01": (12500, "reduced"),
12
+ "BOOSTER-02": (2250, "reduced"),
13
+ "PRAM-03": (31090, "reduced"),
14
+ }
15
+ VAT = {"standard": Decimal("0.20"), "reduced": Decimal("0.05"), "zero": Decimal("0")}
16
+
17
+
18
+ class QuoteError(Exception):
19
+ def __init__(self, code, sku=None):
20
+ super().__init__(code)
21
+ self.body = {"error": code} if sku is None else {"error": code, "sku": sku}
22
+
23
+
24
+ @dataclass
25
+ class Line:
26
+ sku: str
27
+ qty: int
28
+
29
+ def to_dict(self):
30
+ price, band = CATALOGUE[self.sku]
31
+ line_total = price * self.qty
32
+ vat = round(Decimal(line_total) * VAT[band])
33
+ return {"sku": self.sku, "qty": self.qty, "unit_price_minor": price,
34
+ "line_total_minor": line_total, "vat_band": band, "vat_minor": int(vat)}
35
+
36
+
37
+ def _lines(items):
38
+ if not items:
39
+ raise QuoteError("empty_basket")
40
+ merged = {}
41
+ for item in items:
42
+ if item["sku"] not in CATALOGUE:
43
+ raise QuoteError("unknown_sku", item["sku"])
44
+ if item["qty"] < 1:
45
+ raise QuoteError("invalid_qty", item["sku"])
46
+ merged[item["sku"]] = merged.get(item["sku"], 0) + item["qty"]
47
+ return [Line(sku, qty).to_dict() for sku, qty in sorted(merged.items())]
48
+
49
+
50
+ def quote(request):
51
+ try:
52
+ lines = _lines(request.get("items") or [])
53
+ except QuoteError as err:
54
+ return 422, err.body
55
+ subtotal = sum(l["line_total_minor"] for l in lines)
56
+ vat_total = sum(l["vat_minor"] for l in lines)
57
+ return 200, {
58
+ "quote_id": "Q-" + secrets.token_hex(4),
59
+ "quoted_at": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
60
+ "currency": request.get("currency", "GBP"),
61
+ "lines": lines,
62
+ "subtotal_minor": subtotal,
63
+ "vat_minor": vat_total,
64
+ "total_minor": subtotal + vat_total,
65
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "rules": [
3
+ {"path": "body.quoted_at", "rule": "ignore", "reason": "wall-clock time of the call"},
4
+ {"path": "body.quote_id", "rule": "regex", "pattern": "^Q-[0-9a-f]{8}$", "replace": "Q-<id>", "reason": "random per call"},
5
+ {"path": "body.lines", "rule": "sort", "by": "sku", "reason": "line order is not part of the contract"},
6
+ {"path": "body.lines[].vat_minor", "rule": "tolerance", "abs": 1, "reason": "penny rounding noise between Decimal implementations"},
7
+ {"path": "body.vat_minor", "rule": "tolerance", "abs": 1, "reason": "penny rounding noise"},
8
+ {"path": "body.total_minor", "rule": "tolerance", "abs": 1, "reason": "penny rounding noise"}
9
+ ]
10
+ }
@@ -0,0 +1,16 @@
1
+ golden master: search-api v1 (legacy) vs search-api v2 (rewrite)
2
+ replayed: 400 requests sampled from prod access logs, 1-7 Oct, PII scrubbed
3
+ normalised: took_ms, request_id, served_by
4
+ old-vs-old rerun: not done
5
+
6
+ result: 340 match, 60 differ
7
+
8
+ field cases example (case: old -> new)
9
+ results[].id (order) 41 s0112: [p-88, p-91, p-17] -> [p-91, p-88, p-17] (p-88 and p-91 both score 0.731)
10
+ error.message 16 s0207: "No results for query" -> "We couldn't find anything matching 'blue suede'"
11
+ status 3 s0350: 404 -> 200 (body: {"results": [], "total": 0})
12
+
13
+ notes from Dan (search lead):
14
+ - the ordering ones are ties, any order is fine
15
+ - product (Mei) signed off the new error copy on 3 Oct
16
+ - so really it's all cosmetic. ok to switch traffic?
@@ -0,0 +1,13 @@
1
+ $ diff <(cut -d, -f1,2 old_output.csv | sort) <(cut -d, -f1,2 new_output.csv | sort)
2
+ 1c1
3
+ < A1001,163
4
+ ---
5
+ > A1001,162
6
+ 9c9
7
+ < A1009,361
8
+ ---
9
+ > A1009,360
10
+ 11c11
11
+ < A1011,113
12
+ ---
13
+ > A1011,112
@@ -0,0 +1,16 @@
1
+ # fee_calc.py -- Python 2.7, runs nightly on legacy box batch01
2
+ import csv, sys, datetime
3
+
4
+ def main(inp, outp):
5
+ fees = {}
6
+ for row in csv.DictReader(open(inp, 'rb')):
7
+ fee = int(round(int(row['amount_cents']) * int(row['rate_bp']) / 10000.0))
8
+ fees[row['account_id']] = (fee, row['currency'])
9
+ ts = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
10
+ w = csv.writer(open(outp, 'wb'))
11
+ w.writerow(['account_id', 'fee_cents', 'currency', 'run_ts'])
12
+ for acct, (fee, ccy) in fees.items():
13
+ w.writerow([acct, fee, ccy, ts])
14
+
15
+ if __name__ == '__main__':
16
+ main(sys.argv[1], sys.argv[2])
@@ -0,0 +1,24 @@
1
+ # fee_calc.py -- Python 3.12 port
2
+ import csv
3
+ import sys
4
+ from datetime import datetime
5
+
6
+
7
+ def main(inp, outp):
8
+ fees = {}
9
+ with open(inp, newline="") as f:
10
+ for row in csv.DictReader(f):
11
+ fee = round(int(row["amount_cents"]) * int(row["rate_bp"]) / 10000)
12
+ # no fee -> no currency needed downstream
13
+ ccy = row["currency"] if fee > 0 else ""
14
+ fees[row["account_id"]] = (fee, ccy)
15
+ ts = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
16
+ with open(outp, "w", newline="") as f:
17
+ w = csv.writer(f)
18
+ w.writerow(["account_id", "fee_cents", "currency", "run_ts"])
19
+ for acct, (fee, ccy) in fees.items():
20
+ w.writerow([acct, fee, ccy, ts])
21
+
22
+
23
+ if __name__ == "__main__":
24
+ main(sys.argv[1], sys.argv[2])
@@ -0,0 +1,13 @@
1
+ account_id,amount_cents,rate_bp,currency
2
+ A1001,32500,50,GBP
3
+ A1002,48210,75,GBP
4
+ A1003,33500,50,GBP
5
+ A1004,100000,120,GBP
6
+ A1005,0,50,GBP
7
+ A1006,129000,50,GBP
8
+ A1007,9000,75,GBP
9
+ A1008,25000,50,GBP
10
+ A1009,72100,50,GBP
11
+ A1010,61999,120,GBP
12
+ A1011,15000,75,GBP
13
+ A1012,87650,50,GBP
@@ -0,0 +1,13 @@
1
+ account_id,fee_cents,currency,run_ts
2
+ A1001,162,GBP,2026-10-19T02:00:01
3
+ A1002,362,GBP,2026-10-19T02:00:01
4
+ A1003,168,GBP,2026-10-19T02:00:01
5
+ A1004,1200,GBP,2026-10-19T02:00:01
6
+ A1005,0,,2026-10-19T02:00:01
7
+ A1006,645,GBP,2026-10-19T02:00:01
8
+ A1007,68,GBP,2026-10-19T02:00:01
9
+ A1008,125,GBP,2026-10-19T02:00:01
10
+ A1009,360,GBP,2026-10-19T02:00:01
11
+ A1010,744,GBP,2026-10-19T02:00:01
12
+ A1011,112,GBP,2026-10-19T02:00:01
13
+ A1012,438,GBP,2026-10-19T02:00:01
@@ -0,0 +1,13 @@
1
+ account_id,fee_cents,currency,run_ts
2
+ A1007,68,GBP,2026-10-19T02:00:04
3
+ A1002,362,GBP,2026-10-19T02:00:04
4
+ A1011,113,GBP,2026-10-19T02:00:04
5
+ A1004,1200,GBP,2026-10-19T02:00:04
6
+ A1009,361,GBP,2026-10-19T02:00:04
7
+ A1001,163,GBP,2026-10-19T02:00:04
8
+ A1012,438,GBP,2026-10-19T02:00:04
9
+ A1005,0,GBP,2026-10-19T02:00:04
10
+ A1010,744,GBP,2026-10-19T02:00:04
11
+ A1003,168,GBP,2026-10-19T02:00:04
12
+ A1008,125,GBP,2026-10-19T02:00:04
13
+ A1006,645,GBP,2026-10-19T02:00:04
@@ -0,0 +1,19 @@
1
+ # orders-api: Express -> Fastify migration, parity notes
2
+
3
+ Replay setup: the 8 requests in `recordings/` were sent to the old Express service twice
4
+ (old_run1, old_run2) and once to the new Fastify service (new_run), against the same seeded
5
+ staging database (reset between runs).
6
+
7
+ Result after applying `normalisation.yaml`: 7 of 8 exchanges identical.
8
+
9
+ The one remaining diff is seq 4 (`GET /orders/ord_bad!!`, 400 vs 500). That's a garbage id
10
+ no real client sends, so we're treating it as noise.
11
+
12
+ Conclusion: **equivalent**. Proposing cutover Tuesday 27 Oct.
13
+
14
+ Context for reviewers:
15
+ - Consumers: the web checkout and the Android app. The Android app switches on `error.code`
16
+ to choose which message to show the user.
17
+ - The web checkout reads `total` and passes it straight to the payment widget as a string.
18
+
19
+ -- Dana Okafor, platform-orders
@@ -0,0 +1,12 @@
1
+ # orders-api parity replay -- normalisation applied before comparing old vs new
2
+ # owner: platform-orders team
3
+ ignore:
4
+ - response.headers.date
5
+ - response.headers.content-type
6
+ - response.body.request_id
7
+ - response.body.created_at
8
+ - response.body.id
9
+ - response.body.error # error messages were reworded in the rewrite, not a behaviour change
10
+ coerce:
11
+ numeric_strings_to_numbers: true # "19.90" == 19.9
12
+ sort_all_arrays: true
@@ -0,0 +1,8 @@
1
+ {"seq": 1, "request": {"method": "GET", "path": "/orders/ord_1001", "body": null}, "response": {"status": 200, "headers": {"content-type": "application/json", "date": "Mon, 19 Oct 2026 14:15:30 GMT"}, "body": {"id": "ord_1001", "status": "shipped", "total": 19.9, "currency": "EUR", "tags": ["express", "gift"], "created_at": "2026-10-01T09:15:00Z", "request_id": "req_e88b1"}}}
2
+ {"seq": 2, "request": {"method": "GET", "path": "/orders/ord_1002", "body": null}, "response": {"status": 200, "headers": {"content-type": "application/json", "date": "Mon, 19 Oct 2026 14:15:30 GMT"}, "body": {"id": "ord_1002", "status": "pending", "total": 5.0, "currency": "EUR", "tags": ["bulk"], "created_at": "2026-10-03T16:40:00Z", "request_id": "req_e88b2"}}}
3
+ {"seq": 3, "request": {"method": "GET", "path": "/orders/ord_9999", "body": null}, "response": {"status": 404, "headers": {"content-type": "application/json", "date": "Mon, 19 Oct 2026 14:15:30 GMT"}, "body": {"error": {"code": "ORDER_NOT_FOUND", "message": "Order not found"}, "request_id": "req_e88b3"}}}
4
+ {"seq": 4, "request": {"method": "GET", "path": "/orders/ord_bad!!", "body": null}, "response": {"status": 500, "headers": {"content-type": "application/json", "date": "Mon, 19 Oct 2026 14:15:30 GMT"}, "body": {"error": {"code": "INTERNAL_ERROR", "message": "Something went wrong"}, "request_id": "req_e88b4"}}}
5
+ {"seq": 5, "request": {"method": "POST", "path": "/orders", "body": {"sku": "SKU-7", "quantity": 2}}, "response": {"status": 201, "headers": {"content-type": "application/json", "date": "Mon, 19 Oct 2026 14:15:30 GMT"}, "body": {"id": "ord_2003", "status": "pending", "total": 39.8, "currency": "EUR", "tags": [], "created_at": "2026-10-19T14:15:30Z", "request_id": "req_e88b5"}}}
6
+ {"seq": 6, "request": {"method": "POST", "path": "/orders", "body": {"sku": "SKU-7", "quantity": 0}}, "response": {"status": 422, "headers": {"content-type": "application/json", "date": "Mon, 19 Oct 2026 14:15:30 GMT"}, "body": {"error": {"code": "VALIDATION_ERROR", "message": "body/quantity must be >= 1"}, "request_id": "req_e88b6"}}}
7
+ {"seq": 7, "request": {"method": "GET", "path": "/orders?customer=c_42", "body": null}, "response": {"status": 200, "headers": {"content-type": "application/json", "date": "Mon, 19 Oct 2026 14:15:30 GMT"}, "body": {"orders": [{"id": "ord_1001", "total": 19.9}, {"id": "ord_1002", "total": 5.0}], "request_id": "req_e88b7"}}}
8
+ {"seq": 8, "request": {"method": "DELETE", "path": "/orders/ord_1002", "body": null}, "response": {"status": 204, "headers": {"date": "Mon, 19 Oct 2026 14:15:30 GMT"}, "body": null}}
@@ -0,0 +1,8 @@
1
+ {"seq": 1, "request": {"method": "GET", "path": "/orders/ord_1001", "body": null}, "response": {"status": 200, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:02:11 GMT"}, "body": {"id": "ord_1001", "status": "shipped", "total": "19.90", "currency": "EUR", "tags": ["gift", "express"], "created_at": "2026-10-01T09:15:00Z", "request_id": "req_7f3a1"}}}
2
+ {"seq": 2, "request": {"method": "GET", "path": "/orders/ord_1002", "body": null}, "response": {"status": 200, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:02:11 GMT"}, "body": {"id": "ord_1002", "status": "pending", "total": "5.00", "currency": "EUR", "tags": ["bulk"], "created_at": "2026-10-03T16:40:00Z", "request_id": "req_7f3a2"}}}
3
+ {"seq": 3, "request": {"method": "GET", "path": "/orders/ord_9999", "body": null}, "response": {"status": 404, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:02:11 GMT"}, "body": {"error": {"code": "ORDER_NOT_FOUND", "message": "Order ord_9999 not found"}, "request_id": "req_7f3a3"}}}
4
+ {"seq": 4, "request": {"method": "GET", "path": "/orders/ord_bad!!", "body": null}, "response": {"status": 400, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:02:11 GMT"}, "body": {"error": {"code": "INVALID_ORDER_ID", "message": "Order id must match ord_[0-9]+"}, "request_id": "req_7f3a4"}}}
5
+ {"seq": 5, "request": {"method": "POST", "path": "/orders", "body": {"sku": "SKU-7", "quantity": 2}}, "response": {"status": 201, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:02:11 GMT"}, "body": {"id": "ord_2001", "status": "pending", "total": "39.80", "currency": "EUR", "tags": [], "created_at": "2026-10-19T14:02:11Z", "request_id": "req_7f3a5"}}}
6
+ {"seq": 6, "request": {"method": "POST", "path": "/orders", "body": {"sku": "SKU-7", "quantity": 0}}, "response": {"status": 422, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:02:11 GMT"}, "body": {"error": {"code": "QUANTITY_INVALID", "message": "quantity must be >= 1"}, "request_id": "req_7f3a6"}}}
7
+ {"seq": 7, "request": {"method": "GET", "path": "/orders?customer=c_42", "body": null}, "response": {"status": 200, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:02:11 GMT"}, "body": {"orders": [{"id": "ord_1001", "total": "19.90"}, {"id": "ord_1002", "total": "5.00"}], "request_id": "req_7f3a7"}}}
8
+ {"seq": 8, "request": {"method": "DELETE", "path": "/orders/ord_1002", "body": null}, "response": {"status": 204, "headers": {"date": "Mon, 19 Oct 2026 14:02:11 GMT"}, "body": null}}
@@ -0,0 +1,8 @@
1
+ {"seq": 1, "request": {"method": "GET", "path": "/orders/ord_1001", "body": null}, "response": {"status": 200, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:09:47 GMT"}, "body": {"id": "ord_1001", "status": "shipped", "total": "19.90", "currency": "EUR", "tags": ["express", "gift"], "created_at": "2026-10-01T09:15:00Z", "request_id": "req_c2191"}}}
2
+ {"seq": 2, "request": {"method": "GET", "path": "/orders/ord_1002", "body": null}, "response": {"status": 200, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:09:47 GMT"}, "body": {"id": "ord_1002", "status": "pending", "total": "5.00", "currency": "EUR", "tags": ["bulk"], "created_at": "2026-10-03T16:40:00Z", "request_id": "req_c2192"}}}
3
+ {"seq": 3, "request": {"method": "GET", "path": "/orders/ord_9999", "body": null}, "response": {"status": 404, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:09:47 GMT"}, "body": {"error": {"code": "ORDER_NOT_FOUND", "message": "Order ord_9999 not found"}, "request_id": "req_c2193"}}}
4
+ {"seq": 4, "request": {"method": "GET", "path": "/orders/ord_bad!!", "body": null}, "response": {"status": 400, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:09:47 GMT"}, "body": {"error": {"code": "INVALID_ORDER_ID", "message": "Order id must match ord_[0-9]+"}, "request_id": "req_c2194"}}}
5
+ {"seq": 5, "request": {"method": "POST", "path": "/orders", "body": {"sku": "SKU-7", "quantity": 2}}, "response": {"status": 201, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:09:47 GMT"}, "body": {"id": "ord_2002", "status": "pending", "total": "39.80", "currency": "EUR", "tags": [], "created_at": "2026-10-19T14:09:47Z", "request_id": "req_c2195"}}}
6
+ {"seq": 6, "request": {"method": "POST", "path": "/orders", "body": {"sku": "SKU-7", "quantity": 0}}, "response": {"status": 422, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:09:47 GMT"}, "body": {"error": {"code": "QUANTITY_INVALID", "message": "quantity must be >= 1"}, "request_id": "req_c2196"}}}
7
+ {"seq": 7, "request": {"method": "GET", "path": "/orders?customer=c_42", "body": null}, "response": {"status": 200, "headers": {"content-type": "application/json; charset=utf-8", "date": "Mon, 19 Oct 2026 14:09:47 GMT"}, "body": {"orders": [{"id": "ord_1001", "total": "19.90"}, {"id": "ord_1002", "total": "5.00"}], "request_id": "req_c2197"}}}
8
+ {"seq": 8, "request": {"method": "DELETE", "path": "/orders/ord_1002", "body": null}, "response": {"status": 204, "headers": {"date": "Mon, 19 Oct 2026 14:09:47 GMT"}, "body": null}}
@@ -0,0 +1,78 @@
1
+ # Worked example: delivery-estimate endpoint rewrite
2
+
3
+ ## Input given to the skill
4
+
5
+ > We've rewritten `GET /v1/delivery-estimate` (Ruby → Kotlin). I replayed 8 requests against
6
+ > both and recorded the responses: `old_a.jsonl` and `old_b.jsonl` are two runs of the old
7
+ > service on the same requests, `new.jsonl` is the new one. Rule we care about: orders placed
8
+ > before 22:00 dispatch the same day, weekdays only. The diff is full of IDs and timestamps.
9
+ > Is it the same?
10
+
11
+ Each record is `{"case_id", "status", "body"}`; the body has `estimate_id`, `generated_at`,
12
+ `dispatch_date`, `delivery_date` and a `carriers` list. Requests (`placed_at` in UTC):
13
+
14
+ | Case | placed_at | Postcode |
15
+ |---|---|---|
16
+ | e01 | 2026-07-14 08:05 | SW1A 1AA |
17
+ | e02 | 2026-07-14 21:30 | SW1A 1AA |
18
+ | e03 | 2026-01-13 21:30 | SW1A 1AA |
19
+ | e04 | 2026-07-18 10:00 (Saturday) | EH1 1AA |
20
+ | e05 | 2026-07-14 09:00 | ZZ9 9ZZ (unknown) |
21
+ | e06 | 2026-01-13 22:15 | EH1 1AA |
22
+ | e07 | 2026-03-29 21:20 (Sunday, BST starts) | SW1A 1AA |
23
+ | e08 | 2026-07-15 21:59 | EH1 1AA |
24
+
25
+ ## What the run looked like
26
+
27
+ Old against old, no rules: `estimate_id`, `generated_at` and the order of `carriers` differ in
28
+ all seven 200 responses; nothing else does. Those three fields get one rule each:
29
+
30
+ ```json
31
+ {"rules": [
32
+ {"path": "body.generated_at", "rule": "ignore", "reason": "wall-clock time of the call"},
33
+ {"path": "body.estimate_id", "rule": "regex", "pattern": "^est_[0-9a-f]{10}$", "replace": "est_<id>", "reason": "random per call"},
34
+ {"path": "body.carriers", "rule": "sort", "reason": "old service shuffles; the checkout page sorts carriers itself"}
35
+ ]}
36
+ ```
37
+
38
+ Old against old with the rules: `EQUIVALENT`, 8 of 8. Old against new with the same rules:
39
+
40
+ ```
41
+ Verdict after rules: NOT EQUIVALENT
42
+
43
+ Cases: 8 old, 8 new, 8 compared: 6 matched, 2 mismatched, 0 accepted-only
44
+
45
+ Differences by field:
46
+ path diffs cases case ids
47
+ body.dispatch_date 2 2 e02, e08
48
+ body.delivery_date 2 2 e02, e08
49
+
50
+ First 4 of 4 differences:
51
+ e02 body.dispatch_date [changed] old=2026-07-15 new=2026-07-14
52
+ e02 body.delivery_date [changed] old=2026-07-16 new=2026-07-15
53
+ e08 body.dispatch_date [changed] old=2026-07-16 new=2026-07-15
54
+ e08 body.delivery_date [changed] old=2026-07-20 new=2026-07-17
55
+ ```
56
+
57
+ ## Reply
58
+
59
+ **NOT EQUIVALENT.** The new service applies the 22:00 cut-off in UTC; the old one applies it
60
+ in London time. In summer, orders placed between 21:00 and 22:00 UTC (22:00–23:00 in London)
61
+ now get same-day dispatch and a delivery date one to three days earlier than the old service
62
+ gave.
63
+
64
+ **Differences**
65
+ - 🟥 Defect: `dispatch_date` and `delivery_date`, e02 and e08. e02 (21:30 UTC, 22:30 BST): dispatch 15 Jul → 14 Jul. e08 (21:59 UTC, Wednesday): dispatch 16 Jul → 15 Jul, delivery Mon 20 Jul → Fri 17 Jul. Both are after 22:00 London time and before 22:00 UTC; e03, the same clock time in January, matches. Check where the new code converts `placed_at` before comparing with the cut-off.
66
+
67
+ **Noise, normalised:** `generated_at` (clock), `estimate_id` (random, format still checked) and
68
+ `carriers` order (shuffled by the old service, sorted by the page). All three varied between
69
+ the two old runs; nothing else did.
70
+
71
+ **Covered:** morning and late-evening weekday orders in winter and summer, a Saturday, an
72
+ unknown postcode (422, same body), a post-cut-off winter order. **Not covered:** the DST
73
+ change itself, because e07 falls on a Sunday and both versions roll it to Monday; the Friday
74
+ after-cut-off case; bank holidays, if the old service knows any.
75
+
76
+ **To confirm**
77
+ - Add a weekday order between 21:00 and 22:00 UTC on the Monday after each clock change, and a Friday 22:30 London order, then rerun.
78
+ - Fix the time-zone conversion and rerun all 8 before anyone treats the earlier dates as an improvement.
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "behaviour-equivalence",
3
+ "version": "0.1.0",
4
+ "description": "Prove that a rewritten, migrated or refactored component behaves the same as the old one with a golden-master comparison — record inputs and outputs at a boundary (HTTP, function calls, batch files, DB writes, events) from production-shaped traffic and edge cases, run the old system twice to find its nondeterminism, normalise narrowly field by field, replay against the new one, triage every difference as defect, intended change (with a named approver) or missing normalisation, and give a verdict of EQUIVALENT, EQUIVALENT EXCEPT LISTED CHANGES, NOT EQUIVALENT or NOT PROVEN. Use when the user asks \"does the new one behave the same as the old one\", \"can we sign off the rewrite\", \"are these diffs just noise\", wants a golden-master, approval or shadow-traffic comparison, a parity check between old and new outputs, or says \"prove behaviour equivalence\".",
5
+ "entrypoint": "SKILL.md",
6
+ "deps": { "npm": [], "pip": [] },
7
+ "env": { "required": [], "optional": [] },
8
+ "related": [
9
+ "codebase-tour",
10
+ "migration-plan",
11
+ "refactor-plan",
12
+ "test-plan-generator"
13
+ ],
14
+ "handoff": [
15
+ {
16
+ "to": "migration-plan",
17
+ "when": "the verdict is EQUIVALENT or EQUIVALENT EXCEPT LISTED CHANGES and traffic or data still has to move",
18
+ "purpose": "Use the parity verdict as the gate evidence for the cutover phases.",
19
+ "fallback": "If migration-plan is absent, state the cutover gate inline: the verdict, the cases it covers, and the rerun that must stay clean before each traffic step."
20
+ }
21
+ ],
22
+ "tags": ["testing", "golden-master", "legacy-code", "migration", "verification"],
23
+ "permissions": {
24
+ "network": [],
25
+ "commands": [],
26
+ "files": "named-paths",
27
+ "notes": "golden_diff.py reads the old and new recordings and the rules files it is given, and writes a report only to the -o path."
28
+ },
29
+ "model": {
30
+ "tier": "standard",
31
+ "rationale": "Designing a golden-master comparison is structured: what to record, what to normalise, and what counts as a difference; the diffing is done by its script."
32
+ }
33
+ }
@@ -0,0 +1,97 @@
1
+ # behaviour-equivalence — reference
2
+
3
+ ## What to record, per boundary
4
+
5
+ | Boundary | Record from the old system | Usually forgotten |
6
+ |---|---|---|
7
+ | HTTP endpoint | method, path, query, body, the headers that change behaviour (auth scope, locale, `Accept`); response status, body, headers clients read (`Location`, `Content-Type`, cache headers) | error responses, redirects, `Retry-After`, empty 204s |
8
+ | Function / library call | arguments, return value, exception type and message, mutations of arguments | argument mutation callers rely on, exceptions swallowed and logged |
9
+ | Batch job | input files and parameters; every output file, the reject or error file, exit code | the reject file, header and trailer records, line endings, encoding |
10
+ | Database writes | rows inserted, updated and deleted per case (query the touched tables before and after) | audit rows, soft deletes, sequence or counter bumps |
11
+ | Events / messages | topic, key, payload, headers, count and order per key | events emitted on the error path, duplicates on retry |
12
+ | Email / notifications | recipient, template, rendered fields | the ones sent on failure |
13
+
14
+ Record at the narrowest seam both versions answer at. Capture production traffic with a tee,
15
+ a shadow deployment or request logs, then scrub personal data before it leaves production:
16
+ replace names, emails and card numbers with stable placeholders so the same customer maps to
17
+ the same placeholder on both sides.
18
+
19
+ ## Making replay deterministic before normalising
20
+
21
+ Freeze what can be frozen, then normalise only what remains:
22
+
23
+ - **Clock:** inject a fixed `now` per case if both systems accept one; otherwise normalise the timestamp field.
24
+ - **Random and IDs:** seed the generator, or normalise to the format (`regex`), never ignore.
25
+ - **External calls:** replay recorded responses from dependencies (rates, stock, a tax service) so both versions see the same world.
26
+ - **Concurrency:** single-threaded replay first; ordering differences under concurrency are a separate test.
27
+
28
+ ## Normalisation catalogue
29
+
30
+ | Noise | Rule | Scope it to | Not noise when |
31
+ |---|---|---|---|
32
+ | Wall-clock timestamp | `ignore` or `regex` to `<ts>` | the one timestamp field | the field is a computed date (due date, expiry) |
33
+ | Generated ID (UUID, sequence) | `regex` on the format | the id field | the new format differs: a contract change |
34
+ | Order of an unordered collection | `sort` by a key | the one list | a caller displays or pages it, or ties are broken differently |
35
+ | Float printed by another library | `tolerance` abs or rel | the one field, with the precision it is computed to | the field is money |
36
+ | Display rounding | `round` digits | the one field | the rounded value is what is stored or charged |
37
+ | Whitespace, line endings | `regex` | the text field or file | a fixed-width consumer reads it |
38
+ | Locale formatting (`1,5` vs `1.5`) | `regex` | the field | a user sees it |
39
+ | JSON key order | none needed | compared as objects already | the consumer parses by position |
40
+
41
+ A rule is wrong when it is wider than the noise: a rule on `body`, `*` or "every timestamp
42
+ anywhere" hides the next real difference in the same subtree. The script has no any-depth
43
+ wildcard for this reason.
44
+
45
+ ## Rules-file format (for `scripts/golden_diff.py`)
46
+
47
+ ```json
48
+ {"rules": [
49
+ {"path": "body.issued_at", "rule": "ignore", "reason": "wall-clock time of the call"},
50
+ {"path": "body.invoice_id", "rule": "regex", "pattern": "^INV-[0-9]{6}$", "replace": "INV-<id>", "reason": "sequence per environment"},
51
+ {"path": "body.items", "rule": "sort", "by": "line_no", "reason": "built from a hash map"},
52
+ {"path": "body.items[].weight_kg", "rule": "tolerance", "abs": "0.0005", "reason": "computed in float, shown to 3 places"},
53
+ {"path": "body.score", "rule": "round", "digits": 3, "mode": "half_even", "reason": "ranking score printed at different precision"}
54
+ ]}
55
+ ```
56
+
57
+ Paths are dotted keys; `[]` is every element of a list, `[2]` one element, `*` any key at that
58
+ level, `$` the whole record (text files). `ignore` replaces the value but keeps presence, so a
59
+ field that disappears is still a difference. `ignore`, `regex` and `round` run first, then
60
+ `sort`, then the comparison with tolerances. Numbers are compared as decimals, so `1` equals
61
+ `1.0` and no float error creeps in; `true` never equals `1`.
62
+
63
+ Accepted changes go in a separate file so noise and intended changes never mix:
64
+
65
+ ```json
66
+ {"accepted": [
67
+ {"path": "body.error.message", "cases": ["c14", "c15"], "approver": "<name from the input>", "reason": "new error copy, ticket <id from the input>"}
68
+ ]}
69
+ ```
70
+
71
+ The script refuses an accepted change with no approver, and warns about one that matched nothing.
72
+
73
+ ## Difference patterns and their usual causes
74
+
75
+ | What the differing cases share | Usual cause | Check |
76
+ |---|---|---|
77
+ | Values on an exact .5 of the smallest unit | rounding mode (half-up vs half-even, or truncation) | find the rounding call in both versions |
78
+ | Off by one unit on totals only | rounding per line vs on the total | compare line-level fields too |
79
+ | Dates one day apart, times near midnight | time zone (UTC vs local), or DST | add cases either side of midnight and of each DST change |
80
+ | Leap day, 31st, month end | date arithmetic library | add 29 Feb, 31 Jan + 1 month |
81
+ | Non-ASCII input | encoding, normalisation form, collation | add accented, CJK and emoji cases |
82
+ | Order differs only among equal sort keys | unstable sort or a new tie-break | does the old system give the same order twice? |
83
+ | Error status or exception type | validation moved layers, framework default | compare status and body, not only the message |
84
+ | Missing rows, events or files | side effect dropped in the rewrite | diff the writes per case |
85
+ | Large inputs only | limits, pagination, overflow | add a case at and past the maximum |
86
+
87
+ ## Coverage line
88
+
89
+ One row per input class, not per test:
90
+
91
+ | Class | Cases | Branch in the old code |
92
+ |---|---|---|
93
+ | single item, standard rate | c01 | `quote` main path |
94
+ | unknown id | not recorded | error path: equivalence not proven here |
95
+
96
+ The verdict holds for the rows with cases. A class production sends with no case keeps the
97
+ verdict at NOT PROVEN, however clean the rest is.