skilldrop-cli 0.16.44 → 0.16.46

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 (255) hide show
  1. package/catalogue.json +1 -0
  2. package/model-routing.json +1 -0
  3. package/package.json +1 -1
  4. package/packs/converters/skills/md-to-html/manifest.json +1 -1
  5. package/packs/converters/skills/md-to-html/scripts/md_to_html.py +3 -0
  6. package/packs/design/skills/brand-kit/manifest.json +1 -1
  7. package/packs/design/skills/brand-kit/scripts/check_brand.py +11 -1
  8. package/packs/dev-team/skills/behaviour-equivalence/manifest.json +1 -1
  9. package/packs/dev-team/skills/behaviour-equivalence/scripts/golden_diff.py +11 -1
  10. package/packs/frontend/skills/bundle-size-review/manifest.json +1 -1
  11. package/packs/frontend/skills/bundle-size-review/scripts/bundle_stats.py +13 -1
  12. package/packs/frontend/skills/web-performance-review/manifest.json +1 -1
  13. package/packs/frontend/skills/web-performance-review/scripts/lh_summary.py +12 -2
  14. package/packs/grc/skills/risk-register/manifest.json +1 -1
  15. package/packs/grc/skills/risk-register/scripts/check_register.py +9 -1
  16. package/packs/infra-as-code/skills/terraform-plan-review/manifest.json +1 -1
  17. package/packs/infra-as-code/skills/terraform-plan-review/scripts/plan_summary.py +12 -2
  18. package/packs/product-manager/skills/feedback-synthesis/manifest.json +1 -1
  19. package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +6 -1
  20. package/packs/research/skills/hypothesis-comparison/manifest.json +1 -1
  21. package/packs/research/skills/hypothesis-comparison/scripts/ach_matrix.py +10 -2
  22. package/packs/solution-architect/skills/context-graph/SKILL.md +119 -0
  23. package/packs/solution-architect/skills/context-graph/evals/eval_queries.json +11 -0
  24. package/packs/solution-architect/skills/context-graph/evals/evals.json +225 -0
  25. package/packs/solution-architect/skills/context-graph/evals/files/1/docs/adr/0001-record-architecture-decisions.md +19 -0
  26. package/packs/solution-architect/skills/context-graph/evals/files/1/docs/adr/0002-postgres-as-primary-database.md +16 -0
  27. package/packs/solution-architect/skills/context-graph/evals/files/1/docs/adr/0003-use-redis-for-session-storage.md +24 -0
  28. package/packs/solution-architect/skills/context-graph/evals/files/1/docs/adr/0004-structured-json-logging.md +15 -0
  29. package/packs/solution-architect/skills/context-graph/evals/files/1/docs/adr/0005-session-lifetime-eight-hours.md +22 -0
  30. package/packs/solution-architect/skills/context-graph/evals/files/1/docs/adr/0006-move-sessions-to-postgres.md +16 -0
  31. package/packs/solution-architect/skills/context-graph/evals/files/1/docs/adr/0007-stateless-signed-session-cookies.md +35 -0
  32. package/packs/solution-architect/skills/context-graph/evals/files/1/services/api/__init__.py +0 -0
  33. package/packs/solution-architect/skills/context-graph/evals/files/1/services/api/db.py +0 -0
  34. package/packs/solution-architect/skills/context-graph/evals/files/1/services/api/logging_setup.py +0 -0
  35. package/packs/solution-architect/skills/context-graph/evals/files/1/services/auth/__init__.py +0 -0
  36. package/packs/solution-architect/skills/context-graph/evals/files/1/services/auth/cookie_sessions.py +7 -0
  37. package/packs/solution-architect/skills/context-graph/evals/files/1/services/auth/redis_sessions.py +7 -0
  38. package/packs/solution-architect/skills/context-graph/evals/files/10/README.md +8 -0
  39. package/packs/solution-architect/skills/context-graph/evals/files/10/docs/adr/0001-record-architecture-decisions.md +10 -0
  40. package/packs/solution-architect/skills/context-graph/evals/files/10/docs/adr/0004-service-auth-hmac-shared-keys.md +21 -0
  41. package/packs/solution-architect/skills/context-graph/evals/files/10/docs/adr/0007-secrets-from-vault-agent.md +19 -0
  42. package/packs/solution-architect/skills/context-graph/evals/files/10/docs/adr/0011-service-auth-mtls.md +23 -0
  43. package/packs/solution-architect/skills/context-graph/evals/files/10/services/billing-api/app.py +16 -0
  44. package/packs/solution-architect/skills/context-graph/evals/files/10/services/billing-api/auth/__init__.py +4 -0
  45. package/packs/solution-architect/skills/context-graph/evals/files/10/services/billing-api/auth/hmac_verify.py +31 -0
  46. package/packs/solution-architect/skills/context-graph/evals/files/10/services/billing-api/config/settings.py +14 -0
  47. package/packs/solution-architect/skills/context-graph/evals/files/10/services/ledger-worker/auth/__init__.py +4 -0
  48. package/packs/solution-architect/skills/context-graph/evals/files/10/services/ledger-worker/auth/mtls.py +18 -0
  49. package/packs/solution-architect/skills/context-graph/evals/files/11/README.md +4 -0
  50. package/packs/solution-architect/skills/context-graph/evals/files/11/app/models/base.py +3 -0
  51. package/packs/solution-architect/skills/context-graph/evals/files/11/app/models/line_item.py +14 -0
  52. package/packs/solution-architect/skills/context-graph/evals/files/11/app/models/order.py +24 -0
  53. package/packs/solution-architect/skills/context-graph/evals/files/11/eng/decisions/DR-003-money-as-minor-units.md +10 -0
  54. package/packs/solution-architect/skills/context-graph/evals/files/11/eng/decisions/DR-005-orders-typed-columns-only.md +10 -0
  55. package/packs/solution-architect/skills/context-graph/evals/files/11/eng/decisions/DR-009-line-items-immutable.md +11 -0
  56. package/packs/solution-architect/skills/context-graph/evals/files/11/eng/decisions/DR-014-optional-order-attributes-json.md +16 -0
  57. package/packs/solution-architect/skills/context-graph/evals/files/11/eng/decisions/DR-016-order-attributes-table.md +11 -0
  58. package/packs/solution-architect/skills/context-graph/evals/files/11/migrations/0007_create_orders.sql +11 -0
  59. package/packs/solution-architect/skills/context-graph/evals/files/11/migrations/0012_add_order_attributes.sql +2 -0
  60. package/packs/solution-architect/skills/context-graph/evals/files/12/README.md +8 -0
  61. package/packs/solution-architect/skills/context-graph/evals/files/12/config/delivery.yaml +4 -0
  62. package/packs/solution-architect/skills/context-graph/evals/files/12/config/export.yaml +2 -0
  63. package/packs/solution-architect/skills/context-graph/evals/files/12/config/partners.yaml +10 -0
  64. package/packs/solution-architect/skills/context-graph/evals/files/12/docs/adr/0001-record-architecture-decisions.md +10 -0
  65. package/packs/solution-architect/skills/context-graph/evals/files/12/docs/adr/0002-postgres-delivery-log.md +15 -0
  66. package/packs/solution-architect/skills/context-graph/evals/files/12/docs/adr/0003-redis-idempotency-keys.md +15 -0
  67. package/packs/solution-architect/skills/context-graph/evals/files/12/docs/adr/0004-retry-policy.md +16 -0
  68. package/packs/solution-architect/skills/context-graph/evals/files/12/docs/adr/0005-hmac-signed-payloads.md +10 -0
  69. package/packs/solution-architect/skills/context-graph/evals/files/12/docs/adr/0006-postgres-idempotency.md +15 -0
  70. package/packs/solution-architect/skills/context-graph/evals/files/12/docs/adr/0007-sftp-batch-export.md +12 -0
  71. package/packs/solution-architect/skills/context-graph/evals/files/12/docs/adr/0008-partner-config-in-yaml.md +12 -0
  72. package/packs/solution-architect/skills/context-graph/evals/files/12/docs/adr/0009-partner-config-admin-api.md +13 -0
  73. package/packs/solution-architect/skills/context-graph/evals/files/12/migrations/003_idempotency_keys.sql +6 -0
  74. package/packs/solution-architect/skills/context-graph/evals/files/12/migrations/004_partners.sql +7 -0
  75. package/packs/solution-architect/skills/context-graph/evals/files/12/src/delivery/dispatcher.py +29 -0
  76. package/packs/solution-architect/skills/context-graph/evals/files/12/src/delivery/retry.py +15 -0
  77. package/packs/solution-architect/skills/context-graph/evals/files/12/src/delivery/signing.py +13 -0
  78. package/packs/solution-architect/skills/context-graph/evals/files/12/src/export/s3_export.py +24 -0
  79. package/packs/solution-architect/skills/context-graph/evals/files/12/src/idempotency/pg_store.py +11 -0
  80. package/packs/solution-architect/skills/context-graph/evals/files/12/src/idempotency/redis_store.py +8 -0
  81. package/packs/solution-architect/skills/context-graph/evals/files/12/src/partners/loader.py +15 -0
  82. package/packs/solution-architect/skills/context-graph/evals/files/12/src/store/delivery_log.py +18 -0
  83. package/packs/solution-architect/skills/context-graph/evals/files/13/README.md +3 -0
  84. package/packs/solution-architect/skills/context-graph/evals/files/13/config/cache.yaml +2 -0
  85. package/packs/solution-architect/skills/context-graph/evals/files/13/config/settings.yaml +2 -0
  86. package/packs/solution-architect/skills/context-graph/evals/files/13/docs/decisions/RFC-008-pricing-from-erp.md +10 -0
  87. package/packs/solution-architect/skills/context-graph/evals/files/13/docs/decisions/RFC-012-pricing-cache.md +23 -0
  88. package/packs/solution-architect/skills/context-graph/evals/files/13/docs/decisions/RFC-015-checkout-price-lock.md +15 -0
  89. package/packs/solution-architect/skills/context-graph/evals/files/13/docs/decisions/RFC-019-pricing-service.md +17 -0
  90. package/packs/solution-architect/skills/context-graph/evals/files/13/shopfront/catalog/views.py +8 -0
  91. package/packs/solution-architect/skills/context-graph/evals/files/13/shopfront/checkout/lock.py +14 -0
  92. package/packs/solution-architect/skills/context-graph/evals/files/13/shopfront/pricing/cache.py +22 -0
  93. package/packs/solution-architect/skills/context-graph/evals/files/13/shopfront/pricing/client.py +10 -0
  94. package/packs/solution-architect/skills/context-graph/evals/files/14/docs/rfcs/0001-idempotency-keys.md +33 -0
  95. package/packs/solution-architect/skills/context-graph/evals/files/14/docs/rfcs/0002-processor-retry-policy.md +26 -0
  96. package/packs/solution-architect/skills/context-graph/evals/files/14/docs/rfcs/0003-money-in-minor-units.md +31 -0
  97. package/packs/solution-architect/skills/context-graph/evals/files/14/docs/rfcs/0004-webhook-signing.md +27 -0
  98. package/packs/solution-architect/skills/context-graph/evals/files/14/docs/rfcs/0005-retry-with-jitter.md +30 -0
  99. package/packs/solution-architect/skills/context-graph/evals/files/14/docs/rfcs/0006-idempotency-at-gateway.md +28 -0
  100. package/packs/solution-architect/skills/context-graph/evals/files/14/pr-summary.md +27 -0
  101. package/packs/solution-architect/skills/context-graph/evals/files/14/src/payments/amount.ts +10 -0
  102. package/packs/solution-architect/skills/context-graph/evals/files/14/src/payments/charge.ts +28 -0
  103. package/packs/solution-architect/skills/context-graph/evals/files/14/src/payments/idempotency.ts +4 -0
  104. package/packs/solution-architect/skills/context-graph/evals/files/14/src/payments/retry.ts +21 -0
  105. package/packs/solution-architect/skills/context-graph/evals/files/14/src/webhooks/send.ts +9 -0
  106. package/packs/solution-architect/skills/context-graph/evals/files/14/src/webhooks/sign.ts +6 -0
  107. package/packs/solution-architect/skills/context-graph/evals/files/15/deploy/services.yaml +14 -0
  108. package/packs/solution-architect/skills/context-graph/evals/files/15/doc/adr/0001-record-architecture-decisions.md +20 -0
  109. package/packs/solution-architect/skills/context-graph/evals/files/15/doc/adr/0002-use-postgresql-for-shipment-data.md +23 -0
  110. package/packs/solution-architect/skills/context-graph/evals/files/15/doc/adr/0003-store-vehicle-telemetry-in-cassandra.md +22 -0
  111. package/packs/solution-architect/skills/context-graph/evals/files/15/doc/adr/0004-use-redis-for-session-storage.md +22 -0
  112. package/packs/solution-architect/skills/context-graph/evals/files/15/doc/adr/0005-use-react-for-dispatcher-dashboard.md +19 -0
  113. package/packs/solution-architect/skills/context-graph/evals/files/15/doc/adr/0006-archive-delivered-shipments-to-object-storage.md +23 -0
  114. package/packs/solution-architect/skills/context-graph/evals/files/15/doc/adr/0007-move-shipment-records-to-mongodb.md +23 -0
  115. package/packs/solution-architect/skills/context-graph/evals/files/15/doc/adr/0008-store-vehicle-telemetry-in-timescaledb.md +28 -0
  116. package/packs/solution-architect/skills/context-graph/evals/files/15/doc/adr/0009-stateless-sessions-with-signed-cookies.md +24 -0
  117. package/packs/solution-architect/skills/context-graph/evals/files/15/services/api/session.py +17 -0
  118. package/packs/solution-architect/skills/context-graph/evals/files/15/services/shipments/db.py +30 -0
  119. package/packs/solution-architect/skills/context-graph/evals/files/15/services/shipments/migrations.sql +3 -0
  120. package/packs/solution-architect/skills/context-graph/evals/files/15/services/telemetry/store.py +23 -0
  121. package/packs/solution-architect/skills/context-graph/evals/files/15/web/dashboard/App.jsx +3 -0
  122. package/packs/solution-architect/skills/context-graph/evals/files/2/docs/adr/0001-record-architecture-decisions.md +11 -0
  123. package/packs/solution-architect/skills/context-graph/evals/files/2/docs/adr/0002-rabbitmq-for-background-jobs.md +15 -0
  124. package/packs/solution-architect/skills/context-graph/evals/files/2/docs/adr/0003-thumbnail-service.md +20 -0
  125. package/packs/solution-architect/skills/context-graph/evals/files/2/docs/adr/0004-money-as-integer-minor-units.md +17 -0
  126. package/packs/solution-architect/skills/context-graph/evals/files/2/docs/adr/0005-worker-retries.md +12 -0
  127. package/packs/solution-architect/skills/context-graph/evals/files/2/docs/adr/0006-fx-amounts-with-four-decimals.md +18 -0
  128. package/packs/solution-architect/skills/context-graph/evals/files/2/docs/adr/0007-feature-flags.md +16 -0
  129. package/packs/solution-architect/skills/context-graph/evals/files/2/docs/adr/0008-retire-legacy-search.md +14 -0
  130. package/packs/solution-architect/skills/context-graph/evals/files/2/libs/money/__init__.py +0 -0
  131. package/packs/solution-architect/skills/context-graph/evals/files/2/libs/money/amount.py +9 -0
  132. package/packs/solution-architect/skills/context-graph/evals/files/2/services/checkout/flags.py +1 -0
  133. package/packs/solution-architect/skills/context-graph/evals/files/2/services/checkout/publish.py +0 -0
  134. package/packs/solution-architect/skills/context-graph/evals/files/2/services/media/resize.py +5 -0
  135. package/packs/solution-architect/skills/context-graph/evals/files/2/services/worker/consumer.py +0 -0
  136. package/packs/solution-architect/skills/context-graph/evals/files/3/docs/rfcs/0001-how-we-write-rfcs.md +9 -0
  137. package/packs/solution-architect/skills/context-graph/evals/files/3/docs/rfcs/0002-payments-service-boundary.md +16 -0
  138. package/packs/solution-architect/skills/context-graph/evals/files/3/docs/rfcs/0003-webhook-retries-fixed-interval.md +19 -0
  139. package/packs/solution-architect/skills/context-graph/evals/files/3/docs/rfcs/0004-webhook-idempotency-keys.md +16 -0
  140. package/packs/solution-architect/skills/context-graph/evals/files/3/docs/rfcs/0005-notification-retries.md +16 -0
  141. package/packs/solution-architect/skills/context-graph/evals/files/3/docs/rfcs/0006-webhook-retry-backoff.md +26 -0
  142. package/packs/solution-architect/skills/context-graph/evals/files/3/docs/rfcs/0007-webhooks-on-the-job-queue.md +12 -0
  143. package/packs/solution-architect/skills/context-graph/evals/files/3/services/notifications/__init__.py +0 -0
  144. package/packs/solution-architect/skills/context-graph/evals/files/3/services/notifications/retry.py +4 -0
  145. package/packs/solution-architect/skills/context-graph/evals/files/3/services/payments/__init__.py +0 -0
  146. package/packs/solution-architect/skills/context-graph/evals/files/3/services/payments/webhooks/__init__.py +0 -0
  147. package/packs/solution-architect/skills/context-graph/evals/files/3/services/payments/webhooks/retry.py +15 -0
  148. package/packs/solution-architect/skills/context-graph/evals/files/3/services/payments/webhooks/send.py +0 -0
  149. package/packs/solution-architect/skills/context-graph/evals/files/4/docs/adr/0001-record-architecture-decisions.md +19 -0
  150. package/packs/solution-architect/skills/context-graph/evals/files/4/docs/adr/0002-postgres-as-system-of-record.md +18 -0
  151. package/packs/solution-architect/skills/context-graph/evals/files/4/docs/adr/0004-redis-locks-for-scheduler.md +19 -0
  152. package/packs/solution-architect/skills/context-graph/evals/files/4/docs/adr/0007-remove-redis.md +27 -0
  153. package/packs/solution-architect/skills/context-graph/evals/files/4/docs/adr/0009-money-as-integer-minor-units.md +19 -0
  154. package/packs/solution-architect/skills/context-graph/evals/files/4/docs/onboarding.md +12 -0
  155. package/packs/solution-architect/skills/context-graph/evals/files/4/src/ledgerline/__init__.py +0 -0
  156. package/packs/solution-architect/skills/context-graph/evals/files/4/src/ledgerline/db/__init__.py +22 -0
  157. package/packs/solution-architect/skills/context-graph/evals/files/4/src/ledgerline/invoicing.py +9 -0
  158. package/packs/solution-architect/skills/context-graph/evals/files/4/src/ledgerline/money.py +10 -0
  159. package/packs/solution-architect/skills/context-graph/evals/files/4/src/ledgerline/scheduler/__init__.py +0 -0
  160. package/packs/solution-architect/skills/context-graph/evals/files/4/src/ledgerline/scheduler/lock.py +23 -0
  161. package/packs/solution-architect/skills/context-graph/evals/files/4/src/ledgerline/scheduler/runner.py +17 -0
  162. package/packs/solution-architect/skills/context-graph/evals/files/5/lib/types/index.ts +13 -0
  163. package/packs/solution-architect/skills/context-graph/evals/files/5/rfcs/0001-service-layout.md +20 -0
  164. package/packs/solution-architect/skills/context-graph/evals/files/5/rfcs/0003-ingest-retry-policy.md +26 -0
  165. package/packs/solution-architect/skills/context-graph/evals/files/5/rfcs/0006-shared-retry-library.md +24 -0
  166. package/packs/solution-architect/skills/context-graph/evals/files/5/rfcs/0008-outbound-http-client.md +22 -0
  167. package/packs/solution-architect/skills/context-graph/evals/files/5/services/export/client.ts +15 -0
  168. package/packs/solution-architect/skills/context-graph/evals/files/5/services/export/retry.ts +12 -0
  169. package/packs/solution-architect/skills/context-graph/evals/files/5/services/ingest/client.ts +14 -0
  170. package/packs/solution-architect/skills/context-graph/evals/files/5/services/ingest/retry.ts +20 -0
  171. package/packs/solution-architect/skills/context-graph/evals/files/6/README.md +4 -0
  172. package/packs/solution-architect/skills/context-graph/evals/files/6/docs/adr/0001-monorepo-layout.md +10 -0
  173. package/packs/solution-architect/skills/context-graph/evals/files/6/docs/adr/0002-billing-on-postgres.md +13 -0
  174. package/packs/solution-architect/skills/context-graph/evals/files/6/docs/adr/0004-ledger-sync-nightly-batch.md +9 -0
  175. package/packs/solution-architect/skills/context-graph/evals/files/6/docs/rfcs/RFC-0003-invoice-ids.md +9 -0
  176. package/packs/solution-architect/skills/context-graph/evals/files/6/docs/rfcs/RFC-0007-invoice-ids-ulid.md +11 -0
  177. package/packs/solution-architect/skills/context-graph/evals/files/6/docs/rfcs/RFC-0009-billing-event-store.md +15 -0
  178. package/packs/solution-architect/skills/context-graph/evals/files/6/docs/rfcs/RFC-0010-revenue-service.md +13 -0
  179. package/packs/solution-architect/skills/context-graph/evals/files/6/libs/money/ulid.py +8 -0
  180. package/packs/solution-architect/skills/context-graph/evals/files/6/services/billing/src/events.py +10 -0
  181. package/packs/solution-architect/skills/context-graph/evals/files/6/services/billing/src/store.py +10 -0
  182. package/packs/solution-architect/skills/context-graph/evals/files/6/services/invoicing/ids/generator.py +7 -0
  183. package/packs/solution-architect/skills/context-graph/evals/files/6/services/ledger/README.md +4 -0
  184. package/packs/solution-architect/skills/context-graph/evals/files/6/services/ledger/sync.py +3 -0
  185. package/packs/solution-architect/skills/context-graph/evals/files/7/README.md +4 -0
  186. package/packs/solution-architect/skills/context-graph/evals/files/7/db/queries/orders.sql +2 -0
  187. package/packs/solution-architect/skills/context-graph/evals/files/7/db/sharding/router.py +9 -0
  188. package/packs/solution-architect/skills/context-graph/evals/files/7/design/decisions/DD-001-postgres-primary.md +8 -0
  189. package/packs/solution-architect/skills/context-graph/evals/files/7/design/decisions/DD-002-kafka-event-bus.md +20 -0
  190. package/packs/solution-architect/skills/context-graph/evals/files/7/design/decisions/DD-003-no-orm.md +9 -0
  191. package/packs/solution-architect/skills/context-graph/evals/files/7/design/decisions/DD-006-tenant-sharding.md +9 -0
  192. package/packs/solution-architect/skills/context-graph/evals/files/7/design/decisions/DD-011-orm-for-admin-console.md +11 -0
  193. package/packs/solution-architect/skills/context-graph/evals/files/7/pipelines/fanout/router.py +4 -0
  194. package/packs/solution-architect/skills/context-graph/evals/files/7/pipelines/ingest/consumer.py +5 -0
  195. package/packs/solution-architect/skills/context-graph/evals/files/7/pipelines/ingest/topics.yaml +8 -0
  196. package/packs/solution-architect/skills/context-graph/evals/files/7/rfcs/0004-region-sharding.md +15 -0
  197. package/packs/solution-architect/skills/context-graph/evals/files/7/services/admin-console/models.py +11 -0
  198. package/packs/solution-architect/skills/context-graph/evals/files/7/services/orders/repo.py +9 -0
  199. package/packs/solution-architect/skills/context-graph/evals/files/8/README.md +11 -0
  200. package/packs/solution-architect/skills/context-graph/evals/files/8/api/ledger.proto +30 -0
  201. package/packs/solution-architect/skills/context-graph/evals/files/8/config/http.yaml +10 -0
  202. package/packs/solution-architect/skills/context-graph/evals/files/8/config/messaging.yaml +8 -0
  203. package/packs/solution-architect/skills/context-graph/evals/files/8/docs/adr/0001-record-architecture-decisions.md +20 -0
  204. package/packs/solution-architect/skills/context-graph/evals/files/8/docs/adr/0002-postgresql-for-ledger.md +22 -0
  205. package/packs/solution-architect/skills/context-graph/evals/files/8/docs/adr/0003-rabbitmq-domain-events.md +22 -0
  206. package/packs/solution-architect/skills/context-graph/evals/files/8/docs/adr/0004-integer-minor-units.md +32 -0
  207. package/packs/solution-architect/skills/context-graph/evals/files/8/docs/adr/0005-provider-http-retries.md +26 -0
  208. package/packs/solution-architect/skills/context-graph/evals/files/8/docs/adr/0006-openapi-generated-clients.md +20 -0
  209. package/packs/solution-architect/skills/context-graph/evals/files/8/docs/adr/0007-kafka-domain-events.md +25 -0
  210. package/packs/solution-architect/skills/context-graph/evals/files/8/docs/adr/0008-structured-logging.md +18 -0
  211. package/packs/solution-architect/skills/context-graph/evals/files/8/docs/rfcs/RFC-012-idempotency-keys.md +19 -0
  212. package/packs/solution-architect/skills/context-graph/evals/files/8/services/ledger/api/idempotency.py +19 -0
  213. package/packs/solution-architect/skills/context-graph/evals/files/8/services/ledger/api/server.py +35 -0
  214. package/packs/solution-architect/skills/context-graph/evals/files/8/services/ledger/clients/provider.py +33 -0
  215. package/packs/solution-architect/skills/context-graph/evals/files/8/services/ledger/db/migrations/0001_init.sql +14 -0
  216. package/packs/solution-architect/skills/context-graph/evals/files/8/services/ledger/db/migrations/0008_idempotency_keys.sql +5 -0
  217. package/packs/solution-architect/skills/context-graph/evals/files/8/services/ledger/db/migrations/0009_fx_entries.sql +4 -0
  218. package/packs/solution-architect/skills/context-graph/evals/files/8/services/ledger/events/publisher.py +28 -0
  219. package/packs/solution-architect/skills/context-graph/evals/files/8/services/ledger/logging.py +25 -0
  220. package/packs/solution-architect/skills/context-graph/evals/files/8/services/ledger/models/entry.py +16 -0
  221. package/packs/solution-architect/skills/context-graph/evals/files/9/.env.example +3 -0
  222. package/packs/solution-architect/skills/context-graph/evals/files/9/README.md +5 -0
  223. package/packs/solution-architect/skills/context-graph/evals/files/9/docker-compose.yml +18 -0
  224. package/packs/solution-architect/skills/context-graph/evals/files/9/docs/decisions/001-session-storage.md +21 -0
  225. package/packs/solution-architect/skills/context-graph/evals/files/9/docs/decisions/002-api-rate-limiting.md +32 -0
  226. package/packs/solution-architect/skills/context-graph/evals/files/9/docs/decisions/003-jwt-sessions.md +29 -0
  227. package/packs/solution-architect/skills/context-graph/evals/files/9/docs/decisions/004-catalog-cache.md +19 -0
  228. package/packs/solution-architect/skills/context-graph/evals/files/9/src/catalog/cache.ts +13 -0
  229. package/packs/solution-architect/skills/context-graph/evals/files/9/src/catalog/repository.ts +13 -0
  230. package/packs/solution-architect/skills/context-graph/evals/files/9/src/jobs/email.ts +3 -0
  231. package/packs/solution-architect/skills/context-graph/evals/files/9/src/jobs/export.ts +3 -0
  232. package/packs/solution-architect/skills/context-graph/evals/files/9/src/jobs/queue.ts +10 -0
  233. package/packs/solution-architect/skills/context-graph/evals/files/9/src/jobs/worker.ts +8 -0
  234. package/packs/solution-architect/skills/context-graph/evals/files/9/src/lib/db.ts +10 -0
  235. package/packs/solution-architect/skills/context-graph/evals/files/9/src/lib/redis.ts +8 -0
  236. package/packs/solution-architect/skills/context-graph/evals/files/9/src/middleware/rateLimit.ts +26 -0
  237. package/packs/solution-architect/skills/context-graph/evals/files/9/src/server.ts +17 -0
  238. package/packs/solution-architect/skills/context-graph/evals/files/9/src/session/store.ts +10 -0
  239. package/packs/solution-architect/skills/context-graph/examples/inputs/order-events/docs/adr/0004-kafka-for-order-events.md +19 -0
  240. package/packs/solution-architect/skills/context-graph/examples/inputs/order-events/docs/adr/0006-email-receipts.md +7 -0
  241. package/packs/solution-architect/skills/context-graph/examples/inputs/order-events/docs/adr/0009-sqs-for-order-events.md +19 -0
  242. package/packs/solution-architect/skills/context-graph/examples/inputs/order-events/services/notifications/receipts.py +0 -0
  243. package/packs/solution-architect/skills/context-graph/examples/inputs/order-events/services/orders/publisher.py +0 -0
  244. package/packs/solution-architect/skills/context-graph/examples/order-events.md +63 -0
  245. package/packs/solution-architect/skills/context-graph/manifest.json +33 -0
  246. package/packs/solution-architect/skills/context-graph/reference.md +104 -0
  247. package/packs/solution-architect/skills/context-graph/scripts/context_graph.py +634 -0
  248. package/packs/solution-architect/skills/event-contract-draft/manifest.json +1 -1
  249. package/packs/solution-architect/skills/event-contract-draft/scripts/check_asyncapi.py +14 -2
  250. package/packs/sre-oncall/skills/cloud-cost-review/manifest.json +1 -1
  251. package/packs/sre-oncall/skills/cloud-cost-review/scripts/cost_summary.py +2 -0
  252. package/packs/trackers/skills/backlog-triage/manifest.json +1 -1
  253. package/packs/trackers/skills/backlog-triage/scripts/triage_backlog.py +4 -1
  254. package/packs/trackers/skills/team-status-report/manifest.json +1 -1
  255. package/packs/trackers/skills/team-status-report/scripts/status_counts.py +4 -1
@@ -0,0 +1,18 @@
1
+ """Check the peer certificate's SPIFFE ID on internal requests (see ADR-0011)."""
2
+ from functools import wraps
3
+
4
+ ALLOWED_PEERS = {
5
+ "spiffe://tallowmere.internal/billing-api",
6
+ }
7
+ CA_BUNDLE = "/etc/ironbark/ca-bundle.pem"
8
+
9
+
10
+ def require_peer_identity(handler):
11
+ @wraps(handler)
12
+ def wrapper(event, peer_cert, *args, **kwargs):
13
+ spiffe_id = peer_cert.get("uri_san", "")
14
+ if spiffe_id not in ALLOWED_PEERS:
15
+ raise PermissionError(f"peer {spiffe_id!r} not allowed")
16
+ return handler(event, peer_cert, *args, **kwargs)
17
+
18
+ return wrapper
@@ -0,0 +1,4 @@
1
+ # Pebblecart
2
+
3
+ Small storefront backend. Models in `app/models/`, SQL migrations in `migrations/`.
4
+ Design decisions are kept in `eng/decisions/` (DR-NNN).
@@ -0,0 +1,3 @@
1
+ from sqlalchemy.orm import declarative_base
2
+
3
+ Base = declarative_base()
@@ -0,0 +1,14 @@
1
+ """Line item model. Immutable once the parent order is placed (DR-009)."""
2
+ from sqlalchemy import BigInteger, Column, ForeignKey, Integer, String
3
+
4
+ from .base import Base
5
+
6
+
7
+ class LineItem(Base):
8
+ __tablename__ = "line_items"
9
+
10
+ id = Column(BigInteger, primary_key=True)
11
+ order_id = Column(BigInteger, ForeignKey("orders.id"), nullable=False, index=True)
12
+ sku = Column(String(32), nullable=False)
13
+ quantity = Column(Integer, nullable=False)
14
+ unit_price_minor = Column(Integer, nullable=False)
@@ -0,0 +1,24 @@
1
+ """Order model."""
2
+ from sqlalchemy import JSON, BigInteger, Column, DateTime, Integer, String
3
+
4
+ from .base import Base
5
+
6
+ # Keys allowed in Order.attributes (DR-014). Review additions like a column.
7
+ ORDER_ATTRIBUTE_KEYS = {
8
+ "delivery_note",
9
+ "leave_with_neighbour",
10
+ }
11
+
12
+
13
+ class Order(Base):
14
+ __tablename__ = "orders"
15
+
16
+ id = Column(BigInteger, primary_key=True)
17
+ customer_id = Column(BigInteger, nullable=False, index=True)
18
+ status = Column(String(16), nullable=False, default="draft")
19
+ currency = Column(String(3), nullable=False)
20
+ subtotal_minor = Column(Integer, nullable=False)
21
+ shipping_minor = Column(Integer, nullable=False, default=0)
22
+ total_minor = Column(Integer, nullable=False)
23
+ placed_at = Column(DateTime(timezone=True))
24
+ attributes = Column(JSON, nullable=False, default=dict)
@@ -0,0 +1,10 @@
1
+ ---
2
+ id: DR-003
3
+ title: Store money as integer minor units with a currency code
4
+ status: Accepted
5
+ date: 2022-02-17
6
+ governs: [app/models/order.py, app/models/line_item.py]
7
+ ---
8
+
9
+ All monetary amounts are stored as integer minor units (`*_minor`, e.g. pence) alongside a
10
+ three-letter `currency` column. No floats or decimals for money anywhere in the schema.
@@ -0,0 +1,10 @@
1
+ ---
2
+ id: DR-005
3
+ title: Orders table uses typed columns only
4
+ status: Accepted
5
+ date: 2022-04-11
6
+ governs: [app/models/order.py, migrations/]
7
+ ---
8
+
9
+ Every field on an order is a typed, named column with a migration. We do not store JSON blobs
10
+ on the orders table: they hide schema changes from review and cannot be indexed.
@@ -0,0 +1,11 @@
1
+ ---
2
+ id: DR-009
3
+ title: Order line items are immutable once the order is placed
4
+ status: Accepted
5
+ date: 2023-01-30
6
+ governs: [app/models/line_item.py]
7
+ cites: [DR-003]
8
+ ---
9
+
10
+ After an order moves to `placed`, its line items are never updated. Price or quantity
11
+ corrections are written as rows in `order_adjustments`, amounts in minor units per DR-003.
@@ -0,0 +1,16 @@
1
+ ---
2
+ id: DR-014
3
+ title: Optional customer-supplied order fields go in orders.attributes
4
+ status: Accepted
5
+ date: 2024-02-08
6
+ amends: DR-005
7
+ governs: [app/models/order.py, migrations/0012_add_order_attributes.sql]
8
+ ---
9
+
10
+ DR-005's typed-columns rule produced a migration for every small checkout tweak. We relax it:
11
+
12
+ - New **optional, customer-supplied** fields that are not filtered, joined or reported on
13
+ (delivery notes, gift options, and similar) are stored as keys in the `orders.attributes`
14
+ JSON column. Each key is listed in `ORDER_ATTRIBUTE_KEYS` in `app/models/order.py` and is
15
+ reviewed like a column.
16
+ - Anything we filter, join, index or report on stays a typed column, as DR-005 says.
@@ -0,0 +1,11 @@
1
+ ---
2
+ id: DR-016
3
+ title: Move order attributes into a separate order_attributes table
4
+ status: Proposed
5
+ date: 2026-09-30
6
+ cites: [DR-014]
7
+ ---
8
+
9
+ Proposal (under discussion, not agreed): replace the `orders.attributes` JSON column from
10
+ DR-014 with an `order_attributes(order_id, key, value)` table so attributes can be indexed.
11
+ Open question: whether existing keys are back-filled or dual-written.
@@ -0,0 +1,11 @@
1
+ CREATE TABLE orders (
2
+ id BIGINT PRIMARY KEY,
3
+ customer_id BIGINT NOT NULL,
4
+ status VARCHAR(16) NOT NULL DEFAULT 'draft',
5
+ currency CHAR(3) NOT NULL,
6
+ subtotal_minor INTEGER NOT NULL,
7
+ shipping_minor INTEGER NOT NULL DEFAULT 0,
8
+ total_minor INTEGER NOT NULL,
9
+ placed_at TIMESTAMPTZ
10
+ );
11
+ CREATE INDEX orders_customer_id_idx ON orders (customer_id);
@@ -0,0 +1,2 @@
1
+ -- DR-014: optional customer-supplied order fields.
2
+ ALTER TABLE orders ADD COLUMN attributes JSONB NOT NULL DEFAULT '{}'::jsonb;
@@ -0,0 +1,8 @@
1
+ # parcelhook
2
+
3
+ Sends shipment-status webhooks (picked_up, in_transit, delivered, exception) from the
4
+ Corvane Freight tracking platform to partner endpoints. Owned by Platform Integrations.
5
+
6
+ - Entry point: `src/delivery/dispatcher.py`
7
+ - Decisions: `docs/adr/`
8
+ - Config: `config/`
@@ -0,0 +1,4 @@
1
+ retry:
2
+ max_attempts: 20
3
+ base_delay_seconds: 10
4
+ timeout_seconds: 10
@@ -0,0 +1,2 @@
1
+ schedule: "0 2 * * *"
2
+ bucket: corvane-parcelhook-exports
@@ -0,0 +1,10 @@
1
+ # Partner configuration (ADR-0008). Loaded at startup.
2
+ partners:
3
+ - id: velo
4
+ url: https://hooks.velo.example/parcel
5
+ events: [picked_up, delivered, exception]
6
+ secret_ref: vault://parcelhook/partners/velo
7
+ - id: northwind-retail
8
+ url: https://api.northwind.example/webhooks/shipments
9
+ events: [delivered]
10
+ secret_ref: vault://parcelhook/partners/northwind-retail
@@ -0,0 +1,10 @@
1
+ # ADR-0001: Record architecture decisions
2
+
3
+ - Status: Accepted
4
+ - Date: 2022-03-01
5
+ - Deciders: Platform Integrations
6
+
7
+ ## Decision
8
+ We record significant decisions about parcelhook as numbered ADRs in `docs/adr/`.
9
+ A record that replaces another says "Supersedes ADR-NNNN", and the replaced record's
10
+ status is changed to "Superseded by ADR-MMMM".
@@ -0,0 +1,15 @@
1
+ # ADR-0002: Postgres for the delivery log
2
+
3
+ - Status: Accepted
4
+ - Date: 2022-03-10
5
+ - Governs: `src/store/`
6
+
7
+ ## Context
8
+ Partners dispute whether a webhook was sent. We need a durable, queryable record of every attempt.
9
+
10
+ ## Decision
11
+ Every delivery attempt is written to the `delivery_attempts` table in the service's Postgres
12
+ database (event_id, partner_id, attempt_no, status_code, sent_at).
13
+
14
+ ## Consequences
15
+ Support can answer "did we send it?" with a query. The table grows ~2M rows/month; partition by month.
@@ -0,0 +1,15 @@
1
+ # ADR-0003: Idempotency keys in Redis
2
+
3
+ - Status: Accepted
4
+ - Date: 2022-05-02
5
+ - Governs: `src/idempotency/redis_store.py`
6
+
7
+ ## Context
8
+ The tracking platform delivers events at-least-once. Partners complained about duplicate webhooks.
9
+
10
+ ## Decision
11
+ Before dispatching, the dispatcher sets `idem:{event_id}:{partner_id}` in Redis with NX and a
12
+ 24h TTL. If the key already exists the event is dropped as a duplicate.
13
+
14
+ ## Consequences
15
+ Fast and simple. If Redis is flushed or evicts keys under memory pressure, duplicates can get through.
@@ -0,0 +1,16 @@
1
+ # ADR-0004: Retry policy for failed deliveries
2
+
3
+ - Status: Accepted
4
+ - Date: 2022-06-20
5
+ - Governs: `src/delivery/retry.py`, `config/delivery.yaml`
6
+
7
+ ## Context
8
+ Partner endpoints go down. Retrying too aggressively has had us rate-limited and blocked by two partners.
9
+
10
+ ## Decision
11
+ Retry failed deliveries with exponential backoff: delay = 30s * 2^(attempt-1), at most 6 attempts
12
+ (last retry roughly 16 minutes after the first). After the 6th failure, publish the event to the
13
+ dead-letter queue `parcelhook.dlq` for manual replay.
14
+
15
+ ## Consequences
16
+ A partner outage of more than ~30 minutes means events land in the DLQ and need replaying.
@@ -0,0 +1,10 @@
1
+ # ADR-0005: Sign payloads with HMAC-SHA256
2
+
3
+ - Status: Accepted
4
+ - Date: 2022-07-11
5
+ - Governs: `src/delivery/signing.py`
6
+
7
+ ## Decision
8
+ Each webhook body is signed with HMAC-SHA256 using the partner's shared secret. The hex digest
9
+ is sent in the `X-Parcelhook-Signature` header, together with `X-Parcelhook-Timestamp`, and the
10
+ timestamp is part of the signed string (`{timestamp}.{body}`) to prevent replay.
@@ -0,0 +1,15 @@
1
+ # ADR-0006: Move idempotency keys to Postgres
2
+
3
+ - Status: Accepted
4
+ - Date: 2023-09-04
5
+ - Supersedes: ADR-0003
6
+ - Governs: `src/idempotency/pg_store.py`, `migrations/003_idempotency_keys.sql`
7
+
8
+ ## Context
9
+ INC-1874 (2023-08-22): a Redis failover lost the idempotency keyspace and ~41,000 duplicate
10
+ webhooks went to partners in one hour.
11
+
12
+ ## Decision
13
+ Idempotency keys move to the `idempotency_keys` table in Postgres with a unique constraint on
14
+ (event_id, partner_id). The dispatcher inserts with ON CONFLICT DO NOTHING and only dispatches
15
+ if a row was inserted. Rows older than 7 days are purged nightly. Redis is no longer used for idempotency.
@@ -0,0 +1,12 @@
1
+ # ADR-0007: Nightly SFTP export of the delivery log
2
+
3
+ - Status: Accepted
4
+ - Date: 2023-02-15
5
+ - Governs: `src/export/sftp_export.py`
6
+
7
+ ## Context
8
+ Three partners reconcile against a daily file rather than our webhooks.
9
+
10
+ ## Decision
11
+ At 02:00 UTC, export the previous day's `delivery_attempts` rows per partner as CSV and push
12
+ them to each partner's SFTP server using `src/export/sftp_export.py`.
@@ -0,0 +1,12 @@
1
+ # ADR-0008: Partner configuration in a YAML file
2
+
3
+ - Status: Accepted
4
+ - Date: 2023-11-20
5
+ - Governs: `config/partners.yaml`, `src/partners/`
6
+
7
+ ## Decision
8
+ Partner endpoints, enabled event types and secret references live in `config/partners.yaml`,
9
+ reviewed by pull request. The service loads the file at startup.
10
+
11
+ ## Consequences
12
+ Onboarding a partner needs a deploy. Acceptable at our current 12 partners.
@@ -0,0 +1,13 @@
1
+ # ADR-0009: Partner configuration via an admin API
2
+
3
+ - Status: Accepted
4
+ - Date: 2024-05-13
5
+ - Governs: `src/partners/`, `migrations/004_partners.sql`
6
+
7
+ ## Context
8
+ We now have 60+ partners and onboarding through a YAML PR and a deploy takes days.
9
+
10
+ ## Decision
11
+ Partner configuration is stored in the `partners` table and edited through an internal admin API.
12
+ The service reloads partners from the table every 60 seconds. See ADR-0011 for the authentication
13
+ model of the admin API.
@@ -0,0 +1,6 @@
1
+ CREATE TABLE idempotency_keys (
2
+ event_id text NOT NULL,
3
+ partner_id text NOT NULL,
4
+ created_at timestamptz NOT NULL DEFAULT now(),
5
+ PRIMARY KEY (event_id, partner_id)
6
+ );
@@ -0,0 +1,7 @@
1
+ CREATE TABLE partners (
2
+ id text PRIMARY KEY,
3
+ url text NOT NULL,
4
+ events text[] NOT NULL,
5
+ secret_ref text NOT NULL,
6
+ enabled boolean NOT NULL DEFAULT true
7
+ );
@@ -0,0 +1,29 @@
1
+ """Consume tracking events and deliver them to partner endpoints."""
2
+ import json
3
+ import urllib.request
4
+
5
+ from src.idempotency import redis_store
6
+ from src.partners.loader import load_partners
7
+ from src.delivery.signing import sign
8
+ from src.delivery.retry import schedule_retry
9
+ from src.store.delivery_log import record_attempt
10
+
11
+
12
+ def dispatch(event, attempt_no=1):
13
+ for partner in load_partners():
14
+ if event["type"] not in partner["events"]:
15
+ continue
16
+ # drop duplicates from the at-least-once feed
17
+ if not redis_store.claim(event["id"], partner["id"]):
18
+ continue
19
+ body = json.dumps(event).encode()
20
+ headers = {"Content-Type": "application/json", **sign(partner, body)}
21
+ req = urllib.request.Request(partner["url"], data=body, headers=headers, method="POST")
22
+ try:
23
+ with urllib.request.urlopen(req, timeout=10) as resp:
24
+ status = resp.status
25
+ except Exception:
26
+ status = 0
27
+ record_attempt(event["id"], partner["id"], attempt_no, status)
28
+ if not 200 <= status < 300:
29
+ schedule_retry(event, partner, attempt_no)
@@ -0,0 +1,15 @@
1
+ """Retry scheduling for failed deliveries."""
2
+ from src.config import load_yaml
3
+ from src.store.delivery_log import mark_failed
4
+ from src.queue import enqueue_delayed
5
+
6
+ CFG = load_yaml("config/delivery.yaml")["retry"]
7
+
8
+
9
+ def schedule_retry(event, partner, attempt_no):
10
+ if attempt_no >= CFG["max_attempts"]:
11
+ mark_failed(event["id"], partner["id"])
12
+ return
13
+ # TODO(INC-2291): backoff disabled while partner Velo's endpoint flaps; restore afterwards
14
+ delay = CFG["base_delay_seconds"]
15
+ enqueue_delayed(event, partner, attempt_no + 1, delay_seconds=delay)
@@ -0,0 +1,13 @@
1
+ """HMAC-SHA256 signing of webhook bodies."""
2
+ import hashlib
3
+ import hmac
4
+ import time
5
+
6
+ from src.secrets import partner_secret
7
+
8
+
9
+ def sign(partner, body: bytes) -> dict:
10
+ ts = str(int(time.time()))
11
+ msg = ts.encode() + b"." + body
12
+ digest = hmac.new(partner_secret(partner["id"]), msg, hashlib.sha256).hexdigest()
13
+ return {"X-Parcelhook-Signature": digest, "X-Parcelhook-Timestamp": ts}
@@ -0,0 +1,24 @@
1
+ """Nightly export of the delivery log to per-partner S3 prefixes."""
2
+ import csv
3
+ import io
4
+
5
+ from src.config import load_yaml
6
+ from src.db import execute
7
+ from src.s3 import put_object
8
+
9
+ CFG = load_yaml("config/export.yaml")
10
+
11
+
12
+ def run(day):
13
+ rows = execute(
14
+ "SELECT partner_id, event_id, attempt_no, status_code, sent_at FROM delivery_attempts "
15
+ "WHERE sent_at::date = %s ORDER BY partner_id",
16
+ (day,),
17
+ )
18
+ by_partner = {}
19
+ for r in rows:
20
+ by_partner.setdefault(r[0], []).append(r)
21
+ for partner_id, prows in by_partner.items():
22
+ buf = io.StringIO()
23
+ csv.writer(buf).writerows(prows)
24
+ put_object(CFG["bucket"], f"{partner_id}/{day}.csv", buf.getvalue())
@@ -0,0 +1,11 @@
1
+ """Idempotency claims in Postgres (see ADR-0006)."""
2
+ from src.db import execute
3
+
4
+
5
+ def claim(event_id, partner_id) -> bool:
6
+ rows = execute(
7
+ "INSERT INTO idempotency_keys (event_id, partner_id) VALUES (%s, %s) "
8
+ "ON CONFLICT DO NOTHING RETURNING 1",
9
+ (event_id, partner_id),
10
+ )
11
+ return len(rows) == 1
@@ -0,0 +1,8 @@
1
+ """Idempotency claims in Redis (see ADR-0003)."""
2
+ from src.redis_client import redis
3
+
4
+ TTL_SECONDS = 24 * 3600
5
+
6
+
7
+ def claim(event_id, partner_id) -> bool:
8
+ return bool(redis.set(f"idem:{event_id}:{partner_id}", 1, nx=True, ex=TTL_SECONDS))
@@ -0,0 +1,15 @@
1
+ """Load partner configuration."""
2
+ import time
3
+
4
+ from src.db import execute
5
+
6
+ _cache = {"at": 0, "partners": []}
7
+ RELOAD_SECONDS = 60
8
+
9
+
10
+ def load_partners():
11
+ if time.time() - _cache["at"] > RELOAD_SECONDS:
12
+ rows = execute("SELECT id, url, events FROM partners WHERE enabled")
13
+ _cache["partners"] = [{"id": r[0], "url": r[1], "events": r[2]} for r in rows]
14
+ _cache["at"] = time.time()
15
+ return _cache["partners"]
@@ -0,0 +1,18 @@
1
+ """Delivery attempt log (see ADR-0002)."""
2
+ from src.db import execute
3
+
4
+
5
+ def record_attempt(event_id, partner_id, attempt_no, status_code):
6
+ execute(
7
+ "INSERT INTO delivery_attempts (event_id, partner_id, attempt_no, status_code, sent_at) "
8
+ "VALUES (%s, %s, %s, %s, now())",
9
+ (event_id, partner_id, attempt_no, status_code),
10
+ )
11
+
12
+
13
+ def mark_failed(event_id, partner_id):
14
+ execute(
15
+ "UPDATE delivery_attempts SET final_status = 'failed' "
16
+ "WHERE event_id = %s AND partner_id = %s",
17
+ (event_id, partner_id),
18
+ )
@@ -0,0 +1,3 @@
1
+ # shopfront
2
+
3
+ The Brindle & Co. web store monolith: catalog, cart, checkout. Design records live in `docs/decisions/`.
@@ -0,0 +1,2 @@
1
+ PRICING_SVC_URL: http://pricing-svc.internal:8080
2
+ REDIS_URL: redis://cache.internal:6379/0
@@ -0,0 +1,10 @@
1
+ # RFC-008: Read prices from the Halvorsen ERP
2
+
3
+ Status: Accepted
4
+ Date: 2023-02-07
5
+ Author: M. Okafor
6
+ Governs: shopfront/pricing/erp_client.py
7
+
8
+ ## Decision
9
+ Shopfront reads the current price for a SKU from the Halvorsen ERP `GetItemPrice` SOAP call
10
+ through `shopfront/pricing/erp_client.py`. The ERP is the single source of truth for prices.
@@ -0,0 +1,23 @@
1
+ # RFC-012: Cache prices in Redis
2
+
3
+ Status: Accepted
4
+ Date: 2023-06-14
5
+ Author: M. Okafor
6
+ Builds on: RFC-008
7
+ Governs: shopfront/pricing/cache.py, config/cache.yaml
8
+
9
+ ## Context
10
+ The Halvorsen ERP limits `GetItemPrice` to 40 requests/second per client and its p95 latency is
11
+ ~900 ms. During the June sale the product page timed out and the ERP throttled us for 20 minutes.
12
+
13
+ ## Decision
14
+ Put a read-through Redis cache in front of pricing. Key `price:{sku}`, TTL 60 seconds. A price
15
+ change in the ERP can therefore take up to a minute to show on the site; merchandising agreed
16
+ this is acceptable.
17
+
18
+ ## Alternatives considered
19
+ - Nightly price snapshot: rejected, price changes during sales must show within minutes.
20
+ - Asking Halvorsen to raise the limit: rejected by the vendor.
21
+
22
+ ## Revisit when
23
+ The ERP rate limit no longer applies to us.
@@ -0,0 +1,15 @@
1
+ # RFC-015: Lock the price for the duration of checkout
2
+
3
+ Status: Accepted
4
+ Date: 2024-01-09
5
+ Author: J. Lindqvist
6
+ Amends: RFC-012
7
+ Governs: shopfront/checkout/lock.py
8
+
9
+ ## Context
10
+ Customers were charged a different price from the one shown when a price changed mid-checkout.
11
+
12
+ ## Decision
13
+ When a cart enters checkout, the price a customer saw must be the price they are charged for
14
+ 15 minutes. We do this by reusing the pricing cache: checkout keeps the cached price for each
15
+ SKU in the cart alive for 15 minutes.
@@ -0,0 +1,17 @@
1
+ # RFC-019: Read prices from pricing-svc instead of the ERP
2
+
3
+ Status: Proposed
4
+ Date: 2025-03-18
5
+ Author: P. Achterberg
6
+ Supersedes: RFC-008
7
+
8
+ ## Context
9
+ The pricing team now runs `pricing-svc`, a REST service that replicates ERP prices within
10
+ 5 seconds of a change. It has no per-client rate limit and a p95 of 35 ms.
11
+
12
+ ## Proposal
13
+ Shopfront calls `GET {PRICING_SVC_URL}/v1/prices/{sku}` instead of the ERP SOAP call, and
14
+ `erp_client.py` is deleted.
15
+
16
+ ## Open questions
17
+ - Do we still need the Redis cache from RFC-012 once the ERP is out of the path?
@@ -0,0 +1,8 @@
1
+ """Product detail page."""
2
+ from shopfront.pricing import cache
3
+
4
+
5
+ def product_detail(request, sku):
6
+ product = request.catalog.get(sku)
7
+ price = cache.get_price(sku)
8
+ return request.render("product.html", {"product": product, "price": price})
@@ -0,0 +1,14 @@
1
+ """Hold prices steady while a cart is in checkout (RFC-015)."""
2
+ from shopfront.pricing import cache
3
+
4
+ LOCK_SECONDS = 15 * 60
5
+
6
+
7
+ def lock_cart_prices(cart) -> None:
8
+ for line in cart.lines:
9
+ cache.get_price(line.sku) # make sure the entry exists
10
+ cache.touch(line.sku, LOCK_SECONDS) # keep it alive for the checkout window
11
+
12
+
13
+ def price_for_charge(sku: str) -> dict:
14
+ return cache.get_price(sku)
@@ -0,0 +1,22 @@
1
+ """Read-through price cache (RFC-012)."""
2
+ import json
3
+
4
+ from shopfront.redis_client import redis
5
+ from shopfront.settings import load_yaml
6
+ from shopfront.pricing.client import fetch_price
7
+
8
+ TTL = load_yaml("config/cache.yaml")["pricing"]["ttl_seconds"]
9
+
10
+
11
+ def get_price(sku: str) -> dict:
12
+ key = f"price:{sku}"
13
+ hit = redis.get(key)
14
+ if hit is not None:
15
+ return json.loads(hit)
16
+ price = fetch_price(sku)
17
+ redis.set(key, json.dumps(price), ex=TTL)
18
+ return price
19
+
20
+
21
+ def touch(sku: str, ttl: int) -> None:
22
+ redis.expire(f"price:{sku}", ttl)
@@ -0,0 +1,10 @@
1
+ """Fetch the current price for a SKU."""
2
+ import json
3
+ import urllib.request
4
+
5
+ from shopfront.settings import PRICING_SVC_URL
6
+
7
+
8
+ def fetch_price(sku: str) -> dict:
9
+ with urllib.request.urlopen(f"{PRICING_SVC_URL}/v1/prices/{sku}", timeout=2) as resp:
10
+ return json.load(resp) # {"sku": ..., "amount_minor": ..., "currency": ...}