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.
- package/package.json +1 -1
- package/packs/ai-engineering/skills/ai-usage-report/examples/weekly-engineering-team.md +24 -24
- package/packs/ai-engineering/skills/ai-usage-report/templates/usage-event-schema.md +9 -9
- package/packs/converters/skills/file-to-markdown/manifest.json +1 -1
- package/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +116 -38
- package/packs/core/pack.json +2 -2
- package/packs/{solution-architect → core}/skills/context-graph/SKILL.md +10 -4
- package/packs/{solution-architect → core}/skills/context-graph/evals/eval_queries.json +4 -1
- package/packs/{solution-architect → core}/skills/context-graph/evals/evals.json +30 -0
- package/packs/core/skills/context-graph/evals/files/16/README.md +4 -0
- package/packs/core/skills/context-graph/evals/files/16/docs/adr/0001-record-architecture-decisions.md +12 -0
- package/packs/core/skills/context-graph/evals/files/16/docs/adr/0002-postgres-for-the-ledger.md +11 -0
- package/packs/core/skills/context-graph/evals/files/16/docs/adr/0003-redis-for-ephemeral-state.md +12 -0
- package/packs/core/skills/context-graph/evals/files/16/docs/adr/0004-idempotency-keys-for-payment-creation.md +28 -0
- package/packs/core/skills/context-graph/evals/files/16/git-history.json +62 -0
- package/packs/core/skills/context-graph/evals/files/16/services/payments/api/create.py +7 -0
- package/packs/core/skills/context-graph/evals/files/16/services/payments/ledger.py +9 -0
- package/packs/core/skills/context-graph/evals/files/17/README.md +5 -0
- package/packs/core/skills/context-graph/evals/files/17/docs/adr/0001-record-architecture-decisions.md +9 -0
- package/packs/core/skills/context-graph/evals/files/17/docs/adr/0002-email-via-smtp-relay.md +9 -0
- package/packs/core/skills/context-graph/evals/files/17/docs/adr/0003-rabbitmq-for-async-messaging.md +9 -0
- package/packs/core/skills/context-graph/evals/files/17/docs/adr/0004-email-via-provider-api.md +11 -0
- package/packs/core/skills/context-graph/evals/files/17/docs/adr/0005-server-side-template-rendering.md +10 -0
- package/packs/core/skills/context-graph/evals/files/17/docs/adr/0006-notification-retry-backoff.md +10 -0
- package/packs/core/skills/context-graph/evals/files/17/docs/adr/0007-kafka-for-event-streaming.md +12 -0
- package/packs/core/skills/context-graph/evals/files/17/docs/rfcs/RFC-012-unified-retry-policy.md +10 -0
- package/packs/core/skills/context-graph/evals/files/17/packages/notification-templates/README.md +4 -0
- package/packs/core/skills/context-graph/evals/files/17/packages/notification-templates/invoice_issued.html +2 -0
- package/packs/core/skills/context-graph/evals/files/17/packages/notification-templates/welcome.html +2 -0
- package/packs/core/skills/context-graph/evals/files/17/services/billing/retry.py +17 -0
- package/packs/core/skills/context-graph/evals/files/17/services/notifications/consumer.py +18 -0
- package/packs/core/skills/context-graph/evals/files/17/services/notifications/dispatch.py +21 -0
- package/packs/core/skills/context-graph/evals/files/17/services/notifications/email/sender.py +17 -0
- package/packs/core/skills/context-graph/evals/files/17/services/notifications/render.py +12 -0
- package/packs/core/skills/context-graph/evals/files/17/services/notifications/retry.py +14 -0
- package/packs/core/skills/context-graph/examples/inputs/order-events/services/notifications/receipts.py +0 -0
- package/packs/core/skills/context-graph/examples/inputs/order-events/services/orders/publisher.py +0 -0
- package/packs/{solution-architect → core}/skills/context-graph/manifest.json +3 -3
- package/packs/{solution-architect → core}/skills/context-graph/reference.md +28 -0
- package/packs/{solution-architect → core}/skills/context-graph/scripts/context_graph.py +105 -5
- package/packs/dev-team/loops/modernize/LOOP.md +11 -1
- package/packs/dev-team/loops/modernize/evals/evals.json +12 -0
- package/packs/dev-team/loops/modernize/loop.json +3 -3
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/Dockerfile +1 -1
- package/packs/dev-team/skills/rollout-plan/manifest.json +1 -1
- package/packs/dev-team/skills/rollout-plan/scripts/canary_math.py +2 -2
- package/packs/research/skills/hypothesis-comparison/manifest.json +1 -1
- package/packs/research/skills/hypothesis-comparison/scripts/ach_matrix.py +9 -5
- package/packs/solution-architect/loops/design/LOOP.md +14 -5
- package/packs/solution-architect/loops/design/evals/evals.json +12 -0
- package/packs/solution-architect/loops/design/loop.json +5 -5
- package/packs/sre-oncall/skills/delivery-metrics-report/manifest.json +1 -1
- package/packs/sre-oncall/skills/delivery-metrics-report/scripts/delivery_metrics.py +7 -1
- package/packs/trackers/skills/team-status-report/manifest.json +1 -1
- package/packs/trackers/skills/team-status-report/scripts/status_counts.py +5 -2
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0001-record-architecture-decisions.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0002-postgres-as-primary-database.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0003-use-redis-for-session-storage.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0004-structured-json-logging.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0005-session-lifetime-eight-hours.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0006-move-sessions-to-postgres.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/docs/adr/0007-stateless-signed-session-cookies.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/services/api/__init__.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/services/api/db.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/services/api/logging_setup.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/services/auth/__init__.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/services/auth/cookie_sessions.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/1/services/auth/redis_sessions.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/README.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/docs/adr/0001-record-architecture-decisions.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/docs/adr/0004-service-auth-hmac-shared-keys.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/docs/adr/0007-secrets-from-vault-agent.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/docs/adr/0011-service-auth-mtls.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/services/billing-api/app.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/services/billing-api/auth/__init__.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/services/billing-api/auth/hmac_verify.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/services/billing-api/config/settings.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/services/ledger-worker/auth/__init__.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/10/services/ledger-worker/auth/mtls.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/README.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/app/models/base.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/app/models/line_item.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/app/models/order.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/eng/decisions/DR-003-money-as-minor-units.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/eng/decisions/DR-005-orders-typed-columns-only.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/eng/decisions/DR-009-line-items-immutable.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/eng/decisions/DR-014-optional-order-attributes-json.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/eng/decisions/DR-016-order-attributes-table.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/migrations/0007_create_orders.sql +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/11/migrations/0012_add_order_attributes.sql +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/README.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/config/delivery.yaml +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/config/export.yaml +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/config/partners.yaml +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0001-record-architecture-decisions.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0002-postgres-delivery-log.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0003-redis-idempotency-keys.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0004-retry-policy.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0005-hmac-signed-payloads.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0006-postgres-idempotency.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0007-sftp-batch-export.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0008-partner-config-in-yaml.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/docs/adr/0009-partner-config-admin-api.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/migrations/003_idempotency_keys.sql +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/migrations/004_partners.sql +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/delivery/dispatcher.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/delivery/retry.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/delivery/signing.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/export/s3_export.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/idempotency/pg_store.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/idempotency/redis_store.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/partners/loader.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/12/src/store/delivery_log.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/README.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/config/cache.yaml +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/config/settings.yaml +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/docs/decisions/RFC-008-pricing-from-erp.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/docs/decisions/RFC-012-pricing-cache.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/docs/decisions/RFC-015-checkout-price-lock.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/docs/decisions/RFC-019-pricing-service.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/shopfront/catalog/views.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/shopfront/checkout/lock.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/shopfront/pricing/cache.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/13/shopfront/pricing/client.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/docs/rfcs/0001-idempotency-keys.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/docs/rfcs/0002-processor-retry-policy.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/docs/rfcs/0003-money-in-minor-units.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/docs/rfcs/0004-webhook-signing.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/docs/rfcs/0005-retry-with-jitter.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/docs/rfcs/0006-idempotency-at-gateway.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/pr-summary.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/src/payments/amount.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/src/payments/charge.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/src/payments/idempotency.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/src/payments/retry.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/src/webhooks/send.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/14/src/webhooks/sign.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/deploy/services.yaml +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0001-record-architecture-decisions.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0002-use-postgresql-for-shipment-data.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0003-store-vehicle-telemetry-in-cassandra.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0004-use-redis-for-session-storage.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0005-use-react-for-dispatcher-dashboard.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0006-archive-delivered-shipments-to-object-storage.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0007-move-shipment-records-to-mongodb.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0008-store-vehicle-telemetry-in-timescaledb.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/doc/adr/0009-stateless-sessions-with-signed-cookies.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/services/api/session.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/services/shipments/db.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/services/shipments/migrations.sql +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/services/telemetry/store.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/15/web/dashboard/App.jsx +0 -0
- /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
- /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
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0001-record-architecture-decisions.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0002-rabbitmq-for-background-jobs.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0003-thumbnail-service.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0004-money-as-integer-minor-units.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0005-worker-retries.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0006-fx-amounts-with-four-decimals.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0007-feature-flags.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/docs/adr/0008-retire-legacy-search.md +0 -0
- /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
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/libs/money/amount.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/services/checkout/flags.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/services/checkout/publish.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/services/media/resize.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/2/services/worker/consumer.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0001-how-we-write-rfcs.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0002-payments-service-boundary.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0003-webhook-retries-fixed-interval.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0004-webhook-idempotency-keys.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0005-notification-retries.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0006-webhook-retry-backoff.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/docs/rfcs/0007-webhooks-on-the-job-queue.md +0 -0
- /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
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/services/notifications/retry.py +0 -0
- /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
- /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
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/services/payments/webhooks/retry.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/3/services/payments/webhooks/send.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/docs/adr/0001-record-architecture-decisions.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/docs/adr/0002-postgres-as-system-of-record.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/docs/adr/0004-redis-locks-for-scheduler.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/docs/adr/0007-remove-redis.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/docs/adr/0009-money-as-integer-minor-units.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/docs/onboarding.md +0 -0
- /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
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/src/ledgerline/db/__init__.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/src/ledgerline/invoicing.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/src/ledgerline/money.py +0 -0
- /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
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/src/ledgerline/scheduler/lock.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/4/src/ledgerline/scheduler/runner.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/lib/types/index.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/rfcs/0001-service-layout.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/rfcs/0003-ingest-retry-policy.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/rfcs/0006-shared-retry-library.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/rfcs/0008-outbound-http-client.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/services/export/client.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/services/export/retry.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/services/ingest/client.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/5/services/ingest/retry.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/README.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/adr/0001-monorepo-layout.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/adr/0002-billing-on-postgres.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/adr/0004-ledger-sync-nightly-batch.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/rfcs/RFC-0003-invoice-ids.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/rfcs/RFC-0007-invoice-ids-ulid.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/rfcs/RFC-0009-billing-event-store.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/docs/rfcs/RFC-0010-revenue-service.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/libs/money/ulid.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/services/billing/src/events.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/services/billing/src/store.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/services/invoicing/ids/generator.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/services/ledger/README.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/6/services/ledger/sync.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/README.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/db/queries/orders.sql +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/db/sharding/router.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/design/decisions/DD-001-postgres-primary.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/design/decisions/DD-002-kafka-event-bus.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/design/decisions/DD-003-no-orm.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/design/decisions/DD-006-tenant-sharding.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/design/decisions/DD-011-orm-for-admin-console.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/pipelines/fanout/router.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/pipelines/ingest/consumer.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/pipelines/ingest/topics.yaml +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/rfcs/0004-region-sharding.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/services/admin-console/models.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/7/services/orders/repo.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/README.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/api/ledger.proto +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/config/http.yaml +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/config/messaging.yaml +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0001-record-architecture-decisions.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0002-postgresql-for-ledger.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0003-rabbitmq-domain-events.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0004-integer-minor-units.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0005-provider-http-retries.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0006-openapi-generated-clients.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0007-kafka-domain-events.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/adr/0008-structured-logging.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/docs/rfcs/RFC-012-idempotency-keys.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/api/idempotency.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/api/server.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/clients/provider.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/db/migrations/0001_init.sql +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/db/migrations/0008_idempotency_keys.sql +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/db/migrations/0009_fx_entries.sql +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/events/publisher.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/logging.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/8/services/ledger/models/entry.py +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/.env.example +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/README.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/docker-compose.yml +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/docs/decisions/001-session-storage.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/docs/decisions/002-api-rate-limiting.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/docs/decisions/003-jwt-sessions.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/docs/decisions/004-catalog-cache.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/catalog/cache.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/catalog/repository.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/jobs/email.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/jobs/export.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/jobs/queue.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/jobs/worker.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/lib/db.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/lib/redis.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/middleware/rateLimit.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/server.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/evals/files/9/src/session/store.ts +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/examples/inputs/order-events/docs/adr/0004-kafka-for-order-events.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/examples/inputs/order-events/docs/adr/0006-email-receipts.md +0 -0
- /package/packs/{solution-architect → core}/skills/context-graph/examples/inputs/order-events/docs/adr/0009-sqs-for-order-events.md +0 -0
- /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,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,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`.
|
package/packs/core/skills/context-graph/evals/files/17/docs/adr/0003-rabbitmq-for-async-messaging.md
ADDED
|
@@ -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.*`.
|
package/packs/core/skills/context-graph/evals/files/17/docs/adr/0004-email-via-provider-api.md
ADDED
|
@@ -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.
|
package/packs/core/skills/context-graph/evals/files/17/docs/adr/0006-notification-retry-backoff.md
ADDED
|
@@ -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.
|
package/packs/core/skills/context-graph/evals/files/17/docs/adr/0007-kafka-for-event-streaming.md
ADDED
|
@@ -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.
|
package/packs/core/skills/context-graph/evals/files/17/docs/rfcs/RFC-012-unified-retry-policy.md
ADDED
|
@@ -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,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))
|
|
File without changes
|
package/packs/core/skills/context-graph/examples/inputs/order-events/services/orders/publisher.py
ADDED
|
File without changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-graph",
|
|
3
|
-
"version": "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
|
|
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)
|
|
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",
|
|
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})
|