skilldrop-cli 1.0.1 → 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/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/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/{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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skilldrop-cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Portable AI-agent skills, measured against the agent without them. ADRs, PRDs, runbooks, decks and reviews, installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Sanjay Ananthanarayan",
|
package/packs/core/pack.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "The cross-role toolkit every pack builds on: structured intake, document critique, the review council,
|
|
2
|
+
"description": "The cross-role toolkit every pack builds on: structured intake, document critique, the review council, output hygiene, and the decision graph behind a codebase, plus the ship-a-draft wrapper. Installed automatically with every role pack.",
|
|
3
3
|
"display_name": "Core",
|
|
4
|
-
"keywords": ["intake", "critique", "review", "hygiene"],
|
|
4
|
+
"keywords": ["intake", "critique", "review", "hygiene", "decisions"],
|
|
5
5
|
"links": {
|
|
6
6
|
"documentation": "https://sananthanarayan.github.io/skilldrop/packs/core/"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: context-graph
|
|
3
|
-
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
|
+
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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# context-graph
|
|
@@ -31,7 +31,7 @@ the code itself is `reverse-architecture`.
|
|
|
31
31
|
# Other IDEs (from the skill folder)
|
|
32
32
|
python3 scripts/context_graph.py --root <repo> --docs architecture/decisions --json -o graph.json --mermaid graph.mmd
|
|
33
33
|
```
|
|
34
|
-
`--git` adds the commits whose messages mention each record. A path counts as governed when a record names it in backticks or links to it, and it exists. Without the script, do the same by hand and say the links were found by reading, not computed.
|
|
34
|
+
`--git` adds the commits whose messages mention each record. For each current decision that governs code, it also lists the commits that changed that code since the decision's date (or since the record was committed, when it has no date). It gives who made them, how many cite the record, and when the record itself was last edited. A path counts as governed when a record names it in backticks or links to it, and it exists. Without the script, do the same by hand and say the links were found by reading, not computed.
|
|
35
35
|
|
|
36
36
|
**The script's findings are leads, not answers.** Confirm each one by reading the records and the code before you report it, and drop the ones that don't hold. Two records "governing the same path" often don't clash at all: say so only when their decisions actually conflict, and quote both.
|
|
37
37
|
|
|
@@ -43,12 +43,15 @@ the code itself is `reverse-architecture`.
|
|
|
43
43
|
|
|
44
44
|
4. **Check the deciding record against the code.** Open the files it governs and check each thing the record requires (every "must", every named field, check, limit or setting), not only its headline. Confirm or contradict each with the code, quoting the line: ✅ *"`scheduler/jobs.py` line 18 still opens a database transaction per job, as ADR-0012 says."* Report drift as a finding with both sides: ✅ *"ADR-0015 promises 30 days of audit-log retention; `ops/logging.yaml` sets `retention_days: 14`."* When the record describes a phased change (a migration, a dual-write, a rollout), say which phase the code is in. While it's mid-rollout, the old decision still holds for the parts not yet moved: that is a missing cross-reference, not a contradiction.
|
|
45
45
|
|
|
46
|
+
With git history, start with what changed since the decision: the commits to its code that don't cite it, newest first. Read what each one changed against the record's requirements. A commit is not drift by itself; a requirement the code no longer meets is. ✅ *"ADR-0009 (2026-02-10) requires one queue per tenant. Three commits since; `a1b2c3d` (Ben, 2026-05-03, 'Switch to shared queue') moved `orders/publish.py` to a single queue."* When the code changed a lot and the record was never touched, say so even when you find no contradiction. Name the people who made the changes: they are who to ask about what the record doesn't say.
|
|
47
|
+
|
|
46
48
|
5. **For "can I remove or change X?", search the code for every use of X,** not only the records: imports, client constructors, config keys, environment variables and container definitions. A use that no record covers is still a dependency: list it as "in use, no recorded decision", with its file. The records tell you why; only the code tells you everything that would break.
|
|
47
49
|
|
|
48
50
|
6. **Find the decisions that bear on it, not only the ones that name its path.** Read any record that amends, extends or narrows the governing one, and any that sets a rule for the same concern across the codebase (error handling, data access, logging, authentication), even when it names a different folder. A shared-library rule governs a change that creates a shared library. Before you call a related record unrelated, read it.
|
|
49
51
|
|
|
50
52
|
7. **Report what is no longer true, worst first.** Problems before warnings:
|
|
51
53
|
- **Supersession recorded on one side only:** "ADR-0004 still says Accepted". Fix it by marking it "Superseded by ADR-0009".
|
|
54
|
+
- **Code that no longer does what its current decision requires:** quote the requirement and the line, and name the commit that changed it when the history shows one.
|
|
52
55
|
- **A current decision pointing at code that is gone:** the record names a folder or file that no longer exists. When the script suggests where it moved (a renamed sibling), or a README says so, report it as moved, not gone, and give the path to update. Only a path with no successor is gone; then the decision is dead, so deprecate it.
|
|
53
56
|
- **A guide that still teaches a superseded decision:** an onboarding doc or a README that cites the old record, or describes its choice. Name the file and the line to change.
|
|
54
57
|
- **Two current decisions governing the same path, neither superseding nor amending the other:** a possible conflict. Read both, quote the clash if there is one, and recommend which one supersedes or amends the other.
|
|
@@ -56,7 +59,7 @@ the code itself is `reverse-architecture`.
|
|
|
56
59
|
|
|
57
60
|
8. **Give each fix as the exact edit.** The line to change in which record, such as changing `## Status` to `Superseded by ADR-0009`, linked to that record's file, or the new record that's needed and what it must decide. Don't rewrite records nobody asked about.
|
|
58
61
|
|
|
59
|
-
9. **Say what the record can't tell.** Decisions made in chat or tickets and never written down, code that no record governs, and, without `--git`, whether anything was actually implemented. When a question has no record, say so, and suggest writing one only if the decision is still live.
|
|
62
|
+
9. **Say what the record can't tell.** Decisions made in chat or tickets and never written down, code that no record governs, and, without `--git`, whether anything was actually implemented or has changed since. When a question has no record, say so, and suggest writing one only if the decision is still live.
|
|
60
63
|
|
|
61
64
|
10. **Write the reply in this order, at the size asked for:**
|
|
62
65
|
- the answer, in two or three sentences, with the governing record's id;
|
|
@@ -71,7 +74,7 @@ the code itself is `reverse-architecture`.
|
|
|
71
74
|
## Useful references in this skill
|
|
72
75
|
|
|
73
76
|
- [`reference.md`](reference.md): the record formats read (MADR, Nygard, RFC frontmatter), how status, supersession, amendment and governed paths are detected, every check and its fix, and the JSON shape
|
|
74
|
-
- [`scripts/context_graph.py`](scripts/context_graph.py): stdlib; builds the graph, checks it, answers `--why PATH` and `--record ID`; `--json`, `--mermaid`, `--git
|
|
77
|
+
- [`scripts/context_graph.py`](scripts/context_graph.py): stdlib; builds the graph, checks it, answers `--why PATH` and `--record ID`; `--json`, `--mermaid`, `--git` (commits behind each record, and changes since each decision); exits 1 on problems and 2 on bad input
|
|
75
78
|
- [`examples/order-events.md`](examples/order-events.md): worked example, a repo whose Kafka ADR was superseded on one side only and whose newer ADR names a folder that was deleted
|
|
76
79
|
|
|
77
80
|
## Quality bar
|
|
@@ -86,6 +89,7 @@ the code itself is `reverse-architecture`.
|
|
|
86
89
|
- **A removal or change question lists every use of the thing in the code,** recorded or not.
|
|
87
90
|
- **Related records are read before they're called unrelated:** amendments, and rules for the same concern elsewhere in the codebase.
|
|
88
91
|
- **A moved path is reported as moved, with its new location; only a path with no successor is "gone".**
|
|
92
|
+
- **Drift is a requirement the code no longer meets, with the record's words, the line, and the commit when the history shows one.** A count of commits is a reason to look, never a finding by itself.
|
|
89
93
|
- **A mid-rollout pair of records is a missing cross-reference, not a contradiction.**
|
|
90
94
|
- **The reply fits the size asked for (a stated limit is obeyed exactly), and never mentions a script, a graph or a tool.**
|
|
91
95
|
- **Statuses, links and governed paths come from the script's output**, or are marked as read by hand when it couldn't run.
|
|
@@ -96,6 +100,7 @@ the code itself is `reverse-architecture`.
|
|
|
96
100
|
- ✅ "Why do we use X?" or "Why is this service built this way?" in a repo with ADRs or RFCs
|
|
97
101
|
- ✅ "Which decisions govern this file, folder or service before I change it?"
|
|
98
102
|
- ✅ "Are our ADRs still current?" or "Find the stale or contradictory decisions"
|
|
103
|
+
- ✅ "Has the code drifted from what we decided?" or "What changed in this service since the ADR, and who changed it?"
|
|
99
104
|
- ✅ A map of a codebase's architecture decisions for onboarding or a review
|
|
100
105
|
|
|
101
106
|
## When NOT to use this skill
|
|
@@ -115,5 +120,6 @@ the code itself is `reverse-architecture`.
|
|
|
115
120
|
- ❌ **Passing on a script warning unread.** "Both govern the same path" is a prompt to read both, not a conflict.
|
|
116
121
|
- ❌ **Answering "can I remove it?" from the records alone.** The uses nobody recorded are the ones that break.
|
|
117
122
|
- ❌ **Taking the record's word for it.** A decision the code no longer follows is the most useful thing to find; read the files.
|
|
123
|
+
- ❌ **Calling it drift because the code changed.** Most commits since a decision implement or refine it. Only a requirement the code no longer meets is drift.
|
|
118
124
|
- ❌ **"Gone" for a folder that was renamed.** Look for where it went before telling someone the decision is dead.
|
|
119
125
|
- ❌ **Telling the reader how you found it.** "The script shows" or "the graph says" means nothing to them; say what the records and the code say.
|
|
@@ -7,5 +7,8 @@
|
|
|
7
7
|
{"query": "We just decided to move our search from Postgres full-text to a hosted search engine. Write the ADR for it in MADR format", "should_trigger": false},
|
|
8
8
|
{"query": "I start on the checkout repo on Monday. How do I run it locally, run the tests, and where's the code for applying discount codes?", "should_trigger": false},
|
|
9
9
|
{"query": "There are no design docs for this monolith. Work out its architecture from the code and the docker-compose file and draw me a C4 diagram", "should_trigger": false},
|
|
10
|
-
{"query": "Here are the notes from this morning's pricing steering meeting. Pull out what was decided, who owns each action and when it's due", "should_trigger": false}
|
|
10
|
+
{"query": "Here are the notes from this morning's pricing steering meeting. Pull out what was decided, who owns each action and when it's due", "should_trigger": false},
|
|
11
|
+
{"query": "ADR-0009 says order events go through one SQS queue per tenant. Has the orders service drifted from that since we decided it, and who changed it?", "should_trigger": true},
|
|
12
|
+
{"query": "Before I rewrite the billing batch job, what decisions did we record about it, and has anyone changed the code since without updating them?", "should_trigger": true},
|
|
13
|
+
{"query": "Show me who committed to services/orders in the last month and summarise what changed", "should_trigger": false}
|
|
11
14
|
]
|
|
@@ -220,6 +220,36 @@
|
|
|
220
220
|
"Delivers the map itself as one pasteable markdown block (e.g. a table of ID, title, store, status, date, replaced by/replaces) with the issues in a short separate list, rather than a long narrative or per-file walkthrough; any check that needs git history (e.g. whether ADR 7 was ever implemented) is given as a command to run, not as a claimed result.",
|
|
221
221
|
"Invents nothing not in the input: no ADR numbers, dates, databases, services, sizes or commit details beyond what the files in doc/adr/, services/, deploy/ and web/ contain."
|
|
222
222
|
]
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"id": 16,
|
|
226
|
+
"held_out": "2026-10-22",
|
|
227
|
+
"prompt": "I'm about to rework how payments handles client retries. ADR-0004 is the idempotency decision. Is the code in services/payments still doing what it says? If anything has drifted, which commit did it and who? Keep it short, I just need to know what I can safely assume before I start.",
|
|
228
|
+
"assertions": [
|
|
229
|
+
"Identifies the real drift: services/payments/idempotency.py no longer compares the stored body fingerprint in lookup(), so a request that reuses an Idempotency-Key with a different body gets the cached response back instead of a 422. That breaks ADR-0004 rule 4, and the response points out that the 422 branch in services/payments/api/create.py can no longer fire because IdempotencyConflict is never raised.",
|
|
230
|
+
"Pins the drift to the 2025-08-19 commit 'refactor: simplify idempotency store lookups' by Dmitri Vasquez, notes that its message says 'No behaviour change intended' even though behaviour did change, and notes that the same commit skipped test_mismatched_body_raises_conflict in tests/test_idempotency.py (reason 'flaky since lookup refactor, see PAY-311').",
|
|
231
|
+
"Does NOT report the 48-hour TTL (TTL_SECONDS = 48 * 60 * 60, i.e. 172800) as drift from ADR-0004's 24 hours. It recognises that ADR-0007 (2025-06-02, Accepted) amends rule 3 and that Lena Hargrove's 2025-06-04 commit implemented it.",
|
|
232
|
+
"Points out that the amendment is recorded on one side only: ADR-0007 says 'Amends: ADR-0004', but ADR-0004 still reads 'Status: Accepted' with 24 hours and no 'Amended by ADR-0007', even though ADR-0001 requires both sides to say so.",
|
|
233
|
+
"Confirms what still holds, so the user knows what is safe to assume: a missing Idempotency-Key is still rejected with 400, records are still scoped per merchant (key idem:{merchant_id}:{key}) and stored in Redis with a TTL, and ADR-0007 says all other ADR-0004 rules are unchanged, so rule 4 is still meant to apply.",
|
|
234
|
+
"Treats the later commits correctly as legitimate or neutral: Priya Natarajan's 2025-03-10 implementation commit, Tomás Okafor's 2025-03-12 tests and his 2025-09-30 debug-logging commit are not flagged as drift.",
|
|
235
|
+
"Answers the question directly and compactly: it starts with whether the code matches ADR-0004 (no, one rule is broken) plus the commit and author, and recommends restoring the comparison and un-skipping the test, or else writing a superseding ADR if dropping rule 4 was intended. It does not pad the answer with a full catalogue of unrelated ADRs (0001-0003).",
|
|
236
|
+
"Invents nothing: every commit, date, author, file, constant and ticket it cites (including PAY-311, which it does not claim to know the contents of) exists in the repository or its git history, and it invents no other ADRs, incidents or email addresses."
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"id": 17,
|
|
241
|
+
"held_out": "2026-10-22",
|
|
242
|
+
"prompt": "hey, joined the platform team last week and i'm inheriting the notifications service. someone told me \"half our ADRs are lies\" lol. which of the decision docs can i actually trust for notifications, and is anything in there just wrong? repo's checked out here",
|
|
243
|
+
"assertions": [
|
|
244
|
+
"Gives a direct trust verdict for each decision record that governs notifications: ADR-0002, ADR-0003, ADR-0004, ADR-0005, ADR-0006, ADR-0007 and RFC-012 (via services/*/retry.py). Each one gets its status and a short trust / don't trust / partly answer, rather than a generic essay about ADR hygiene.",
|
|
245
|
+
"Flags ADR-0003 (RabbitMQ) as superseded on one side only: ADR-0007 says 'Supersedes: ADR-0003', but ADR-0003 still says 'Status: Accepted', breaking the both-sides rule in ADR-0001. It confirms from services/notifications/consumer.py (KafkaConsumer, topics user-events and invoice-events, group notifications) that Kafka is what actually runs, so ADR-0003 should not be trusted.",
|
|
246
|
+
"Flags that ADR-0006 (5 attempts, 2s base) and RFC-012 (3 attempts, 1s base, full jitter, 30s cap) are both Accepted, both govern services/notifications/retry.py, and contradict each other, with neither recording that it supersedes the other. It notes that notifications' retry.py follows ADR-0006 (MAX_ATTEMPTS = 5, BASE_DELAY_SECONDS = 2) while services/billing/retry.py follows RFC-012, and it leaves the conflict for the team to settle and record instead of quietly declaring one the winner.",
|
|
247
|
+
"Flags that ADR-0005 governs services/notifications/templates/, which no longer exists (the templates are in packages/notification-templates/, per its README), and gets the nuance right: rendering is still server-side via services/notifications/render.py with Jinja2 autoescape, so the decision itself still holds and only its governed path is stale.",
|
|
248
|
+
"Flags that ADR-0007 points to 'RFC-015 for the migration plan and cutover dates', but no RFC-015 exists in docs/rfcs/ (only RFC-012).",
|
|
249
|
+
"Does not flag the ADR-0002 to ADR-0004 supersession, which is recorded on both sides. It confirms that services/notifications/email/sender.py uses the provider HTTPS API and that dispatch.py logs the provider message id, so ADR-0004 can be trusted.",
|
|
250
|
+
"Says plainly that the checkout has no git history, so it cannot say when or by whom the code moved away from a record. It gives the commands the user could run to find out (for example git log --follow on services/notifications/retry.py, or git log on the templates move).",
|
|
251
|
+
"Invents nothing: no record, status, date, person, path, topic name or number beyond what the files contain. In particular it does not make up what RFC-015 says or a reason why the templates were moved beyond the README's stated reason (billing previews)."
|
|
252
|
+
]
|
|
223
253
|
}
|
|
224
254
|
]
|
|
225
255
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# ADR-0001: Record architecture decisions
|
|
2
|
+
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Date: 2025-01-14
|
|
5
|
+
Deciders: Priya Natarajan
|
|
6
|
+
|
|
7
|
+
## Decision
|
|
8
|
+
|
|
9
|
+
We record significant decisions as numbered ADRs in `docs/adr/`. An ADR that is
|
|
10
|
+
replaced or changed by a later one says so in its Status line ("Superseded by
|
|
11
|
+
ADR-NNNN" or "Amended by ADR-NNNN"), and the later ADR says what it supersedes
|
|
12
|
+
or amends.
|
package/packs/core/skills/context-graph/evals/files/16/docs/adr/0002-postgres-for-the-ledger.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# ADR-0002: Postgres for the payment ledger
|
|
2
|
+
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Date: 2025-01-20
|
|
5
|
+
Deciders: Priya Natarajan, Tomás Okafor
|
|
6
|
+
Governs: services/payments/ledger.py
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
The ledger of record is a single Postgres database. Every payment is written in
|
|
11
|
+
one transaction to `payments` and `ledger_entries`.
|
package/packs/core/skills/context-graph/evals/files/16/docs/adr/0003-redis-for-ephemeral-state.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# ADR-0003: Redis for short-lived state
|
|
2
|
+
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Date: 2025-02-03
|
|
5
|
+
Deciders: Tomás Okafor
|
|
6
|
+
Governs: services/payments/redis_client.py
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Short-lived, reconstructable state (rate-limit counters, idempotency records)
|
|
11
|
+
lives in Redis with an explicit TTL on every key. Nothing in Redis is the
|
|
12
|
+
system of record.
|
|
@@ -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",
|