skilldrop-cli 1.0.0 → 1.1.0

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 (275) hide show
  1. package/package.json +1 -1
  2. package/packs/ai-engineering/skills/ai-usage-report/examples/weekly-engineering-team.md +24 -24
  3. package/packs/ai-engineering/skills/ai-usage-report/templates/usage-event-schema.md +9 -9
  4. package/packs/converters/skills/file-to-markdown/manifest.json +1 -1
  5. package/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +116 -38
  6. package/packs/core/pack.json +2 -2
  7. package/packs/{solution-architect → core}/skills/context-graph/SKILL.md +10 -4
  8. package/packs/{solution-architect → core}/skills/context-graph/evals/eval_queries.json +4 -1
  9. package/packs/{solution-architect → core}/skills/context-graph/evals/evals.json +30 -0
  10. package/packs/core/skills/context-graph/evals/files/16/README.md +4 -0
  11. package/packs/core/skills/context-graph/evals/files/16/docs/adr/0001-record-architecture-decisions.md +12 -0
  12. package/packs/core/skills/context-graph/evals/files/16/docs/adr/0002-postgres-for-the-ledger.md +11 -0
  13. package/packs/core/skills/context-graph/evals/files/16/docs/adr/0003-redis-for-ephemeral-state.md +12 -0
  14. package/packs/core/skills/context-graph/evals/files/16/docs/adr/0004-idempotency-keys-for-payment-creation.md +28 -0
  15. package/packs/core/skills/context-graph/evals/files/16/git-history.json +62 -0
  16. package/packs/core/skills/context-graph/evals/files/16/services/payments/api/create.py +7 -0
  17. package/packs/core/skills/context-graph/evals/files/16/services/payments/ledger.py +9 -0
  18. package/packs/core/skills/context-graph/evals/files/17/README.md +5 -0
  19. package/packs/core/skills/context-graph/evals/files/17/docs/adr/0001-record-architecture-decisions.md +9 -0
  20. package/packs/core/skills/context-graph/evals/files/17/docs/adr/0002-email-via-smtp-relay.md +9 -0
  21. package/packs/core/skills/context-graph/evals/files/17/docs/adr/0003-rabbitmq-for-async-messaging.md +9 -0
  22. package/packs/core/skills/context-graph/evals/files/17/docs/adr/0004-email-via-provider-api.md +11 -0
  23. package/packs/core/skills/context-graph/evals/files/17/docs/adr/0005-server-side-template-rendering.md +10 -0
  24. package/packs/core/skills/context-graph/evals/files/17/docs/adr/0006-notification-retry-backoff.md +10 -0
  25. package/packs/core/skills/context-graph/evals/files/17/docs/adr/0007-kafka-for-event-streaming.md +12 -0
  26. package/packs/core/skills/context-graph/evals/files/17/docs/rfcs/RFC-012-unified-retry-policy.md +10 -0
  27. package/packs/core/skills/context-graph/evals/files/17/packages/notification-templates/README.md +4 -0
  28. package/packs/core/skills/context-graph/evals/files/17/packages/notification-templates/invoice_issued.html +2 -0
  29. package/packs/core/skills/context-graph/evals/files/17/packages/notification-templates/welcome.html +2 -0
  30. package/packs/core/skills/context-graph/evals/files/17/services/billing/retry.py +17 -0
  31. package/packs/core/skills/context-graph/evals/files/17/services/notifications/consumer.py +18 -0
  32. package/packs/core/skills/context-graph/evals/files/17/services/notifications/dispatch.py +21 -0
  33. package/packs/core/skills/context-graph/evals/files/17/services/notifications/email/sender.py +17 -0
  34. package/packs/core/skills/context-graph/evals/files/17/services/notifications/render.py +12 -0
  35. package/packs/core/skills/context-graph/evals/files/17/services/notifications/retry.py +14 -0
  36. package/packs/core/skills/context-graph/examples/inputs/order-events/services/notifications/receipts.py +0 -0
  37. package/packs/core/skills/context-graph/examples/inputs/order-events/services/orders/publisher.py +0 -0
  38. package/packs/{solution-architect → core}/skills/context-graph/manifest.json +3 -3
  39. package/packs/{solution-architect → core}/skills/context-graph/reference.md +28 -0
  40. package/packs/{solution-architect → core}/skills/context-graph/scripts/context_graph.py +105 -5
  41. package/packs/dev-team/loops/modernize/LOOP.md +11 -1
  42. package/packs/dev-team/loops/modernize/evals/evals.json +12 -0
  43. package/packs/dev-team/loops/modernize/loop.json +3 -3
  44. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/Dockerfile +1 -1
  45. package/packs/dev-team/skills/rollout-plan/manifest.json +1 -1
  46. package/packs/dev-team/skills/rollout-plan/scripts/canary_math.py +2 -2
  47. package/packs/research/skills/hypothesis-comparison/manifest.json +1 -1
  48. package/packs/research/skills/hypothesis-comparison/scripts/ach_matrix.py +9 -5
  49. package/packs/solution-architect/loops/design/LOOP.md +14 -5
  50. package/packs/solution-architect/loops/design/evals/evals.json +12 -0
  51. package/packs/solution-architect/loops/design/loop.json +5 -5
  52. package/packs/sre-oncall/skills/delivery-metrics-report/manifest.json +1 -1
  53. package/packs/sre-oncall/skills/delivery-metrics-report/scripts/delivery_metrics.py +7 -1
  54. package/packs/trackers/skills/team-status-report/manifest.json +1 -1
  55. package/packs/trackers/skills/team-status-report/scripts/status_counts.py +5 -2
  56. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0001-record-architecture-decisions.md +0 -0
  57. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0002-postgres-as-primary-database.md +0 -0
  58. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0003-use-redis-for-session-storage.md +0 -0
  59. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0004-structured-json-logging.md +0 -0
  60. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0005-session-lifetime-eight-hours.md +0 -0
  61. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0006-move-sessions-to-postgres.md +0 -0
  62. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0007-stateless-signed-session-cookies.md +0 -0
  63. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/services/api/__init__.py +0 -0
  64. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/services/api/db.py +0 -0
  65. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/services/api/logging_setup.py +0 -0
  66. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/services/auth/__init__.py +0 -0
  67. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/services/auth/cookie_sessions.py +0 -0
  68. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/services/auth/redis_sessions.py +0 -0
  69. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/README.md +0 -0
  70. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/docs/adr/0001-record-architecture-decisions.md +0 -0
  71. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/docs/adr/0004-service-auth-hmac-shared-keys.md +0 -0
  72. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/docs/adr/0007-secrets-from-vault-agent.md +0 -0
  73. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/docs/adr/0011-service-auth-mtls.md +0 -0
  74. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/services/billing-api/app.py +0 -0
  75. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/services/billing-api/auth/__init__.py +0 -0
  76. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/services/billing-api/auth/hmac_verify.py +0 -0
  77. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/services/billing-api/config/settings.py +0 -0
  78. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/services/ledger-worker/auth/__init__.py +0 -0
  79. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/services/ledger-worker/auth/mtls.py +0 -0
  80. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/README.md +0 -0
  81. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/app/models/base.py +0 -0
  82. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/app/models/line_item.py +0 -0
  83. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/app/models/order.py +0 -0
  84. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/eng/decisions/DR-003-money-as-minor-units.md +0 -0
  85. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/eng/decisions/DR-005-orders-typed-columns-only.md +0 -0
  86. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/eng/decisions/DR-009-line-items-immutable.md +0 -0
  87. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/eng/decisions/DR-014-optional-order-attributes-json.md +0 -0
  88. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/eng/decisions/DR-016-order-attributes-table.md +0 -0
  89. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/migrations/0007_create_orders.sql +0 -0
  90. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/migrations/0012_add_order_attributes.sql +0 -0
  91. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/README.md +0 -0
  92. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/config/delivery.yaml +0 -0
  93. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/config/export.yaml +0 -0
  94. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/config/partners.yaml +0 -0
  95. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0001-record-architecture-decisions.md +0 -0
  96. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0002-postgres-delivery-log.md +0 -0
  97. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0003-redis-idempotency-keys.md +0 -0
  98. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0004-retry-policy.md +0 -0
  99. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0005-hmac-signed-payloads.md +0 -0
  100. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0006-postgres-idempotency.md +0 -0
  101. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0007-sftp-batch-export.md +0 -0
  102. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0008-partner-config-in-yaml.md +0 -0
  103. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0009-partner-config-admin-api.md +0 -0
  104. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/migrations/003_idempotency_keys.sql +0 -0
  105. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/migrations/004_partners.sql +0 -0
  106. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/delivery/dispatcher.py +0 -0
  107. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/delivery/retry.py +0 -0
  108. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/delivery/signing.py +0 -0
  109. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/export/s3_export.py +0 -0
  110. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/idempotency/pg_store.py +0 -0
  111. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/idempotency/redis_store.py +0 -0
  112. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/partners/loader.py +0 -0
  113. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/store/delivery_log.py +0 -0
  114. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/README.md +0 -0
  115. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/config/cache.yaml +0 -0
  116. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/config/settings.yaml +0 -0
  117. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/docs/decisions/RFC-008-pricing-from-erp.md +0 -0
  118. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/docs/decisions/RFC-012-pricing-cache.md +0 -0
  119. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/docs/decisions/RFC-015-checkout-price-lock.md +0 -0
  120. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/docs/decisions/RFC-019-pricing-service.md +0 -0
  121. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/shopfront/catalog/views.py +0 -0
  122. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/shopfront/checkout/lock.py +0 -0
  123. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/shopfront/pricing/cache.py +0 -0
  124. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/shopfront/pricing/client.py +0 -0
  125. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/docs/rfcs/0001-idempotency-keys.md +0 -0
  126. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/docs/rfcs/0002-processor-retry-policy.md +0 -0
  127. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/docs/rfcs/0003-money-in-minor-units.md +0 -0
  128. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/docs/rfcs/0004-webhook-signing.md +0 -0
  129. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/docs/rfcs/0005-retry-with-jitter.md +0 -0
  130. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/docs/rfcs/0006-idempotency-at-gateway.md +0 -0
  131. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/pr-summary.md +0 -0
  132. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/src/payments/amount.ts +0 -0
  133. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/src/payments/charge.ts +0 -0
  134. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/src/payments/idempotency.ts +0 -0
  135. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/src/payments/retry.ts +0 -0
  136. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/src/webhooks/send.ts +0 -0
  137. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/src/webhooks/sign.ts +0 -0
  138. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/deploy/services.yaml +0 -0
  139. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0001-record-architecture-decisions.md +0 -0
  140. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0002-use-postgresql-for-shipment-data.md +0 -0
  141. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0003-store-vehicle-telemetry-in-cassandra.md +0 -0
  142. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0004-use-redis-for-session-storage.md +0 -0
  143. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0005-use-react-for-dispatcher-dashboard.md +0 -0
  144. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0006-archive-delivered-shipments-to-object-storage.md +0 -0
  145. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0007-move-shipment-records-to-mongodb.md +0 -0
  146. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0008-store-vehicle-telemetry-in-timescaledb.md +0 -0
  147. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0009-stateless-sessions-with-signed-cookies.md +0 -0
  148. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/services/api/session.py +0 -0
  149. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/services/shipments/db.py +0 -0
  150. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/services/shipments/migrations.sql +0 -0
  151. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/services/telemetry/store.py +0 -0
  152. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/web/dashboard/App.jsx +0 -0
  153. /package/packs/{solution-architect/skills/context-graph/evals/files/2/libs/money → core/skills/context-graph/evals/files/16/services/payments}/__init__.py +0 -0
  154. /package/packs/{solution-architect/skills/context-graph/evals/files/3/services/notifications → core/skills/context-graph/evals/files/16/services/payments/api}/__init__.py +0 -0
  155. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0001-record-architecture-decisions.md +0 -0
  156. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0002-rabbitmq-for-background-jobs.md +0 -0
  157. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0003-thumbnail-service.md +0 -0
  158. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0004-money-as-integer-minor-units.md +0 -0
  159. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0005-worker-retries.md +0 -0
  160. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0006-fx-amounts-with-four-decimals.md +0 -0
  161. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0007-feature-flags.md +0 -0
  162. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0008-retire-legacy-search.md +0 -0
  163. /package/packs/{solution-architect/skills/context-graph/evals/files/3/services/payments → core/skills/context-graph/evals/files/2/libs/money}/__init__.py +0 -0
  164. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/libs/money/amount.py +0 -0
  165. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/services/checkout/flags.py +0 -0
  166. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/services/checkout/publish.py +0 -0
  167. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/services/media/resize.py +0 -0
  168. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/services/worker/consumer.py +0 -0
  169. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0001-how-we-write-rfcs.md +0 -0
  170. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0002-payments-service-boundary.md +0 -0
  171. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0003-webhook-retries-fixed-interval.md +0 -0
  172. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0004-webhook-idempotency-keys.md +0 -0
  173. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0005-notification-retries.md +0 -0
  174. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0006-webhook-retry-backoff.md +0 -0
  175. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0007-webhooks-on-the-job-queue.md +0 -0
  176. /package/packs/{solution-architect/skills/context-graph/evals/files/3/services/payments/webhooks → core/skills/context-graph/evals/files/3/services/notifications}/__init__.py +0 -0
  177. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/services/notifications/retry.py +0 -0
  178. /package/packs/{solution-architect/skills/context-graph/evals/files/4/src/ledgerline → core/skills/context-graph/evals/files/3/services/payments}/__init__.py +0 -0
  179. /package/packs/{solution-architect/skills/context-graph/evals/files/4/src/ledgerline/scheduler → core/skills/context-graph/evals/files/3/services/payments/webhooks}/__init__.py +0 -0
  180. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/services/payments/webhooks/retry.py +0 -0
  181. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/services/payments/webhooks/send.py +0 -0
  182. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/docs/adr/0001-record-architecture-decisions.md +0 -0
  183. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/docs/adr/0002-postgres-as-system-of-record.md +0 -0
  184. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/docs/adr/0004-redis-locks-for-scheduler.md +0 -0
  185. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/docs/adr/0007-remove-redis.md +0 -0
  186. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/docs/adr/0009-money-as-integer-minor-units.md +0 -0
  187. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/docs/onboarding.md +0 -0
  188. /package/packs/{solution-architect/skills/context-graph/examples/inputs/order-events/services/notifications/receipts.py → core/skills/context-graph/evals/files/4/src/ledgerline/__init__.py} +0 -0
  189. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/src/ledgerline/db/__init__.py +0 -0
  190. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/src/ledgerline/invoicing.py +0 -0
  191. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/src/ledgerline/money.py +0 -0
  192. /package/packs/{solution-architect/skills/context-graph/examples/inputs/order-events/services/orders/publisher.py → core/skills/context-graph/evals/files/4/src/ledgerline/scheduler/__init__.py} +0 -0
  193. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/src/ledgerline/scheduler/lock.py +0 -0
  194. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/src/ledgerline/scheduler/runner.py +0 -0
  195. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/lib/types/index.ts +0 -0
  196. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/rfcs/0001-service-layout.md +0 -0
  197. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/rfcs/0003-ingest-retry-policy.md +0 -0
  198. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/rfcs/0006-shared-retry-library.md +0 -0
  199. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/rfcs/0008-outbound-http-client.md +0 -0
  200. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/services/export/client.ts +0 -0
  201. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/services/export/retry.ts +0 -0
  202. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/services/ingest/client.ts +0 -0
  203. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/services/ingest/retry.ts +0 -0
  204. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/README.md +0 -0
  205. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/adr/0001-monorepo-layout.md +0 -0
  206. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/adr/0002-billing-on-postgres.md +0 -0
  207. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/adr/0004-ledger-sync-nightly-batch.md +0 -0
  208. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/rfcs/RFC-0003-invoice-ids.md +0 -0
  209. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/rfcs/RFC-0007-invoice-ids-ulid.md +0 -0
  210. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/rfcs/RFC-0009-billing-event-store.md +0 -0
  211. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/rfcs/RFC-0010-revenue-service.md +0 -0
  212. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/libs/money/ulid.py +0 -0
  213. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/services/billing/src/events.py +0 -0
  214. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/services/billing/src/store.py +0 -0
  215. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/services/invoicing/ids/generator.py +0 -0
  216. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/services/ledger/README.md +0 -0
  217. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/services/ledger/sync.py +0 -0
  218. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/README.md +0 -0
  219. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/db/queries/orders.sql +0 -0
  220. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/db/sharding/router.py +0 -0
  221. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/design/decisions/DD-001-postgres-primary.md +0 -0
  222. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/design/decisions/DD-002-kafka-event-bus.md +0 -0
  223. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/design/decisions/DD-003-no-orm.md +0 -0
  224. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/design/decisions/DD-006-tenant-sharding.md +0 -0
  225. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/design/decisions/DD-011-orm-for-admin-console.md +0 -0
  226. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/pipelines/fanout/router.py +0 -0
  227. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/pipelines/ingest/consumer.py +0 -0
  228. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/pipelines/ingest/topics.yaml +0 -0
  229. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/rfcs/0004-region-sharding.md +0 -0
  230. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/services/admin-console/models.py +0 -0
  231. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/services/orders/repo.py +0 -0
  232. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/README.md +0 -0
  233. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/api/ledger.proto +0 -0
  234. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/config/http.yaml +0 -0
  235. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/config/messaging.yaml +0 -0
  236. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0001-record-architecture-decisions.md +0 -0
  237. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0002-postgresql-for-ledger.md +0 -0
  238. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0003-rabbitmq-domain-events.md +0 -0
  239. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0004-integer-minor-units.md +0 -0
  240. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0005-provider-http-retries.md +0 -0
  241. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0006-openapi-generated-clients.md +0 -0
  242. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0007-kafka-domain-events.md +0 -0
  243. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0008-structured-logging.md +0 -0
  244. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/rfcs/RFC-012-idempotency-keys.md +0 -0
  245. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/api/idempotency.py +0 -0
  246. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/api/server.py +0 -0
  247. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/clients/provider.py +0 -0
  248. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/db/migrations/0001_init.sql +0 -0
  249. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/db/migrations/0008_idempotency_keys.sql +0 -0
  250. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/db/migrations/0009_fx_entries.sql +0 -0
  251. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/events/publisher.py +0 -0
  252. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/logging.py +0 -0
  253. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/models/entry.py +0 -0
  254. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/.env.example +0 -0
  255. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/README.md +0 -0
  256. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/docker-compose.yml +0 -0
  257. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/docs/decisions/001-session-storage.md +0 -0
  258. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/docs/decisions/002-api-rate-limiting.md +0 -0
  259. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/docs/decisions/003-jwt-sessions.md +0 -0
  260. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/docs/decisions/004-catalog-cache.md +0 -0
  261. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/catalog/cache.ts +0 -0
  262. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/catalog/repository.ts +0 -0
  263. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/jobs/email.ts +0 -0
  264. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/jobs/export.ts +0 -0
  265. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/jobs/queue.ts +0 -0
  266. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/jobs/worker.ts +0 -0
  267. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/lib/db.ts +0 -0
  268. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/lib/redis.ts +0 -0
  269. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/middleware/rateLimit.ts +0 -0
  270. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/server.ts +0 -0
  271. /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/session/store.ts +0 -0
  272. /package/packs/{solution-architect → core}/skills/context-graph/examples/inputs/order-events/docs/adr/0004-kafka-for-order-events.md +0 -0
  273. /package/packs/{solution-architect → core}/skills/context-graph/examples/inputs/order-events/docs/adr/0006-email-receipts.md +0 -0
  274. /package/packs/{solution-architect → core}/skills/context-graph/examples/inputs/order-events/docs/adr/0009-sqs-for-order-events.md +0 -0
  275. /package/packs/{solution-architect → core}/skills/context-graph/examples/order-events.md +0 -0
@@ -0,0 +1,28 @@
1
+ # ADR-0004: Idempotency keys for payment creation
2
+
3
+ Status: Accepted
4
+ Date: 2025-03-03
5
+ Deciders: Priya Natarajan, Tomás Okafor
6
+ Governs: services/payments/api/create.py, services/payments/idempotency.py
7
+
8
+ ## Context
9
+
10
+ Merchant integrations retry `POST /payments` on timeouts. In February we had two
11
+ incidents where a retried request created a second charge.
12
+
13
+ ## Decision
14
+
15
+ 1. `POST /payments` MUST carry an `Idempotency-Key` header. A request without
16
+ one is rejected with `400`.
17
+ 2. Keys are scoped per merchant: the stored record is keyed on
18
+ `(merchant_id, Idempotency-Key)`.
19
+ 3. The first response for a key is stored and replayed verbatim for 24 hours.
20
+ 4. A request that reuses a key with a *different* request body is rejected with
21
+ `422`. A client bug that reuses keys must never be handed the result of a
22
+ different payment.
23
+ 5. Records are stored in Redis, per ADR-0003.
24
+
25
+ ## Consequences
26
+
27
+ Clients must generate a key per logical payment. Rule 4 means we fingerprint
28
+ the request body and compare it on every replay.
@@ -0,0 +1,62 @@
1
+ {
2
+ "start": {
3
+ "date": "2025-03-03",
4
+ "author": "Priya Natarajan",
5
+ "message": "Initial import"
6
+ },
7
+ "commits": [
8
+ {
9
+ "date": "2025-03-10",
10
+ "author": "Priya Natarajan",
11
+ "message": "payments: require Idempotency-Key on POST /payments (ADR-0004)\n\nStores the first response per (merchant, key) in Redis and rejects key reuse with a different body.",
12
+ "files": {
13
+ "services/payments/idempotency.py": "\"\"\"Idempotency store for POST /payments. See docs/adr/0004.\"\"\"\nimport hashlib\nimport json\n\nTTL_SECONDS = 24 * 60 * 60 # ADR-0004: replay window\n\n\nclass IdempotencyConflict(Exception):\n \"\"\"Same key reused with a different request body (ADR-0004 rule 4).\"\"\"\n\n\ndef _store_key(merchant_id, key):\n return f\"idem:{merchant_id}:{key}\"\n\n\ndef _fingerprint(body):\n return hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()\n\n\ndef lookup(redis, merchant_id, key, body):\n raw = redis.get(_store_key(merchant_id, key))\n if raw is None:\n return None\n entry = json.loads(raw)\n if entry[\"fingerprint\"] != _fingerprint(body):\n raise IdempotencyConflict(key)\n return entry[\"status\"], entry[\"response\"]\n\n\ndef save(redis, merchant_id, key, body, status, response):\n entry = {\"fingerprint\": _fingerprint(body), \"status\": status, \"response\": response}\n redis.set(_store_key(merchant_id, key), json.dumps(entry), ex=TTL_SECONDS)\n",
14
+ "services/payments/api/create.py": "from services.payments import idempotency\nfrom services.payments.ledger import record_payment\nfrom services.payments.redis_client import redis\n\n\ndef create_payment(request):\n key = request.headers.get(\"Idempotency-Key\")\n if not key:\n return 400, {\"error\": \"Idempotency-Key header is required\"}\n body = request.json()\n try:\n cached = idempotency.lookup(redis, request.merchant_id, key, body)\n except idempotency.IdempotencyConflict:\n return 422, {\"error\": \"Idempotency-Key reused with a different request body\"}\n if cached is not None:\n return cached\n payment = record_payment(request.merchant_id, body)\n idempotency.save(redis, request.merchant_id, key, body, 201, payment)\n return 201, payment\n",
15
+ "services/payments/redis_client.py": "\"\"\"Shared Redis connection (ADR-0003).\"\"\"\nimport os\n\nimport redis as _redis\n\nredis = _redis.Redis.from_url(os.environ.get(\"REDIS_URL\", \"redis://localhost:6379/0\"))\n"
16
+ }
17
+ },
18
+ {
19
+ "date": "2025-03-12",
20
+ "author": "Tomás Okafor",
21
+ "message": "tests: idempotency replay, merchant scoping, mismatch and TTL",
22
+ "files": {
23
+ "tests/__init__.py": "",
24
+ "tests/test_idempotency.py": "import pytest\n\nfrom services.payments import idempotency\n\n\nclass FakeRedis:\n def __init__(self):\n self.data = {}\n self.ttls = {}\n\n def get(self, k):\n return self.data.get(k)\n\n def set(self, k, v, ex=None):\n self.data[k] = v\n self.ttls[k] = ex\n\n\ndef test_replay_returns_stored_response():\n r = FakeRedis()\n idempotency.save(r, \"m_1\", \"k1\", {\"amount\": 500}, 201, {\"id\": \"pay_1\"})\n assert idempotency.lookup(r, \"m_1\", \"k1\", {\"amount\": 500}) == (201, {\"id\": \"pay_1\"})\n\n\ndef test_keys_are_scoped_per_merchant():\n r = FakeRedis()\n idempotency.save(r, \"m_1\", \"k1\", {\"amount\": 500}, 201, {\"id\": \"pay_1\"})\n assert idempotency.lookup(r, \"m_2\", \"k1\", {\"amount\": 500}) is None\n\n\ndef test_mismatched_body_raises_conflict():\n r = FakeRedis()\n idempotency.save(r, \"m_1\", \"k1\", {\"amount\": 500}, 201, {\"id\": \"pay_1\"})\n with pytest.raises(idempotency.IdempotencyConflict):\n idempotency.lookup(r, \"m_1\", \"k1\", {\"amount\": 900})\n\n\ndef test_ttl_is_24_hours():\n r = FakeRedis()\n idempotency.save(r, \"m_1\", \"k1\", {\"amount\": 500}, 201, {\"id\": \"pay_1\"})\n assert r.ttls[\"idem:m_1:k1\"] == 86400\n"
25
+ }
26
+ },
27
+ {
28
+ "date": "2025-06-02",
29
+ "author": "Lena Hargrove",
30
+ "message": "docs: ADR-0007 extend idempotency retention to 48h",
31
+ "files": {
32
+ "docs/adr/0007-extend-idempotency-retention.md": "# ADR-0007: Extend idempotency retention to 48 hours\n\nStatus: Accepted\nDate: 2025-06-02\nDeciders: Lena Hargrove, Priya Natarajan\nAmends: ADR-0004 (rule 3 only)\nGoverns: services/payments/idempotency.py\n\n## Context\n\nAfter the 17 May processor outage, card-network retries arrived up to ~40 hours\nafter the original request. With a 24-hour window, those retries were treated as\nnew payments.\n\n## Decision\n\nIdempotency records are kept and replayed for 48 hours instead of 24. All other\nrules in ADR-0004 are unchanged.\n\n## Consequences\n\nRoughly double the Redis memory for `idem:*` keys.\n"
33
+ }
34
+ },
35
+ {
36
+ "date": "2025-06-04",
37
+ "author": "Lena Hargrove",
38
+ "message": "payments: idempotency TTL 24h -> 48h per ADR-0007",
39
+ "files": {
40
+ "services/payments/idempotency.py": "\"\"\"Idempotency store for POST /payments. See docs/adr/0004.\"\"\"\nimport hashlib\nimport json\n\nTTL_SECONDS = 48 * 60 * 60 # ADR-0007: replay window extended from 24h\n\n\nclass IdempotencyConflict(Exception):\n \"\"\"Same key reused with a different request body (ADR-0004 rule 4).\"\"\"\n\n\ndef _store_key(merchant_id, key):\n return f\"idem:{merchant_id}:{key}\"\n\n\ndef _fingerprint(body):\n return hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()\n\n\ndef lookup(redis, merchant_id, key, body):\n raw = redis.get(_store_key(merchant_id, key))\n if raw is None:\n return None\n entry = json.loads(raw)\n if entry[\"fingerprint\"] != _fingerprint(body):\n raise IdempotencyConflict(key)\n return entry[\"status\"], entry[\"response\"]\n\n\ndef save(redis, merchant_id, key, body, status, response):\n entry = {\"fingerprint\": _fingerprint(body), \"status\": status, \"response\": response}\n redis.set(_store_key(merchant_id, key), json.dumps(entry), ex=TTL_SECONDS)\n",
41
+ "tests/test_idempotency.py": "import pytest\n\nfrom services.payments import idempotency\n\n\nclass FakeRedis:\n def __init__(self):\n self.data = {}\n self.ttls = {}\n\n def get(self, k):\n return self.data.get(k)\n\n def set(self, k, v, ex=None):\n self.data[k] = v\n self.ttls[k] = ex\n\n\ndef test_replay_returns_stored_response():\n r = FakeRedis()\n idempotency.save(r, \"m_1\", \"k1\", {\"amount\": 500}, 201, {\"id\": \"pay_1\"})\n assert idempotency.lookup(r, \"m_1\", \"k1\", {\"amount\": 500}) == (201, {\"id\": \"pay_1\"})\n\n\ndef test_keys_are_scoped_per_merchant():\n r = FakeRedis()\n idempotency.save(r, \"m_1\", \"k1\", {\"amount\": 500}, 201, {\"id\": \"pay_1\"})\n assert idempotency.lookup(r, \"m_2\", \"k1\", {\"amount\": 500}) is None\n\n\ndef test_mismatched_body_raises_conflict():\n r = FakeRedis()\n idempotency.save(r, \"m_1\", \"k1\", {\"amount\": 500}, 201, {\"id\": \"pay_1\"})\n with pytest.raises(idempotency.IdempotencyConflict):\n idempotency.lookup(r, \"m_1\", \"k1\", {\"amount\": 900})\n\n\ndef test_ttl_is_48_hours():\n r = FakeRedis()\n idempotency.save(r, \"m_1\", \"k1\", {\"amount\": 500}, 201, {\"id\": \"pay_1\"})\n assert r.ttls[\"idem:m_1:k1\"] == 172800\n"
42
+ }
43
+ },
44
+ {
45
+ "date": "2025-08-19",
46
+ "author": "Dmitri Vasquez",
47
+ "message": "refactor: simplify idempotency store lookups\n\nCollapse get/decode into a single _load helper. No behaviour change intended.",
48
+ "files": {
49
+ "services/payments/idempotency.py": "\"\"\"Idempotency store for POST /payments. See docs/adr/0004.\"\"\"\nimport hashlib\nimport json\n\nTTL_SECONDS = 48 * 60 * 60 # ADR-0007: replay window extended from 24h\n\n\nclass IdempotencyConflict(Exception):\n \"\"\"Same key reused with a different request body (ADR-0004 rule 4).\"\"\"\n\n\ndef _store_key(merchant_id, key):\n return f\"idem:{merchant_id}:{key}\"\n\n\ndef _fingerprint(body):\n return hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()\n\n\ndef _load(redis, merchant_id, key):\n raw = redis.get(_store_key(merchant_id, key))\n return json.loads(raw) if raw is not None else None\n\n\ndef lookup(redis, merchant_id, key, body):\n entry = _load(redis, merchant_id, key)\n if entry is None:\n return None\n return entry[\"status\"], entry[\"response\"]\n\n\ndef save(redis, merchant_id, key, body, status, response):\n entry = {\"fingerprint\": _fingerprint(body), \"status\": status, \"response\": response}\n redis.set(_store_key(merchant_id, key), json.dumps(entry), ex=TTL_SECONDS)\n",
50
+ "tests/test_idempotency.py": "import pytest\n\nfrom services.payments import idempotency\n\n\nclass FakeRedis:\n def __init__(self):\n self.data = {}\n self.ttls = {}\n\n def get(self, k):\n return self.data.get(k)\n\n def set(self, k, v, ex=None):\n self.data[k] = v\n self.ttls[k] = ex\n\n\ndef test_replay_returns_stored_response():\n r = FakeRedis()\n idempotency.save(r, \"m_1\", \"k1\", {\"amount\": 500}, 201, {\"id\": \"pay_1\"})\n assert idempotency.lookup(r, \"m_1\", \"k1\", {\"amount\": 500}) == (201, {\"id\": \"pay_1\"})\n\n\ndef test_keys_are_scoped_per_merchant():\n r = FakeRedis()\n idempotency.save(r, \"m_1\", \"k1\", {\"amount\": 500}, 201, {\"id\": \"pay_1\"})\n assert idempotency.lookup(r, \"m_2\", \"k1\", {\"amount\": 500}) is None\n\n\n@pytest.mark.skip(reason=\"flaky since lookup refactor, see PAY-311\")\ndef test_mismatched_body_raises_conflict():\n r = FakeRedis()\n idempotency.save(r, \"m_1\", \"k1\", {\"amount\": 500}, 201, {\"id\": \"pay_1\"})\n with pytest.raises(idempotency.IdempotencyConflict):\n idempotency.lookup(r, \"m_1\", \"k1\", {\"amount\": 900})\n\n\ndef test_ttl_is_48_hours():\n r = FakeRedis()\n idempotency.save(r, \"m_1\", \"k1\", {\"amount\": 500}, 201, {\"id\": \"pay_1\"})\n assert r.ttls[\"idem:m_1:k1\"] == 172800\n"
51
+ }
52
+ },
53
+ {
54
+ "date": "2025-09-30",
55
+ "author": "Tomás Okafor",
56
+ "message": "payments: debug logging on idempotency hits and creates",
57
+ "files": {
58
+ "services/payments/api/create.py": "import logging\n\nfrom services.payments import idempotency\nfrom services.payments.ledger import record_payment\nfrom services.payments.redis_client import redis\n\nlog = logging.getLogger(__name__)\n\n\ndef create_payment(request):\n key = request.headers.get(\"Idempotency-Key\")\n if not key:\n return 400, {\"error\": \"Idempotency-Key header is required\"}\n body = request.json()\n try:\n cached = idempotency.lookup(redis, request.merchant_id, key, body)\n except idempotency.IdempotencyConflict:\n return 422, {\"error\": \"Idempotency-Key reused with a different request body\"}\n if cached is not None:\n log.debug(\"idempotency hit merchant=%s key=%s\", request.merchant_id, key)\n return cached\n payment = record_payment(request.merchant_id, body)\n idempotency.save(redis, request.merchant_id, key, body, 201, payment)\n log.debug(\"payment created merchant=%s id=%s\", request.merchant_id, payment[\"id\"])\n return 201, payment\n"
59
+ }
60
+ }
61
+ ]
62
+ }
@@ -0,0 +1,7 @@
1
+ from services.payments.ledger import record_payment
2
+
3
+
4
+ def create_payment(request):
5
+ body = request.json()
6
+ payment = record_payment(request.merchant_id, body)
7
+ return 201, payment
@@ -0,0 +1,9 @@
1
+ """Ledger writes (ADR-0002)."""
2
+ import itertools
3
+
4
+ _ids = itertools.count(1)
5
+
6
+
7
+ def record_payment(merchant_id, body):
8
+ # Real implementation writes payments + ledger_entries in one transaction.
9
+ return {"id": f"pay_{next(_ids)}", "merchant_id": merchant_id, "amount": body["amount"]}
@@ -0,0 +1,5 @@
1
+ # relay-platform
2
+
3
+ Monorepo for the Quillmont platform team: `services/notifications`, `services/billing`,
4
+ and shared packages under `packages/`. Decisions are in `docs/adr/` (service-level) and
5
+ `docs/rfcs/` (cross-service).
@@ -0,0 +1,9 @@
1
+ # ADR-0001: Record architecture decisions
2
+
3
+ Status: Accepted
4
+ Date: 2024-02-05
5
+ Deciders: Mara Lindqvist
6
+
7
+ We keep ADRs in `docs/adr/` and cross-service RFCs in `docs/rfcs/`. Each record has a
8
+ Status line. When a record is replaced, both records say so: the old one becomes
9
+ "Superseded by X" and the new one says "Supersedes Y".
@@ -0,0 +1,9 @@
1
+ # ADR-0002: Send email through the internal SMTP relay
2
+
3
+ Status: Superseded by ADR-0004
4
+ Date: 2024-03-11
5
+ Deciders: Mara Lindqvist
6
+ Governs: services/notifications/email/
7
+
8
+ Notifications sends email by connecting to the shared SMTP relay at
9
+ `smtp.internal.example.invalid`.
@@ -0,0 +1,9 @@
1
+ # ADR-0003: RabbitMQ for asynchronous messaging
2
+
3
+ Status: Accepted
4
+ Date: 2024-04-02
5
+ Deciders: Mara Lindqvist, Joaquin Ferreira
6
+ Governs: services/notifications/consumer.py
7
+
8
+ Services publish domain events to a RabbitMQ topic exchange (`domain-events`).
9
+ Notifications consumes from its own durable queue bound to `user.*` and `invoice.*`.
@@ -0,0 +1,11 @@
1
+ # ADR-0004: Send email through the provider's HTTP API
2
+
3
+ Status: Accepted
4
+ Date: 2024-07-22
5
+ Deciders: Joaquin Ferreira
6
+ Supersedes: ADR-0002
7
+ Governs: services/notifications/email/
8
+
9
+ The SMTP relay could not give us per-message delivery status. Notifications now sends
10
+ email through the provider's HTTPS send API and records the provider message id for
11
+ every send.
@@ -0,0 +1,10 @@
1
+ # ADR-0005: Render notification templates server-side
2
+
3
+ Status: Accepted
4
+ Date: 2024-08-14
5
+ Deciders: Joaquin Ferreira, Hana Oduya
6
+ Governs: services/notifications/templates/
7
+
8
+ Templates are Jinja2 files rendered inside the notifications service before sending.
9
+ Callers send a template name and variables, never pre-rendered HTML, so that every
10
+ message goes through the same escaping and branding.
@@ -0,0 +1,10 @@
1
+ # ADR-0006: Retry failed notification sends with exponential backoff
2
+
3
+ Status: Accepted
4
+ Date: 2024-09-30
5
+ Deciders: Hana Oduya
6
+ Governs: services/notifications/retry.py
7
+
8
+ Failed sends are retried up to 5 attempts in total, with exponential backoff starting
9
+ at 2 seconds (2s, 4s, 8s, 16s). After the last attempt the message goes to the
10
+ dead-letter store.
@@ -0,0 +1,12 @@
1
+ # ADR-0007: Kafka for domain event streaming
2
+
3
+ Status: Accepted
4
+ Date: 2025-01-27
5
+ Deciders: Joaquin Ferreira, Mara Lindqvist
6
+ Supersedes: ADR-0003
7
+ Governs: services/notifications/consumer.py
8
+
9
+ We need replay and per-key ordering, which our RabbitMQ setup does not give us. Domain
10
+ events move to Kafka topics `user-events` and `invoice-events`. Notifications consumes
11
+ both as consumer group `notifications`. See RFC-015 for the migration plan and cutover
12
+ dates.
@@ -0,0 +1,10 @@
1
+ # RFC-012: One retry policy for every service
2
+
3
+ Status: Accepted
4
+ Date: 2025-03-18
5
+ Authors: Teodora Basu
6
+ Governs: services/*/retry.py
7
+
8
+ Each service had invented its own retry rules, which made incident timelines hard to
9
+ read. Every service's `retry.py` uses the same policy: at most 3 attempts in total,
10
+ full-jitter exponential backoff with a 1 second base and a 30 second cap.
@@ -0,0 +1,4 @@
1
+ # notification-templates
2
+
3
+ Jinja2 templates for notification emails, shared so that billing can preview invoice
4
+ emails. Moved here from `services/notifications/templates/`.
@@ -0,0 +1,2 @@
1
+ <h1>Invoice {{ invoice_number }}</h1>
2
+ <p>Amount due: {{ amount_due }} by {{ due_date }}.</p>
@@ -0,0 +1,2 @@
1
+ <h1>Welcome to Quillmont, {{ first_name }}!</h1>
2
+ <p>Your workspace is ready.</p>
@@ -0,0 +1,17 @@
1
+ import random
2
+ import time
3
+
4
+ MAX_ATTEMPTS = 3
5
+ BASE_DELAY_SECONDS = 1
6
+ MAX_DELAY_SECONDS = 30
7
+
8
+
9
+ def with_retry(fn):
10
+ for attempt in range(1, MAX_ATTEMPTS + 1):
11
+ try:
12
+ return fn()
13
+ except Exception:
14
+ if attempt == MAX_ATTEMPTS:
15
+ raise
16
+ cap = min(MAX_DELAY_SECONDS, BASE_DELAY_SECONDS * 2 ** (attempt - 1))
17
+ time.sleep(random.uniform(0, cap))
@@ -0,0 +1,18 @@
1
+ """Consumes domain events and turns them into notifications."""
2
+ from kafka import KafkaConsumer
3
+
4
+ from services.notifications.dispatch import dispatch
5
+
6
+ TOPICS = ("user-events", "invoice-events")
7
+
8
+
9
+ def run(bootstrap_servers):
10
+ consumer = KafkaConsumer(
11
+ *TOPICS,
12
+ bootstrap_servers=bootstrap_servers,
13
+ group_id="notifications",
14
+ enable_auto_commit=False,
15
+ )
16
+ for message in consumer:
17
+ dispatch(message.topic, message.value)
18
+ consumer.commit()
@@ -0,0 +1,21 @@
1
+ import logging
2
+
3
+ from services.notifications.email.sender import send_email
4
+ from services.notifications.render import render
5
+ from services.notifications.retry import with_retry
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+ EVENT_TEMPLATES = {
10
+ ("user-events", "user.signed_up"): "welcome.html",
11
+ ("invoice-events", "invoice.issued"): "invoice_issued.html",
12
+ }
13
+
14
+
15
+ def dispatch(topic, event):
16
+ template = EVENT_TEMPLATES.get((topic, event["type"]))
17
+ if template is None:
18
+ return
19
+ html = render(template, event["data"])
20
+ message_id = with_retry(lambda: send_email(event["data"]["email"], event["subject"], html))
21
+ log.info("sent template=%s event_id=%s provider_message_id=%s", template, event["id"], message_id)
@@ -0,0 +1,17 @@
1
+ """Email delivery through the provider's HTTPS send API."""
2
+ import os
3
+
4
+ import requests
5
+
6
+ SEND_URL = "https://api.mail-provider.example.invalid/v3/send"
7
+
8
+
9
+ def send_email(to, subject, html):
10
+ response = requests.post(
11
+ SEND_URL,
12
+ headers={"Authorization": f"Bearer {os.environ['MAIL_API_KEY']}"},
13
+ json={"to": to, "subject": subject, "html": html},
14
+ timeout=10,
15
+ )
16
+ response.raise_for_status()
17
+ return response.json()["message_id"]
@@ -0,0 +1,12 @@
1
+ """Server-side rendering of notification templates."""
2
+ from pathlib import Path
3
+
4
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
5
+
6
+ TEMPLATE_DIR = Path(__file__).resolve().parents[2] / "packages" / "notification-templates"
7
+
8
+ _env = Environment(loader=FileSystemLoader(TEMPLATE_DIR), autoescape=select_autoescape(["html"]))
9
+
10
+
11
+ def render(template_name, variables):
12
+ return _env.get_template(template_name).render(**variables)
@@ -0,0 +1,14 @@
1
+ import time
2
+
3
+ MAX_ATTEMPTS = 5
4
+ BASE_DELAY_SECONDS = 2
5
+
6
+
7
+ def with_retry(fn):
8
+ for attempt in range(1, MAX_ATTEMPTS + 1):
9
+ try:
10
+ return fn()
11
+ except Exception:
12
+ if attempt == MAX_ATTEMPTS:
13
+ raise
14
+ time.sleep(BASE_DELAY_SECONDS * 2 ** (attempt - 1))
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "context-graph",
3
- "version": "0.1.0",
4
- "description": "Map the decisions behind a codebase from its ADRs, RFCs and design records — each record's status and date, what supersedes or amends what, which records cite which, the files and folders each decision governs, and with git the commits that implemented it — then answer \"why is it like this?\" from the record and check the record is still true. Finds supersessions only one side records, current decisions pointing at code that is gone, two current decisions governing the same path, and references to records that don't exist. Use when the user asks \"why do we use X\", \"which decisions govern this file or service\", \"is this ADR still current\", \"are our ADRs out of date\", \"what was decided before I change this\", wants a map of the architecture decisions, or is onboarding onto a codebase's history.",
3
+ "version": "0.2.0",
4
+ "description": "Map the decisions behind a codebase from its ADRs, RFCs and design records — each record's status and date, what supersedes or amends what, which records cite which, the files and folders each decision governs, and with git the commits that implemented it and the commits that changed each decision's code since it was made, and who made them — then answer \"why is it like this?\" from the record and check the record is still true. Finds supersessions only one side records, current decisions pointing at code that is gone, code that has drifted from its decision, two current decisions governing the same path, and references to records that don't exist. Use when the user asks \"why do we use X\", \"which decisions govern this file or service\", \"is this ADR still current\", \"has the code drifted since we decided this\", \"are our ADRs out of date\", \"what was decided before I change this\", wants a map of the architecture decisions, or is onboarding onto a codebase's history.",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": { "npm": [], "pip": [] },
7
7
  "env": { "required": [], "optional": [] },
@@ -24,7 +24,7 @@
24
24
  "network": [],
25
25
  "commands": ["git"],
26
26
  "files": "named-paths",
27
- "notes": "context_graph.py reads the decision-record folders under --root and checks whether the paths they name exist. With --git it runs `git log` read-only. It writes only to the -o and --mermaid paths."
27
+ "notes": "context_graph.py reads the decision-record folders under --root and checks whether the paths they name exist. With --git it runs `git log` read-only, for commit messages and for the commits that touched each governed path. It writes only to the -o and --mermaid paths."
28
28
  },
29
29
  "model": {
30
30
  "tier": "standard",
@@ -30,6 +30,7 @@ Every `.md` file in them, recursively, is a record, except:
30
30
  | missing paths | a path that looks like the repository's (its first part is a top-level folder that exists) but doesn't exist |
31
31
  | moved | for a missing path, an existing sibling with a close name (`services/ledger-sync` → `services/ledger`), where the code may have gone |
32
32
  | commits | with `--git`: commits whose subject or body mentions the id, newest first, up to 20 |
33
+ | drift | with `--git`, for a current record that governs a path: the commits that changed those paths since the record's date, newest first. When the record has no date, they count from when its file was first committed. Each has its author and whether it cites the record. Also gives the authors and their counts, the last change, and when the record file was last edited. Merges are left out |
33
34
 
34
35
  **Statuses, normalised:**
35
36
 
@@ -81,6 +82,11 @@ A file that isn't UTF-8 is skipped, with a note in the report.
81
82
  "date": "2025-03-02", "path": "docs/adr/0004-….md",
82
83
  "supersedes": [], "superseded_by": [], "amends": [], "cites": [], "cited_by": [],
83
84
  "governs": ["services/orders/"], "missing_paths": [], "commits": [],
85
+ "drift": {"since": "2025-03-02", "since_from": "record", "count": 3, "not_citing_record": 2,
86
+ "authors": [{"name": "Ben", "commits": 2}, {"name": "Ana", "commits": 1}],
87
+ "last_change": "2026-06-04", "record_last_edited": "2025-03-02",
88
+ "commits": [{"sha": "a1b2c3d", "date": "2026-06-04", "author": "Ben", "subject": "…",
89
+ "cites_record": false}]},
84
90
  "successors": ["ADR-0009"], "successors_only_claimed_by_successor": ["ADR-0009"],
85
91
  "moved": {"services/kafka-bridge": ["services/bridge"]}
86
92
  }],
@@ -91,9 +97,31 @@ A file that isn't UTF-8 is skipped, with a note in the report.
91
97
  }
92
98
  ```
93
99
 
100
+ `drift` is `null` without `--git`, and for a record that isn't current or governs no path.
101
+
94
102
  `successors` joins a record's own "superseded by" with every record that says it supersedes this
95
103
  one. The second list holds the ones only the newer record records.
96
104
 
105
+ ## Code changed since each decision (`--git`)
106
+
107
+ The full report gains a table of the current decisions whose governed code has changed since
108
+ they were made, most changed first:
109
+
110
+ | Column | Meaning |
111
+ |---|---|
112
+ | Decided | the record's date; `*` when it has none and the date is when the record was first committed |
113
+ | Commits since | commits to the governed paths since that date, merges left out |
114
+ | Not citing it | how many of those don't mention the record's id: the changes made without reference to it |
115
+ | By | the authors, with their counts, most first |
116
+ | Last change | the newest of those commits |
117
+ | Record last edited | the last commit to the record file itself |
118
+
119
+ `--why` adds a "Changed since" line under each current decision, and `--record` lists up to 10
120
+ of the commits. The Markdown table shows 15 rows; `--json` has them all.
121
+
122
+ A commit to a decision's code is a reason to check it, not drift by itself. Drift is a
123
+ requirement in the record that the code no longer meets.
124
+
97
125
  ## Limits
98
126
 
99
127
  - **A decision made in a ticket or a chat, and never written down, isn't in the graph.** The
@@ -3,13 +3,14 @@
3
3
 
4
4
  Reads decision records (ADRs, RFCs, design decisions) as Markdown, finds each one's id, title,
5
5
  status and date, and the edges between them: what supersedes or amends what, which records cite
6
- which, the files and folders each one governs, and, with --git, the commits that mention it.
6
+ which, the files and folders each one governs, and, with --git, the commits that mention it and
7
+ the commits that changed each current decision's code since it was made, and who made them.
7
8
  Then it checks the graph for what goes wrong as a codebase ages: a supersession only one side
8
9
  records, a current decision pointing at code that is gone, two current decisions governing the
9
10
  same path, references to records that don't exist.
10
11
 
11
12
  python3 context_graph.py # find records under ./docs, check them
12
- python3 context_graph.py --root repo --git # and the commits that mention each record
13
+ python3 context_graph.py --root repo --git # and the commits behind each record, and since it
13
14
  python3 context_graph.py --why src/auth/session.py
14
15
  python3 context_graph.py --record ADR-0004
15
16
  python3 context_graph.py --docs architecture/decisions --json -o graph.json --mermaid graph.mmd
@@ -191,7 +192,7 @@ def parse_record(path, root, text):
191
192
  rec = {"id": rid, "title": title_of(meta, body, stem.replace("-", " ")), "status": status,
192
193
  "status_text": raw_status, "date": date_of(meta, body), "path": rel,
193
194
  "supersedes": [], "superseded_by": [], "amends": [], "cites": [], "governs": [],
194
- "missing_paths": [], "commits": []}
195
+ "missing_paths": [], "commits": [], "drift": None}
195
196
  rec["supersedes"] += list_ids(meta.get("supersedes", "")) + list_ids(meta.get("replaces", ""))
196
197
  rec["superseded_by"] += list_ids(meta.get("superseded_by", "")) + list_ids(meta.get("replaced_by", ""))
197
198
  rec["amends"] += list_ids(meta.get("amends", ""))
@@ -290,6 +291,69 @@ def git_commits(root, records, limit):
290
291
  return None
291
292
 
292
293
 
294
+ def _git(root, *args):
295
+ """stdout of a git command in root, or None when git is missing or the command fails."""
296
+ try:
297
+ out = subprocess.run(["git", "-C", root, *args], capture_output=True, text=True,
298
+ encoding="utf-8", errors="replace", timeout=60)
299
+ except (OSError, subprocess.SubprocessError):
300
+ return None
301
+ return out.stdout if out.returncode == 0 else None
302
+
303
+
304
+ def git_drift(root, records, limit):
305
+ """For each current decision that governs code: the commits that changed that code since the
306
+ decision was made, who made them, and when the record itself was last edited. Code that keeps
307
+ changing under a record nobody revisits is where a decision quietly stops being true."""
308
+ for r in records:
309
+ if r["status"] not in CURRENT or r["successors"] or not r["governs"]:
310
+ continue
311
+ since, source = r["date"], "record"
312
+ if not since: # no date in the record: when the record was first committed
313
+ added = _git(root, "log", "--diff-filter=A", "--follow", "--date=short", "--format=%ad", "--", r["path"])
314
+ since, source = ((added or "").split() or [""])[-1], "git"
315
+ if not since:
316
+ r["drift"] = {"since": None, "note": "the record has no date and isn't committed, so changes since it can't be dated"}
317
+ continue
318
+ log = _git(root, "log", "--no-merges", "--since=%s 00:00" % since, "-n", str(limit), "--date=short",
319
+ "--format=%h%x1f%ad%x1f%an%x1f%s%x1f%b%x1e", "--", *[g.rstrip("/") or "." for g in r["governs"]])
320
+ commits = []
321
+ for entry in (log or "").split("\x1e"):
322
+ parts = entry.strip("\n").split("\x1f")
323
+ if len(parts) < 4:
324
+ continue
325
+ body = parts[4] if len(parts) > 4 else ""
326
+ commits.append({"sha": parts[0], "date": parts[1], "author": parts[2], "subject": parts[3][:120],
327
+ "cites_record": r["id"] in ids_in(parts[3] + "\n" + body)})
328
+ authors = {}
329
+ for c in commits:
330
+ authors[c["author"]] = authors.get(c["author"], 0) + 1
331
+ edited = (_git(root, "log", "-1", "--date=short", "--format=%ad", "--", r["path"]) or "").strip()
332
+ r["drift"] = {
333
+ "since": since, "since_from": source, "count": len(commits),
334
+ "not_citing_record": sum(1 for c in commits if not c["cites_record"]),
335
+ "authors": [{"name": n, "commits": k} for n, k in sorted(authors.items(), key=lambda x: (-x[1], x[0]))],
336
+ "last_change": commits[0]["date"] if commits else None,
337
+ "record_last_edited": edited or None,
338
+ "commits": commits[:20],
339
+ }
340
+
341
+
342
+ def drift_line(d):
343
+ """One line: '12 commits by Ana (9), Ben (3) since 2026-02-10, the last on 2026-09-30'."""
344
+ if not d or d.get("since") is None:
345
+ return (d or {}).get("note", "")
346
+ if not d["count"]:
347
+ return "no commits have changed its code since %s" % d["since"]
348
+ who = ", ".join("%s (%d)" % (a["name"], a["commits"]) for a in d["authors"][:4]) + (" …" if len(d["authors"]) > 4 else "")
349
+ cite = "" if not d["not_citing_record"] else ("; %d don't cite the record" % d["not_citing_record"]
350
+ if d["not_citing_record"] < d["count"] else "; none cites the record")
351
+ return "%d commit%s by %s since %s%s, the last on %s%s" % (
352
+ d["count"], "" if d["count"] == 1 else "s", who, d["since"],
353
+ " (when the record was committed; it has no date)" if d.get("since_from") == "git" else "",
354
+ d["last_change"], cite)
355
+
356
+
293
357
  SKIP_DIRS = {".git", "node_modules", "vendor", "dist", "build", ".venv", "venv", "__pycache__", "target"}
294
358
 
295
359
 
@@ -432,7 +496,8 @@ def why(records, path, root):
432
496
  hits.sort(key=lambda h: (0 if live(h[0]) else 1, rank.get(h[0]["status"], 2),
433
497
  -int(h[0]["date"].replace("-", "") or 0)))
434
498
  return p, [{"id": r["id"], "title": r["title"], "status": r["status"], "date": r["date"], "path": r["path"],
435
- "via": g, "superseded_by": sorted(set(succ.get(r["id"], []))), "current": live(r)} for r, g in hits]
499
+ "via": g, "superseded_by": sorted(set(succ.get(r["id"], []))), "current": live(r),
500
+ "drift": r.get("drift")} for r, g in hits]
436
501
 
437
502
 
438
503
  def mermaid(records):
@@ -479,6 +544,8 @@ def report(records, findings, note, query, dirs, root):
479
544
  state = h["status"] + (", current" if h["current"] else "")
480
545
  out.append("- **%s %s** (%s%s), via `%s`: %s" % (
481
546
  h["id"], h["title"], state, ", " + h["date"] if h["date"] else "", h["via"], h["path"]))
547
+ if h.get("drift"):
548
+ out.append(" - Changed since: %s" % drift_line(h["drift"]))
482
549
  if not cur:
483
550
  out.append("\nNone of these is current: the decisions that governed it were superseded, "
484
551
  "deprecated or never accepted.")
@@ -502,6 +569,13 @@ def report(records, findings, note, query, dirs, root):
502
569
  out.append("- %s: %s" % (label, ", ".join(r[k])))
503
570
  for c in r["commits"]:
504
571
  out.append("- Commit %s %s: %s" % (c["sha"], c["date"], c["subject"]))
572
+ if r.get("drift"):
573
+ out.append("- Code changed since the decision: %s" % drift_line(r["drift"]))
574
+ if r["drift"].get("record_last_edited"):
575
+ out.append("- Record last edited: %s" % r["drift"]["record_last_edited"])
576
+ for c in r["drift"].get("commits", [])[:10]:
577
+ out.append(" - %s %s %s: %s%s" % (c["sha"], c["date"], c["author"], c["subject"],
578
+ " (cites it)" if c["cites_record"] else ""))
505
579
  out.append("")
506
580
  counts = {}
507
581
  for r in records:
@@ -516,6 +590,28 @@ def report(records, findings, note, query, dirs, root):
516
590
  out += ["- %s %s" % (rid, msg) for _, rid, msg in probs] or ["None."]
517
591
  out.append("\n## Warnings (%d)\n" % len(warns))
518
592
  out += ["- %s %s" % (rid, msg) for _, rid, msg in warns] or ["None."]
593
+ drifted = sorted((r for r in records if r.get("drift") and r["drift"].get("count")),
594
+ key=lambda r: (-r["drift"]["count"], r["id"]))
595
+ if not query and any(r.get("drift") for r in records):
596
+ out.append("\n## Code changed since each decision\n")
597
+ if not drifted:
598
+ out.append("No commit has changed the code a current decision governs since it was made.")
599
+ else:
600
+ out.append("Current decisions whose code has changed since they were made, most changed first. "
601
+ "Each is a place to check that the code still does what the record says.\n")
602
+ out.append("| Record | Decided | Commits since | Not citing it | By | Last change | Record last edited |")
603
+ out.append("|---|---|---|---|---|---|---|")
604
+ for r in drifted[:15]:
605
+ d = r["drift"]
606
+ out.append("| %s %s | %s%s | %d | %d | %s | %s | %s |" % (
607
+ r["id"], r["title"].replace("|", "/")[:50], d["since"], "*" if d["since_from"] == "git" else "",
608
+ d["count"], d["not_citing_record"],
609
+ ", ".join("%s (%d)" % (a["name"].replace("|", "/"), a["commits"]) for a in d["authors"][:3])
610
+ + (" …" if len(d["authors"]) > 3 else ""), d["last_change"], d["record_last_edited"] or "-"))
611
+ if len(drifted) > 15:
612
+ out.append("\n…and %d more; `--json` lists them all." % (len(drifted) - 15))
613
+ if any(r["drift"]["since_from"] == "git" for r in drifted[:15]):
614
+ out.append("\n\\* The record has no date, so this counts from when it was first committed.")
519
615
  if not query:
520
616
  out.append("\n## Graph\n")
521
617
  out.append("| Record | Status | Date | Supersedes | Superseded by | Governs |")
@@ -536,7 +632,9 @@ def _main(argv=None):
536
632
  q = ap.add_mutually_exclusive_group()
537
633
  q.add_argument("--why", metavar="PATH", help="list the decisions that govern a file or folder")
538
634
  q.add_argument("--record", metavar="ID", help="show one record and its neighbours, e.g. ADR-0004")
539
- ap.add_argument("--git", action="store_true", help="also read commit messages for record ids")
635
+ ap.add_argument("--git", action="store_true",
636
+ help="also read the commits that mention each record, and those that changed each current "
637
+ "decision's code since it was made")
540
638
  ap.add_argument("--git-limit", type=int, default=5000)
541
639
  ap.add_argument("--json", action="store_true", help="print the graph as JSON instead of Markdown")
542
640
  ap.add_argument("--mermaid", metavar="FILE", help="also write a Mermaid flowchart of the graph")
@@ -579,6 +677,8 @@ def _main(argv=None):
579
677
  claimed = [o["id"] for o in records if r["id"] in o["supersedes"]]
580
678
  r["successors"] = sorted(set(r["superseded_by"]) | set(claimed))
581
679
  r["successors_only_claimed_by_successor"] = sorted(set(claimed) - set(r["superseded_by"]))
680
+ if a.git and not any("no commits were read" in n for n in notes):
681
+ git_drift(root, records, a.git_limit)
582
682
  # Guides and READMEs that still cite a superseded decision may still teach it.
583
683
  by_id = {r["id"]: r for r in records}
584
684
  docs = other_docs(root, {r["path"] for r in records})