@1aboveio/skills 0.0.0-stage → 1.0.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/README.md +30 -2
- package/bin/cli.mjs +14 -0
- package/bin/select-core.mjs +172 -0
- package/package.json +146 -4
- package/skills/app-dev/airflow/LICENSE +3 -0
- package/skills/app-dev/airflow/SKILL.md +199 -0
- package/skills/app-dev/airflow/release-check.mjs +93 -0
- package/skills/app-dev/android-foundation/LICENSE +3 -0
- package/skills/app-dev/android-foundation/SKILL.md +192 -0
- package/skills/app-dev/android-foundation/assets/android-ci-job.yml +217 -0
- package/skills/app-dev/android-foundation/references/ci.md +87 -0
- package/skills/app-dev/android-foundation/references/gotchas.md +221 -0
- package/skills/app-dev/android-foundation/references/setup.md +164 -0
- package/skills/app-dev/android-foundation/release-check.mjs +93 -0
- package/skills/app-dev/android-foundation/scripts/device-run.sh +176 -0
- package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +172 -0
- package/skills/app-dev/app-debug/LICENSE +3 -0
- package/skills/app-dev/app-debug/SKILL.md +110 -0
- package/skills/app-dev/app-debug/references/common-errors.md +128 -0
- package/skills/app-dev/app-debug/release-check.mjs +93 -0
- package/skills/app-dev/better-auth/LICENSE +3 -0
- package/skills/app-dev/better-auth/SKILL.md +598 -0
- package/skills/app-dev/better-auth/references/feishu-api.md +270 -0
- package/skills/app-dev/better-auth/release-check.mjs +93 -0
- package/skills/app-dev/data-catalog/LICENSE +3 -0
- package/skills/app-dev/data-catalog/SKILL.md +204 -0
- package/skills/app-dev/data-catalog/agents/openai.yaml +3 -0
- package/skills/app-dev/data-catalog/references/data-catalog-conventions.md +75 -0
- package/skills/app-dev/data-catalog/references/field-definitions.md +116 -0
- package/skills/app-dev/data-catalog/references/format.md +121 -0
- package/skills/app-dev/data-catalog/release-check.mjs +93 -0
- package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +36 -0
- package/skills/app-dev/data-catalog/scripts/data_catalog.py +1392 -0
- package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +117 -0
- package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +17 -0
- package/skills/app-dev/data-catalog/scripts/test_data_catalog.py +571 -0
- package/skills/app-dev/ios-foundation/LICENSE +3 -0
- package/skills/app-dev/ios-foundation/SKILL.md +184 -0
- package/skills/app-dev/ios-foundation/references/setup.md +243 -0
- package/skills/app-dev/ios-foundation/release-check.mjs +93 -0
- package/skills/app-dev/monorepo/LICENSE +3 -0
- package/skills/app-dev/monorepo/SKILL.md +462 -0
- package/skills/app-dev/monorepo/release-check.mjs +93 -0
- package/skills/app-dev/nextjs-fullstack/LICENSE +3 -0
- package/skills/app-dev/nextjs-fullstack/SKILL.md +211 -0
- package/skills/app-dev/nextjs-fullstack/conventions.md +318 -0
- package/skills/app-dev/nextjs-fullstack/frontend-conventions.md +61 -0
- package/skills/app-dev/nextjs-fullstack/nextjs16.md +287 -0
- package/skills/app-dev/nextjs-fullstack/release-check.mjs +93 -0
- package/skills/app-dev/nextjs-fullstack/server-actions.md +409 -0
- package/skills/app-dev/prisma-setup/LICENSE +3 -0
- package/skills/app-dev/prisma-setup/SKILL.md +181 -0
- package/skills/app-dev/prisma-setup/nextjs.md +258 -0
- package/skills/app-dev/prisma-setup/release-check.mjs +93 -0
- package/skills/app-dev/prisma-setup/turborepo.md +301 -0
- package/skills/app-dev/pyspark/LICENSE +3 -0
- package/skills/app-dev/pyspark/SKILL.md +127 -0
- package/skills/app-dev/pyspark/assets/templates/etl.py +748 -0
- package/skills/app-dev/pyspark/assets/templates/utils/__init__.py +1 -0
- package/skills/app-dev/pyspark/assets/templates/utils/hudi_metadata.py +14 -0
- package/skills/app-dev/pyspark/references/diagnosis-and-profiling.md +169 -0
- package/skills/app-dev/pyspark/references/etl-contract.md +129 -0
- package/skills/app-dev/pyspark/references/production-validation.md +131 -0
- package/skills/app-dev/pyspark/references/reconciliation.md +38 -0
- package/skills/app-dev/pyspark/references/transformation-design.md +190 -0
- package/skills/app-dev/pyspark/references/velocity-feature-calculation.md +193 -0
- package/skills/app-dev/pyspark/release-check.mjs +93 -0
- package/skills/app-dev/pyspark/scripts/spark_eventlog_summary.py +223 -0
- package/skills/app-dev/python-backend/LICENSE +3 -0
- package/skills/app-dev/python-backend/SKILL.md +325 -0
- package/skills/app-dev/python-backend/assets/uv-install.sh +48 -0
- package/skills/app-dev/python-backend/references/uv-installs.md +83 -0
- package/skills/app-dev/python-backend/release-check.mjs +93 -0
- package/skills/app-dev/shadcn/LICENSE +3 -0
- package/skills/app-dev/shadcn/SKILL.md +120 -0
- package/skills/app-dev/shadcn/assets/shadcn-small.png +0 -0
- package/skills/app-dev/shadcn/assets/shadcn.png +0 -0
- package/skills/app-dev/shadcn/cli.md +411 -0
- package/skills/app-dev/shadcn/customization.md +224 -0
- package/skills/app-dev/shadcn/mcp.md +101 -0
- package/skills/app-dev/shadcn/release-check.mjs +93 -0
- package/skills/app-dev/shadcn/rules/base-vs-radix.md +323 -0
- package/skills/app-dev/shadcn/rules/component-selection.md +67 -0
- package/skills/app-dev/shadcn/rules/composition.md +195 -0
- package/skills/app-dev/shadcn/rules/data-table.md +201 -0
- package/skills/app-dev/shadcn/rules/forms.md +255 -0
- package/skills/app-dev/shadcn/rules/icons.md +103 -0
- package/skills/app-dev/shadcn/rules/styling.md +167 -0
- package/skills/app-dev/zod-v4/LICENSE +3 -0
- package/skills/app-dev/zod-v4/SKILL.md +286 -0
- package/skills/app-dev/zod-v4/release-check.mjs +93 -0
- package/skills/cicd/cloud-build/LICENSE +3 -0
- package/skills/cicd/cloud-build/SKILL.md +707 -0
- package/skills/cicd/cloud-build/release-check.mjs +93 -0
- package/skills/cicd/cloud-debug/LICENSE +3 -0
- package/skills/cicd/cloud-debug/SKILL.md +315 -0
- package/skills/cicd/cloud-debug/references/build-failures.md +238 -0
- package/skills/cicd/cloud-debug/references/deploy-failures.md +376 -0
- package/skills/cicd/cloud-debug/references/pipeline-failures.md +378 -0
- package/skills/cicd/cloud-debug/release-check.mjs +93 -0
- package/skills/cicd/cloud-deploy/LICENSE +3 -0
- package/skills/cicd/cloud-deploy/SKILL.md +230 -0
- package/skills/cicd/cloud-deploy/references/config-templates.md +257 -0
- package/skills/cicd/cloud-deploy/release-check.mjs +93 -0
- package/skills/cicd/docker/LICENSE +3 -0
- package/skills/cicd/docker/SKILL.md +151 -0
- package/skills/cicd/docker/release-check.mjs +93 -0
- package/skills/cicd/github-actions/LICENSE +3 -0
- package/skills/cicd/github-actions/SKILL.md +111 -0
- package/skills/cicd/github-actions/references/audit-profile-optimize.md +150 -0
- package/skills/cicd/github-actions/references/ci-case-studies.md +141 -0
- package/skills/cicd/github-actions/references/conditional-ci.md +214 -0
- package/skills/cicd/github-actions/references/debugging.md +13 -0
- package/skills/cicd/github-actions/references/pipeline.md +87 -0
- package/skills/cicd/github-actions/references/post-deploy-and-publishing.md +33 -0
- package/skills/cicd/github-actions/references/runners.md +253 -0
- package/skills/cicd/github-actions/references/security.md +29 -0
- package/skills/cicd/github-actions/references/speed-and-audit.md +115 -0
- package/skills/cicd/github-actions/references/two-pass-reading.md +92 -0
- package/skills/cicd/github-actions/release-check.mjs +93 -0
- package/skills/cicd/github-actions/scripts/ci-run-audit-core.mjs +612 -0
- package/skills/cicd/github-actions/scripts/ci-run-audit.mjs +4 -0
- package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +637 -0
- package/skills/cicd/github-flow/LICENSE +3 -0
- package/skills/cicd/github-flow/SKILL.md +123 -0
- package/skills/cicd/github-flow/references/ci-conventions.md +14 -0
- package/skills/cicd/github-flow/release-check.mjs +93 -0
- package/skills/cicd/github-flow/scripts/flow-core.mjs +105 -0
- package/skills/cicd/github-flow/scripts/flow.mjs +81 -0
- package/skills/cicd/github-flow/scripts/flow.test.mjs +114 -0
- package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +117 -0
- package/skills/cicd/github-flow/scripts/persist-conventions.mjs +49 -0
- package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +108 -0
- package/skills/cicd/google-cloud/LICENSE +3 -0
- package/skills/cicd/google-cloud/SKILL.md +119 -0
- package/skills/cicd/google-cloud/references/gcs.md +469 -0
- package/skills/cicd/google-cloud/references/iam.md +451 -0
- package/skills/cicd/google-cloud/references/project.md +349 -0
- package/skills/cicd/google-cloud/references/secrets.md +336 -0
- package/skills/cicd/google-cloud/references/vpc.md +312 -0
- package/skills/cicd/google-cloud/release-check.mjs +93 -0
- package/skills/cicd/google-cloud/scripts/create-sa.sh +36 -0
- package/skills/cicd/google-cloud/scripts/gcp-config.sh +31 -0
- package/skills/cicd/google-cloud/scripts/grant-iap.sh +41 -0
- package/skills/cicd/google-cloud/scripts/setup-secrets.sh +48 -0
- package/skills/cicd/mergify/LICENSE +3 -0
- package/skills/cicd/mergify/SKILL.md +102 -0
- package/skills/cicd/mergify/assets/templates/mergify.yml +215 -0
- package/skills/cicd/mergify/assets/templates/ruleset.json +46 -0
- package/skills/cicd/mergify/references/branch-protection.md +282 -0
- package/skills/cicd/mergify/references/configuration.md +274 -0
- package/skills/cicd/mergify/references/diagnosis.md +109 -0
- package/skills/cicd/mergify/references/stacks.md +84 -0
- package/skills/cicd/mergify/references/traps.md +119 -0
- package/skills/cicd/mergify/references/watch-contract.md +274 -0
- package/skills/cicd/mergify/release-check.mjs +93 -0
- package/skills/cicd/mergify/scripts/audit-core.mjs +208 -0
- package/skills/cicd/mergify/scripts/audit.mjs +4 -0
- package/skills/cicd/mergify/scripts/audit.test.mjs +201 -0
- package/skills/cicd/mergify/scripts/entrypoints.test.mjs +106 -0
- package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +30 -0
- package/skills/cicd/mergify/scripts/stack-push-core.mjs +258 -0
- package/skills/cicd/mergify/scripts/stack-push.mjs +4 -0
- package/skills/cicd/mergify/scripts/stack-push.test.mjs +233 -0
- package/skills/cicd/mergify/scripts/watch-pr-delivery-core.mjs +1065 -0
- package/skills/cicd/mergify/scripts/watch-pr-delivery.mjs +4 -0
- package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +1826 -0
- package/skills/cicd/podman/LICENSE +3 -0
- package/skills/cicd/podman/SKILL.md +71 -0
- package/skills/cicd/podman/agents/openai.yaml +4 -0
- package/skills/cicd/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
- package/skills/cicd/podman/assets/templates/podman-service-override.conf +3 -0
- package/skills/cicd/podman/references/compose-compatibility.md +70 -0
- package/skills/cicd/podman/references/networking-and-ports.md +74 -0
- package/skills/cicd/podman/references/rootless-services-and-sockets.md +156 -0
- package/skills/cicd/podman/references/troubleshooting.md +98 -0
- package/skills/cicd/podman/release-check.mjs +93 -0
- package/skills/content/article-review/LICENSE +3 -0
- package/skills/content/article-review/SKILL.md +66 -0
- package/skills/content/article-review/references/report-format.md +68 -0
- package/skills/content/article-review/references/template-guide.md +79 -0
- package/skills/content/article-review/references/template-wechat-payments.md +125 -0
- package/skills/content/article-review/release-check.mjs +93 -0
- package/skills/content/article-to-image/LICENSE +3 -0
- package/skills/content/article-to-image/SKILL.md +91 -0
- package/skills/content/article-to-image/references/formats.md +53 -0
- package/skills/content/article-to-image/release-check.mjs +93 -0
- package/skills/content/flowchart-svg/LICENSE +3 -0
- package/skills/content/flowchart-svg/SKILL.md +43 -0
- package/skills/content/flowchart-svg/release-check.mjs +93 -0
- package/skills/content/flowchart-svg/scripts/export_png.sh +8 -0
- package/skills/content/image-gen/LICENSE +3 -0
- package/skills/content/image-gen/SKILL.md +192 -0
- package/skills/content/image-gen/release-check.mjs +93 -0
- package/skills/content/image-gen/scripts/credentials.mjs +217 -0
- package/skills/content/image-gen/scripts/credentials.test.mjs +212 -0
- package/skills/content/image-gen/scripts/generate.mjs +240 -0
- package/skills/content/image-gen/scripts/options.mjs +293 -0
- package/skills/content/image-gen/scripts/options.test.mjs +245 -0
- package/skills/content/mermaid/LICENSE +3 -0
- package/skills/content/mermaid/SKILL.md +635 -0
- package/skills/content/mermaid/release-check.mjs +93 -0
- package/skills/content/wechat-article-writer/LICENSE +3 -0
- package/skills/content/wechat-article-writer/SKILL.md +92 -0
- package/skills/content/wechat-article-writer/references/style-selector.md +133 -0
- package/skills/content/wechat-article-writer/references/writing-rules.md +152 -0
- package/skills/content/wechat-article-writer/release-check.mjs +93 -0
- package/skills/content/wechat-article-writer/templates/wechat-comparison.md +52 -0
- package/skills/content/wechat-article-writer/templates/wechat-howto.md +51 -0
- package/skills/content/wechat-article-writer/templates/wechat-insight.md +50 -0
- package/skills/content/wechat-article-writer/templates/wechat-pitfall.md +52 -0
- package/skills/content/wechat-publisher/LICENSE +3 -0
- package/skills/content/wechat-publisher/SKILL.md +171 -0
- package/skills/content/wechat-publisher/references/api.md +105 -0
- package/skills/content/wechat-publisher/references/layout-rules.md +57 -0
- package/skills/content/wechat-publisher/references/wechat-safe-template.html +86 -0
- package/skills/content/wechat-publisher/release-check.mjs +93 -0
- package/skills/content/wechat-publisher/scripts/md_to_wechat_html.py +194 -0
- package/skills/content/wechat-publisher/scripts/publish.py +239 -0
- package/skills/content/wechat-search/LICENSE +3 -0
- package/skills/content/wechat-search/SKILL.md +211 -0
- package/skills/content/wechat-search/release-check.mjs +93 -0
- package/skills/content/wechat-search/scripts/start-chrome.sh +83 -0
- package/skills/content/xai-search/LICENSE +3 -0
- package/skills/content/xai-search/SKILL.md +79 -0
- package/skills/content/xai-search/release-check.mjs +93 -0
- package/skills/content/xai-search/scripts/xai_search.sh +187 -0
- package/skills/content/xai-search/tests/smoke_test.sh +36 -0
- package/skills/core/distill-writing/LICENSE +3 -0
- package/skills/core/distill-writing/SKILL.md +74 -0
- package/skills/core/distill-writing/references/extraction.md +95 -0
- package/skills/core/distill-writing/references/updating.md +46 -0
- package/skills/core/distill-writing/release-check.mjs +93 -0
- package/skills/core/doc-to-md/LICENSE +3 -0
- package/skills/core/doc-to-md/SKILL.md +78 -0
- package/skills/core/doc-to-md/release-check.mjs +93 -0
- package/skills/core/doc-to-md/scripts/convert.py +157 -0
- package/skills/core/editorial-review/LICENSE +3 -0
- package/skills/core/editorial-review/SKILL.md +79 -0
- package/skills/core/editorial-review/references/checklist.md +94 -0
- package/skills/core/editorial-review/release-check.mjs +93 -0
- package/skills/core/house-style/LICENSE +3 -0
- package/skills/core/house-style/SKILL.md +33 -0
- package/skills/core/house-style/references/defaults.md +66 -0
- package/skills/core/house-style/references/rules.md +267 -0
- package/skills/core/house-style/references/sources.md +27 -0
- package/skills/core/house-style/references/style-en.md +131 -0
- package/skills/core/house-style/references/style-zh.md +141 -0
- package/skills/core/house-style/references/terminology-zh.md +223 -0
- package/skills/core/house-style/release-check.mjs +93 -0
- package/skills/core/house-style/templates/brd.md +103 -0
- package/skills/core/house-style/templates/commercial-proposal.md +93 -0
- package/skills/core/house-style/templates/compliance-policy.md +109 -0
- package/skills/core/house-style/templates/gap-analysis.md +105 -0
- package/skills/core/house-style/templates/general.md +63 -0
- package/skills/core/house-style/templates/integration-guide.md +98 -0
- package/skills/core/house-style/templates/prd.md +81 -0
- package/skills/core/house-style/templates/risk-operations-procedure.md +94 -0
- package/skills/core/house-style/templates/technical-spec.md +96 -0
- package/skills/core/polish-cn/LICENSE +3 -0
- package/skills/core/polish-cn/SKILL.md +102 -0
- package/skills/core/polish-cn/references/brief.md +93 -0
- package/skills/core/polish-cn/references/models.md +76 -0
- package/skills/core/polish-cn/release-check.mjs +93 -0
- package/skills/core/polish-cn/scripts/polish-cn.test.mjs +244 -0
- package/skills/core/polish-cn/scripts/polish_blocks.py +489 -0
- package/skills/core/reg-parser/LICENSE +3 -0
- package/skills/core/reg-parser/SKILL.md +212 -0
- package/skills/core/reg-parser/references/checklist-schema.md +223 -0
- package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.json +1306 -0
- package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.md +469 -0
- package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.json +634 -0
- package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.md +233 -0
- package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.json +2194 -0
- package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.md +771 -0
- package/skills/core/reg-parser/references/checklists/kyc-aml-checklist.md +258 -0
- package/skills/core/reg-parser/references/checklists/visa-vars-checklist.md +1058 -0
- package/skills/core/reg-parser/references/mandates/hk-amlo-cap615.md +389 -0
- package/skills/core/reg-parser/references/mandates/visa-vars.md +205 -0
- package/skills/core/reg-parser/release-check.mjs +93 -0
- package/skills/core/reg-parser/scripts/compare_checklist.py +230 -0
- package/skills/core/reg-parser/scripts/dump_checklist.py +361 -0
- package/skills/core/reg-parser/scripts/preprocess-mandate.sh +187 -0
- package/skills/core/reg-parser/scripts/reg-parser.test.mjs +165 -0
- package/skills/core/reg-parser/scripts/test_quarantine.sh +105 -0
- package/skills/core/reg-parser/scripts/validate_checklist.py +563 -0
- package/skills/core/reg-parser/scripts/validate_mandate.py +273 -0
- package/skills/core/terminology/LICENSE +3 -0
- package/skills/core/terminology/SKILL.md +62 -0
- package/skills/core/terminology/references/format.md +50 -0
- package/skills/core/terminology/references/init.md +32 -0
- package/skills/core/terminology/release-check.mjs +93 -0
- package/skills/core/write-doc/LICENSE +3 -0
- package/skills/core/write-doc/SKILL.md +141 -0
- package/skills/core/write-doc/references/bilingual.md +60 -0
- package/skills/core/write-doc/release-check.mjs +93 -0
- package/skills/engineering/better-review/LICENSE +3 -0
- package/skills/engineering/better-review/SKILL.md +249 -0
- package/skills/engineering/better-review/agents/openai.yaml +9 -0
- package/skills/engineering/better-review/references/domain-hazards.md +53 -0
- package/skills/engineering/better-review/references/passes.md +147 -0
- package/skills/engineering/better-review/references/result.md +151 -0
- package/skills/engineering/better-review/release-check.mjs +93 -0
- package/skills/engineering/better-review/scripts/pr-context.mjs +212 -0
- package/skills/engineering/better-review/scripts/pr-context.test.mjs +168 -0
- package/skills/engineering/better-review/scripts/rereview-scope.mjs +503 -0
- package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +587 -0
- package/skills/engineering/better-review/scripts/scan-diff.mjs +365 -0
- package/skills/engineering/better-review/scripts/scan-diff.test.mjs +414 -0
- package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +96 -0
- package/skills/engineering/e2e-test/LICENSE +3 -0
- package/skills/engineering/e2e-test/SKILL.md +98 -0
- package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +24 -0
- package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-config.md +73 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +144 -0
- package/skills/engineering/e2e-test/references/ci-integration.md +25 -0
- package/skills/engineering/e2e-test/references/ci-playwright-container.md +290 -0
- package/skills/engineering/e2e-test/references/presentation-sweep.md +104 -0
- package/skills/engineering/e2e-test/references/reviewing.md +41 -0
- package/skills/engineering/e2e-test/references/route-manifest.md +51 -0
- package/skills/engineering/e2e-test/release-check.mjs +93 -0
- package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
- package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
- package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +106 -0
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +117 -0
- package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
- package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +69 -0
- package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
- package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +98 -0
- package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +147 -0
- package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +71 -0
- package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
- package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +59 -0
- package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +82 -0
- package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +76 -0
- package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
- package/skills/engineering/engineering-runtime/LICENSE +3 -0
- package/skills/engineering/engineering-runtime/SKILL.md +11 -0
- package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/engineering-runtime/coherence/workflow.json +597 -0
- package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +68 -0
- package/skills/engineering/engineering-runtime/release-check.mjs +93 -0
- package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
- package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.mjs +187 -0
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +95 -0
- package/skills/engineering/engineering-runtime/scripts/main-module.mjs +79 -0
- package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +288 -0
- package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
- package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
- package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +96 -0
- package/skills/engineering/engineering-runtime/scripts/review-completion.mjs +56 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +595 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +189 -0
- package/skills/engineering/engineering-runtime/scripts/yaml-mapping.mjs +18 -0
- package/skills/engineering/ensure-coverage/LICENSE +3 -0
- package/skills/engineering/ensure-coverage/SKILL.md +129 -0
- package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +92 -0
- package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +85 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
- package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
- package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +19 -0
- package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
- package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +57 -0
- package/skills/engineering/ensure-coverage/references/depth/test-smells.md +39 -0
- package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +44 -0
- package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +110 -0
- package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +35 -0
- package/skills/engineering/ensure-coverage/references/presentation/axis.md +64 -0
- package/skills/engineering/ensure-coverage/references/process/audit-mode.md +27 -0
- package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
- package/skills/engineering/ensure-coverage/references/process/test-strategy.md +56 -0
- package/skills/engineering/ensure-coverage/release-check.mjs +93 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +67 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +140 -0
- package/skills/engineering/ensure-coverage/scripts/README.md +99 -0
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +182 -0
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +96 -0
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +105 -0
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +47 -0
- package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +137 -0
- package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +86 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +674 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +447 -0
- package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
- package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +552 -0
- package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +282 -0
- package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +289 -0
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +119 -0
- package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
- package/skills/engineering/golden-path/LICENSE +3 -0
- package/skills/engineering/golden-path/SKILL.md +230 -0
- package/skills/engineering/golden-path/agents/openai.yaml +3 -0
- package/skills/engineering/golden-path/assets/golden-paths.example.json +57 -0
- package/skills/engineering/golden-path/references/contract.md +239 -0
- package/skills/engineering/golden-path/references/migration.md +111 -0
- package/skills/engineering/golden-path/release-check.mjs +93 -0
- package/skills/engineering/golden-path/scripts/golden-path-core.mjs +1779 -0
- package/skills/engineering/golden-path/scripts/golden-path.mjs +6 -0
- package/skills/engineering/golden-path/scripts/golden-path.test.mjs +1457 -0
- package/skills/engineering/harness-runtime/LICENSE +3 -0
- package/skills/engineering/harness-runtime/SKILL.md +19 -0
- package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
- package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
- package/skills/engineering/harness-runtime/contracts.md +15 -0
- package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
- package/skills/engineering/harness-runtime/discover-models.test.mjs +363 -0
- package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
- package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
- package/skills/engineering/harness-runtime/harness-runtime.test.mjs +967 -0
- package/skills/engineering/harness-runtime/index.mjs +767 -0
- package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +829 -0
- package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
- package/skills/engineering/harness-runtime/model-catalog.test.mjs +366 -0
- package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
- package/skills/engineering/harness-runtime/references/model-catalog-seed.json +408 -0
- package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
- package/skills/engineering/harness-runtime/release-check.mjs +93 -0
- package/skills/engineering/resolve-issues/LICENSE +3 -0
- package/skills/engineering/resolve-issues/SKILL.md +250 -0
- package/skills/engineering/resolve-issues/agents/openai.yaml +9 -0
- package/skills/engineering/resolve-issues/plugin-hooks.json +17 -0
- package/skills/engineering/resolve-issues/references/canary.md +28 -0
- package/skills/engineering/resolve-issues/references/ci.md +105 -0
- package/skills/engineering/resolve-issues/references/done.md +25 -0
- package/skills/engineering/resolve-issues/references/dropbox.md +90 -0
- package/skills/engineering/resolve-issues/references/expire.md +318 -0
- package/skills/engineering/resolve-issues/references/exploration.md +202 -0
- package/skills/engineering/resolve-issues/references/implementation.md +303 -0
- package/skills/engineering/resolve-issues/references/preflight.md +445 -0
- package/skills/engineering/resolve-issues/references/review.md +228 -0
- package/skills/engineering/resolve-issues/references/shared-modules.md +44 -0
- package/skills/engineering/resolve-issues/references/task-plan.md +264 -0
- package/skills/engineering/resolve-issues/release-check.mjs +93 -0
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.mjs +364 -0
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +280 -0
- package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/dropbox.mjs +473 -0
- package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +346 -0
- package/skills/engineering/resolve-issues/scripts/findings-reuse.mjs +98 -0
- package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +88 -0
- package/skills/engineering/resolve-issues/scripts/github-reviews.mjs +384 -0
- package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +264 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +135 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +79 -0
- package/skills/engineering/resolve-issues/scripts/issue-tree.mjs +162 -0
- package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +107 -0
- package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +14 -0
- package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +149 -0
- package/skills/engineering/resolve-issues/scripts/next-actions.mjs +856 -0
- package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +915 -0
- package/skills/engineering/resolve-issues/scripts/plan-sync.mjs +269 -0
- package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +156 -0
- package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +736 -0
- package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +448 -0
- package/skills/engineering/resolve-issues/scripts/ready-check.mjs +185 -0
- package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +217 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +98 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +91 -0
- package/skills/engineering/resolve-issues/scripts/review-verdict.mjs +509 -0
- package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +1060 -0
- package/skills/engineering/resolve-issues/scripts/size-check.mjs +492 -0
- package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +458 -0
- package/skills/engineering/resolve-release/LICENSE +3 -0
- package/skills/engineering/resolve-release/SKILL.md +215 -0
- package/skills/engineering/resolve-release/agents/openai.yaml +8 -0
- package/skills/engineering/resolve-release/flows/cloud-run-canary.md +13 -0
- package/skills/engineering/resolve-release/flows/cloud-run.md +7 -0
- package/skills/engineering/resolve-release/flows/root-tag-npm.md +13 -0
- package/skills/engineering/resolve-release/flows/workspace-npm.md +22 -0
- package/skills/engineering/resolve-release/references/canary.md +51 -0
- package/skills/engineering/resolve-release/references/candidate.md +41 -0
- package/skills/engineering/resolve-release/references/cut.md +131 -0
- package/skills/engineering/resolve-release/references/dark-launch.md +66 -0
- package/skills/engineering/resolve-release/references/flows.md +150 -0
- package/skills/engineering/resolve-release/references/preflight.md +154 -0
- package/skills/engineering/resolve-release/references/prepared-metadata.md +260 -0
- package/skills/engineering/resolve-release/references/promotion.md +82 -0
- package/skills/engineering/resolve-release/references/publish.md +232 -0
- package/skills/engineering/resolve-release/references/workspace-npm.md +103 -0
- package/skills/engineering/resolve-release/release-check.mjs +93 -0
- package/skills/engineering/resolve-release/scripts/apply.mjs +212 -0
- package/skills/engineering/resolve-release/scripts/apply.test.mjs +99 -0
- package/skills/engineering/resolve-release/scripts/dark-flags.mjs +134 -0
- package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +100 -0
- package/skills/engineering/resolve-release/scripts/flow.mjs +500 -0
- package/skills/engineering/resolve-release/scripts/flow.test.mjs +340 -0
- package/skills/engineering/resolve-release/scripts/green-head.mjs +231 -0
- package/skills/engineering/resolve-release/scripts/green-head.test.mjs +101 -0
- package/skills/engineering/resolve-release/scripts/npm-version-absence.mjs +138 -0
- package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +426 -0
- package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +659 -0
- package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +317 -0
- package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +291 -0
- package/skills/engineering/resolve-release/scripts/plan.mjs +806 -0
- package/skills/engineering/resolve-release/scripts/plan.test.mjs +428 -0
- package/skills/engineering/rush-issues/LICENSE +3 -0
- package/skills/engineering/rush-issues/SKILL.md +58 -0
- package/skills/engineering/rush-issues/agents/openai.yaml +8 -0
- package/skills/engineering/rush-issues/rehydrate.mjs +10 -0
- package/skills/engineering/rush-issues/release-check.mjs +93 -0
- package/skills/engineering/rush-release/LICENSE +3 -0
- package/skills/engineering/rush-release/SKILL.md +35 -0
- package/skills/engineering/rush-release/agents/openai.yaml +8 -0
- package/skills/engineering/rush-release/release-check.mjs +93 -0
- package/skills/engineering/test-quality/LICENSE +3 -0
- package/skills/engineering/test-quality/SKILL.md +79 -0
- package/skills/engineering/test-quality/references/audit.md +80 -0
- package/skills/engineering/test-quality/references/lint.md +24 -0
- package/skills/engineering/test-quality/references/principles.md +143 -0
- package/skills/engineering/test-quality/references/testing-conventions.md +9 -0
- package/skills/engineering/test-quality/release-check.mjs +93 -0
- package/skills/engineering/test-quality/scripts/lint-tests.mjs +574 -0
- package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +591 -0
- package/skills/engineering/test-quality/scripts/mock-policy-config.mjs +176 -0
- package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +119 -0
- package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +117 -0
- package/skills/engineering/test-quality/scripts/persist-conventions.mjs +17 -0
- package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +104 -0
- package/skills/risk-compliance/compliance-audit/LICENSE +3 -0
- package/skills/risk-compliance/compliance-audit/SKILL.md +439 -0
- package/skills/risk-compliance/compliance-audit/references/findings-schema.md +293 -0
- package/skills/risk-compliance/compliance-audit/references/normalization-rules.md +45 -0
- package/skills/risk-compliance/compliance-audit/references/vars-checklist.md +1068 -0
- package/skills/risk-compliance/compliance-audit/references/vars-findings-schema.md +398 -0
- package/skills/risk-compliance/compliance-audit/release-check.mjs +93 -0
- package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +14 -0
- package/skills/risk-compliance/compliance-audit/scripts/export_tracker_xlsx.py +221 -0
- package/skills/risk-compliance/compliance-audit/scripts/reconcile_tracker.py +458 -0
- package/skills/risk-compliance/compliance-audit/scripts/test_export_tracker_xlsx.py +97 -0
- package/skills/risk-compliance/compliance-audit/scripts/test_validate_tracker.py +157 -0
- package/skills/risk-compliance/compliance-audit/scripts/validate_tracker.py +829 -0
- package/skills/risk-compliance/contract-compare/LICENSE +3 -0
- package/skills/risk-compliance/contract-compare/SKILL.md +199 -0
- package/skills/risk-compliance/contract-compare/references/comparison-dimensions.md +209 -0
- package/skills/risk-compliance/contract-compare/release-check.mjs +93 -0
- package/skills/risk-compliance/contract-review/LICENSE +3 -0
- package/skills/risk-compliance/contract-review/SKILL.md +240 -0
- package/skills/risk-compliance/contract-review/references/review-criteria.md +223 -0
- package/skills/risk-compliance/contract-review/release-check.mjs +93 -0
- package/skills/risk-compliance/doc-authenticity/LICENSE +3 -0
- package/skills/risk-compliance/doc-authenticity/SKILL.md +160 -0
- package/skills/risk-compliance/doc-authenticity/agents/openai.yaml +8 -0
- package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +12 -0
- package/skills/risk-compliance/doc-authenticity/references/agreement.md +66 -0
- package/skills/risk-compliance/doc-authenticity/references/bank-statement.md +150 -0
- package/skills/risk-compliance/doc-authenticity/references/forensic-signals.md +157 -0
- package/skills/risk-compliance/doc-authenticity/references/image-signals.md +114 -0
- package/skills/risk-compliance/doc-authenticity/references/invoice.md +66 -0
- package/skills/risk-compliance/doc-authenticity/references/issuer-profiles.md +86 -0
- package/skills/risk-compliance/doc-authenticity/references/verdict-and-report.md +95 -0
- package/skills/risk-compliance/doc-authenticity/references/verdict.schema.json +81 -0
- package/skills/risk-compliance/doc-authenticity/release-check.mjs +93 -0
- package/skills/risk-compliance/doc-authenticity/scripts/image_forensics.py +294 -0
- package/skills/risk-compliance/doc-authenticity/scripts/pdf_forensics.py +401 -0
- package/skills/risk-compliance/doc-authenticity/scripts/validate_codes.py +224 -0
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.py +308 -0
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +212 -0
- package/skills/risk-compliance/fraud-analysis/LICENSE +3 -0
- package/skills/risk-compliance/fraud-analysis/SKILL.md +114 -0
- package/skills/risk-compliance/fraud-analysis/references/archetypes/authorized-payment-scam.md +41 -0
- package/skills/risk-compliance/fraud-analysis/references/archetypes/first-party-fraud.md +44 -0
- package/skills/risk-compliance/fraud-analysis/references/archetypes/third-party-fraud.md +27 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/bank-transfer.md +24 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/card-payment.md +30 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/payment-collection.md +20 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/payout.md +20 -0
- package/skills/risk-compliance/fraud-analysis/references/feature-engineering.md +158 -0
- package/skills/risk-compliance/fraud-analysis/references/mechanisms/account-takeover.md +36 -0
- package/skills/risk-compliance/fraud-analysis/references/report-rationale.md +45 -0
- package/skills/risk-compliance/fraud-analysis/references/report-template.md +190 -0
- package/skills/risk-compliance/fraud-analysis/references/review-checklist.md +175 -0
- package/skills/risk-compliance/fraud-analysis/references/taxonomy.md +79 -0
- package/skills/risk-compliance/fraud-analysis/references/terminology.md +108 -0
- package/skills/risk-compliance/fraud-analysis/references/workflow.md +175 -0
- package/skills/risk-compliance/fraud-analysis/release-check.mjs +93 -0
- package/skills/risk-compliance/payment-analysis/LICENSE +3 -0
- package/skills/risk-compliance/payment-analysis/SKILL.md +180 -0
- package/skills/risk-compliance/payment-analysis/references/auth-rate-actions.md +30 -0
- package/skills/risk-compliance/payment-analysis/references/chargebacks.md +208 -0
- package/skills/risk-compliance/payment-analysis/references/chinese-polish.md +51 -0
- package/skills/risk-compliance/payment-analysis/references/event-layers.md +127 -0
- package/skills/risk-compliance/payment-analysis/references/fx.md +59 -0
- package/skills/risk-compliance/payment-analysis/references/journey.md +79 -0
- package/skills/risk-compliance/payment-analysis/references/metrics.md +67 -0
- package/skills/risk-compliance/payment-analysis/references/report-template.md +119 -0
- package/skills/risk-compliance/payment-analysis/references/terminology.md +95 -0
- package/skills/risk-compliance/payment-analysis/references/visualization.md +56 -0
- package/skills/risk-compliance/payment-analysis/release-check.mjs +93 -0
- package/skills/risk-compliance/report-writing/LICENSE +3 -0
- package/skills/risk-compliance/report-writing/SKILL.md +220 -0
- package/skills/risk-compliance/report-writing/references/audit-report-template.md +121 -0
- package/skills/risk-compliance/report-writing/references/external-assessment-template.md +167 -0
- package/skills/risk-compliance/report-writing/references/management-report-template.md +145 -0
- package/skills/risk-compliance/report-writing/references/vars-report-template.md +224 -0
- package/skills/risk-compliance/report-writing/release-check.mjs +93 -0
- package/skills/risk-compliance/similar-domain/LICENSE +3 -0
- package/skills/risk-compliance/similar-domain/SKILL.md +203 -0
- package/skills/risk-compliance/similar-domain/references/entities.md +67 -0
- package/skills/risk-compliance/similar-domain/references/fingerprints.md +134 -0
- package/skills/risk-compliance/similar-domain/references/pivots.md +82 -0
- package/skills/risk-compliance/similar-domain/references/report-template.md +93 -0
- package/skills/risk-compliance/similar-domain/references/scoring.md +130 -0
- package/skills/risk-compliance/similar-domain/references/terminology.md +41 -0
- package/skills/risk-compliance/similar-domain/references/workflow.md +85 -0
- package/skills/risk-compliance/similar-domain/release-check.mjs +93 -0
- package/skills/risk-compliance/tld-detection/LICENSE +3 -0
- package/skills/risk-compliance/tld-detection/SKILL.md +245 -0
- package/skills/risk-compliance/tld-detection/agents/openai.yaml +9 -0
- package/skills/risk-compliance/tld-detection/references/chinese-polish.md +55 -0
- package/skills/risk-compliance/tld-detection/references/clean.md +51 -0
- package/skills/risk-compliance/tld-detection/references/clustering.md +235 -0
- package/skills/risk-compliance/tld-detection/references/decision.md +189 -0
- package/skills/risk-compliance/tld-detection/references/fx-conversion.md +232 -0
- package/skills/risk-compliance/tld-detection/references/report-template.md +272 -0
- package/skills/risk-compliance/tld-detection/references/streams.md +141 -0
- package/skills/risk-compliance/tld-detection/references/terminology.md +95 -0
- package/skills/risk-compliance/tld-detection/references/workflow.md +95 -0
- package/skills/risk-compliance/tld-detection/release-check.mjs +93 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.py +390 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +14 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.py +650 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +14 -0
- package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +134 -0
- package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +520 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Infer payment-pattern hypotheses inside a descriptor: pricing currency, preset menus, rate consistency, retail lists.
|
|
3
|
+
|
|
4
|
+
Discovery layer that runs before exact currency-amount-pair clustering. It reads a cleaned
|
|
5
|
+
authorization CSV plus a daily FX cache and, per descriptor and BIN-country group, reports:
|
|
6
|
+
|
|
7
|
+
* inferred wallet currency (the currency under which charges collapse onto the fewest peaks)
|
|
8
|
+
* the preset menu (peak set) and which known scheme it matches (10-scheme, 12-scheme)
|
|
9
|
+
* rate consistency: source-specific cross-rate tests for frozen-rate evidence
|
|
10
|
+
* a retail-list constant (list prices ending .99/.95/.98/.00 times one factor)
|
|
11
|
+
* off-menu groups (no peak structure)
|
|
12
|
+
* behaviour metrics: repeat cards, retry after decline, burst regularity, hour concentration,
|
|
13
|
+
address verification, 3-D Secure, prepaid share, issuer-program concentration
|
|
14
|
+
|
|
15
|
+
Every threshold is a CLI option and is echoed in the output. Stdlib only.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import csv
|
|
21
|
+
import json
|
|
22
|
+
import math
|
|
23
|
+
import random
|
|
24
|
+
import statistics
|
|
25
|
+
import sys
|
|
26
|
+
import urllib.request
|
|
27
|
+
from collections import Counter, defaultdict
|
|
28
|
+
from datetime import date, datetime, timedelta
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
PEGS = {"USD": 1.0, "SAR": 3.75, "AED": 3.6725, "QAR": 3.64, "KWD": 0.3066, "BHD": 0.376, "OMR": 0.3845, "HKD": 7.8, "JOD": 0.709}
|
|
32
|
+
|
|
33
|
+
COUNTRY_CURRENCY = {
|
|
34
|
+
"united states of america": "USD", "united states": "USD", "us": "USD", "usa": "USD",
|
|
35
|
+
"united kingdom": "GBP", "gb": "GBP", "uk": "GBP", "australia": "AUD", "new zealand": "NZD", "canada": "CAD",
|
|
36
|
+
"switzerland": "CHF", "poland": "PLN", "czech republic": "CZK", "czechia": "CZK", "hungary": "HUF", "norway": "NOK",
|
|
37
|
+
"sweden": "SEK", "denmark": "DKK", "romania": "RON", "bulgaria": "BGN", "iceland": "ISK", "brazil": "BRL",
|
|
38
|
+
"mexico": "MXN", "japan": "JPY", "india": "INR", "south africa": "ZAR", "turkey": "TRY", "israel": "ILS",
|
|
39
|
+
"singapore": "SGD", "hong kong": "HKD", "korea, republic of": "KRW", "south korea": "KRW", "thailand": "THB",
|
|
40
|
+
"malaysia": "MYR", "philippines": "PHP", "indonesia": "IDR", "saudi arabia": "SAR", "united arab emirates": "AED",
|
|
41
|
+
"qatar": "QAR", "kuwait": "KWD", "bahrain": "BHD", "oman": "OMR", "jordan": "JOD",
|
|
42
|
+
}
|
|
43
|
+
EURO_COUNTRIES = {
|
|
44
|
+
"germany", "italy", "france", "spain", "portugal", "greece", "slovakia", "finland", "ireland", "austria",
|
|
45
|
+
"netherlands", "belgium", "lithuania", "croatia", "slovenia", "estonia", "latvia", "cyprus", "luxembourg", "malta",
|
|
46
|
+
"moldova, republic of", "moldova", "montenegro", "kosovo",
|
|
47
|
+
}
|
|
48
|
+
for _c in EURO_COUNTRIES:
|
|
49
|
+
COUNTRY_CURRENCY[_c] = "EUR"
|
|
50
|
+
|
|
51
|
+
SCHEMES = {
|
|
52
|
+
"10": [5, 10, 15, 20, 25, 30, 40, 50, 60, 75, 80, 100, 150, 200, 250, 300, 400, 500, 750, 1000, 1500, 2000],
|
|
53
|
+
"12": [12, 24, 36, 48, 72, 96, 108, 144, 192, 288, 384, 480],
|
|
54
|
+
}
|
|
55
|
+
SCHEME_SCALES = [1, 5, 10, 25, 100, 400, 1000]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ----------------------------------------------------------------------------- rates
|
|
59
|
+
|
|
60
|
+
def fetch_rates(start: str, end: str, base: str, symbols: list[str]) -> dict:
|
|
61
|
+
url = f"https://api.frankfurter.dev/v1/{start}..{end}?base={base}&symbols={','.join(sorted(set(symbols)))}"
|
|
62
|
+
with urllib.request.urlopen(url, timeout=60) as resp: # noqa: S310 - explicit user action
|
|
63
|
+
payload = json.load(resp)
|
|
64
|
+
return {"base": base, "source": url, "rates": payload["rates"]}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Rates:
|
|
68
|
+
"""Daily rates quoted as units of currency per one unit of base; prior-day carry up to ``carry`` days."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, cache: dict, carry: int = 7):
|
|
71
|
+
self.base = cache.get("base", "USD")
|
|
72
|
+
self.rates = cache["rates"]
|
|
73
|
+
self.days = sorted(self.rates)
|
|
74
|
+
self.carry = carry
|
|
75
|
+
self.carried: Counter = Counter()
|
|
76
|
+
|
|
77
|
+
def rate(self, day: str, cur: str) -> float | None:
|
|
78
|
+
if cur == self.base:
|
|
79
|
+
return 1.0
|
|
80
|
+
if cur in PEGS and self.base == "USD":
|
|
81
|
+
return PEGS[cur]
|
|
82
|
+
if not self.days:
|
|
83
|
+
return None
|
|
84
|
+
d = date.fromisoformat(day)
|
|
85
|
+
for lag in range(self.carry + 1):
|
|
86
|
+
dd = (d - timedelta(days=lag)).isoformat()
|
|
87
|
+
if dd in self.rates and cur in self.rates[dd]:
|
|
88
|
+
if lag:
|
|
89
|
+
self.carried[cur] += 1
|
|
90
|
+
return self.rates[dd][cur]
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def market_move(self, cur: str, day_a: str, day_b: str, source: str | None = None) -> float | None:
|
|
94
|
+
source = self.base if source is None else source
|
|
95
|
+
if cur == source:
|
|
96
|
+
return 0.0
|
|
97
|
+
a, b = self.rate(day_a, cur), self.rate(day_b, cur)
|
|
98
|
+
src_a, src_b = self.rate(day_a, source), self.rate(day_b, source)
|
|
99
|
+
if not a or not b or not src_a or not src_b:
|
|
100
|
+
return None
|
|
101
|
+
return abs((b / src_b) / (a / src_a) - 1)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ----------------------------------------------------------------------------- helpers
|
|
105
|
+
|
|
106
|
+
def find_peaks(values: list[float], tol: float, min_share: float) -> list[tuple[float, int]]:
|
|
107
|
+
"""Group values within ±tol (relative) of the modal value, largest first; return (center, count)."""
|
|
108
|
+
if not values:
|
|
109
|
+
return []
|
|
110
|
+
counts = Counter(round(v, 2) for v in values)
|
|
111
|
+
n = len(values)
|
|
112
|
+
used: set[float] = set()
|
|
113
|
+
peaks = []
|
|
114
|
+
for v, _ in counts.most_common():
|
|
115
|
+
if v in used:
|
|
116
|
+
continue
|
|
117
|
+
grp = [w for w in counts if w not in used and abs(w - v) <= tol * max(v, 1e-9)]
|
|
118
|
+
cnt = sum(counts[w] for w in grp)
|
|
119
|
+
if cnt / n < min_share:
|
|
120
|
+
continue
|
|
121
|
+
used.update(grp)
|
|
122
|
+
center = sum(w * counts[w] for w in grp) / cnt
|
|
123
|
+
peaks.append((center, cnt))
|
|
124
|
+
return sorted(peaks, key=lambda p: -p[1])
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def coverage(values: list[float], tol: float, min_share: float, top_k: int) -> tuple[float, list[tuple[float, int]]]:
|
|
128
|
+
peaks = find_peaks(values, tol, min_share)[:top_k]
|
|
129
|
+
return (sum(c for _, c in peaks) / len(values) if values else 0.0), peaks
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def coverage_against(values: list[float], centers: list[float], tol: float) -> float:
|
|
133
|
+
"""Share of values within tol of any given centre (used for the jittered control)."""
|
|
134
|
+
if not values or not centers:
|
|
135
|
+
return 0.0
|
|
136
|
+
return sum(1 for v in values if any(abs(v - c) <= tol * c for c in centers)) / len(values)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def day_drift(rows: list[dict], converted: list[float], centers: list[tuple[float, int]], tol: float, min_per_day: int = 3) -> float | None:
|
|
140
|
+
"""Weighted mean, over the top peaks, of (max - min) / centre of the daily median converted value.
|
|
141
|
+
|
|
142
|
+
Under the true wallet currency a preset converts to the same local value every day; under a wrong
|
|
143
|
+
currency the value follows the cross rate day by day."""
|
|
144
|
+
total = 0.0
|
|
145
|
+
weight = 0
|
|
146
|
+
for center, cnt in centers[:5]:
|
|
147
|
+
per_day: dict[str, list[float]] = defaultdict(list)
|
|
148
|
+
for r, v in zip(rows, converted):
|
|
149
|
+
if abs(v - center) <= tol * center:
|
|
150
|
+
per_day[r["day"]].append(v)
|
|
151
|
+
meds = [statistics.median(v) for v in per_day.values() if len(v) >= min_per_day]
|
|
152
|
+
if len(meds) < 2:
|
|
153
|
+
continue
|
|
154
|
+
total += (max(meds) - min(meds)) / center * cnt
|
|
155
|
+
weight += cnt
|
|
156
|
+
return total / weight if weight else None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def jitter(values: list[float], rng: random.Random, lo: float, hi: float) -> list[float]:
|
|
160
|
+
return [v * (1 + rng.choice((-1, 1)) * rng.uniform(lo, hi)) for v in values]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def retail_shaped(x: float, tol: float = 0.006) -> bool:
|
|
164
|
+
r = round(x)
|
|
165
|
+
return any(g > 0 and abs(x - g) <= tol for g in (r, r - 0.01, r - 0.05, r - 0.02))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def match_scheme(peaks: list[tuple[float, int]], tol: float) -> tuple[str, int, float]:
|
|
169
|
+
"""Return (scheme name, scale, share of peak attempts on the scheme)."""
|
|
170
|
+
name, scale, share, _ = match_scheme_peaks(peaks, tol)
|
|
171
|
+
return name, scale, share
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def match_scheme_peaks(peaks: list[tuple[float, int]], tol: float) -> tuple[str, int, float, list[tuple[float, int]]]:
|
|
175
|
+
total = sum(c for _, c in peaks) or 1
|
|
176
|
+
best = ("none", 1, 0.0, [])
|
|
177
|
+
for name, menu in SCHEMES.items():
|
|
178
|
+
for scale in SCHEME_SCALES:
|
|
179
|
+
on = [(center, cnt) for center, cnt in peaks if any(abs(center - m * scale) <= tol * m * scale for m in menu)]
|
|
180
|
+
share = sum(c for _, c in on) / total
|
|
181
|
+
if share > best[2]:
|
|
182
|
+
best = (name, scale, share, on)
|
|
183
|
+
return best
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ----------------------------------------------------------------------------- core
|
|
187
|
+
|
|
188
|
+
def parse_rows(path: Path, cols: dict) -> list[dict]:
|
|
189
|
+
rows = []
|
|
190
|
+
with path.open(newline="", encoding="utf-8-sig") as fh:
|
|
191
|
+
for r in csv.DictReader(fh):
|
|
192
|
+
try:
|
|
193
|
+
amt = float(r[cols["amount"]])
|
|
194
|
+
except (KeyError, ValueError):
|
|
195
|
+
continue
|
|
196
|
+
ts = r.get(cols["date"], "")[:19]
|
|
197
|
+
rows.append({
|
|
198
|
+
"amount": amt,
|
|
199
|
+
"currency": r.get(cols["currency"], "USD").strip().upper() or "USD",
|
|
200
|
+
"day": ts[:10],
|
|
201
|
+
"ts": ts,
|
|
202
|
+
"descriptor": (r.get(cols["descriptor"], "") or "UNKNOWN_DESCRIPTOR").strip(),
|
|
203
|
+
"country": (r.get(cols["country"], "") or "UNKNOWN").strip(),
|
|
204
|
+
"response": (r.get(cols["response"], "") if cols.get("response") else "").strip(),
|
|
205
|
+
"account": (r.get(cols["account"], "") if cols.get("account") else "").strip(),
|
|
206
|
+
"card_type": (r.get(cols["card_type"], "") if cols.get("card_type") else "").strip(),
|
|
207
|
+
"avs": (r.get(cols["avs"], "") if cols.get("avs") else "").strip(),
|
|
208
|
+
"auth": (r.get(cols["auth"], "") if cols.get("auth") else "").strip(),
|
|
209
|
+
})
|
|
210
|
+
return rows
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def infer_wallet(rows: list[dict], rates: Rates, candidates: list[str], opt, rng) -> dict:
|
|
214
|
+
"""Per group: coverage under each candidate currency, control against the same centres, day drift."""
|
|
215
|
+
out = {}
|
|
216
|
+
for cur in candidates:
|
|
217
|
+
vals, ok = [], True
|
|
218
|
+
for r in rows:
|
|
219
|
+
rt = rates.rate(r["day"], cur)
|
|
220
|
+
src = rates.rate(r["day"], r["currency"])
|
|
221
|
+
if rt is None or src is None:
|
|
222
|
+
ok = False
|
|
223
|
+
break
|
|
224
|
+
vals.append(r["amount"] / src * rt)
|
|
225
|
+
if not ok or not vals:
|
|
226
|
+
continue
|
|
227
|
+
cov, peaks = coverage(vals, opt.peak_tol, opt.peak_min_share, opt.top_k)
|
|
228
|
+
ctrl = coverage_against(jitter(vals, rng, opt.jitter_lo, opt.jitter_hi), [c for c, _ in peaks], opt.peak_tol)
|
|
229
|
+
drift = day_drift(rows, vals, peaks, opt.peak_tol)
|
|
230
|
+
name, scale, sshare, on = match_scheme_peaks(peaks, opt.scheme_tol)
|
|
231
|
+
out[cur] = {"coverage": round(cov, 4), "control": round(ctrl, 4), "excess": round(cov - ctrl, 4), "day_drift": None if drift is None else round(drift, 4),
|
|
232
|
+
"peaks": [(round(c, 2), n) for c, n in peaks], "scheme": name, "scheme_scale": scale, "scheme_share": round(sshare, 4),
|
|
233
|
+
"scheme_coverage": round(sum(n for _, n in on) / len(vals), 4), "scheme_peaks": [(round(c, 2), n) for c, n in on]}
|
|
234
|
+
return out
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def wallet_acceptable(info: dict, opt) -> bool:
|
|
238
|
+
if info["excess"] < opt.wallet_min_excess * 0.4:
|
|
239
|
+
return False
|
|
240
|
+
if info["coverage"] >= opt.wallet_min_coverage and info["excess"] >= opt.wallet_min_excess:
|
|
241
|
+
return True
|
|
242
|
+
return info["scheme_coverage"] >= opt.wallet_min_coverage_with_scheme and info["scheme_share"] >= opt.scheme_min_share
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def choose_wallet(walls: dict, legal: str | None, opt) -> str | None:
|
|
246
|
+
"""Prefer a currency whose peaks land on a known menu, then the lowest day drift, then legal tender, EUR, USD."""
|
|
247
|
+
ok = [(cur, info) for cur, info in walls.items() if wallet_acceptable(info, opt)]
|
|
248
|
+
if not ok:
|
|
249
|
+
return None
|
|
250
|
+
prio = {legal: 0, "EUR": 1, "USD": 2}
|
|
251
|
+
|
|
252
|
+
def key(kv):
|
|
253
|
+
cur, info = kv
|
|
254
|
+
drift = info["day_drift"] if info["day_drift"] is not None else 1.0
|
|
255
|
+
return (0 if info["scheme_share"] >= opt.scheme_min_share else 1, round(drift / opt.drift_bucket), prio.get(cur, 3), -info["coverage"])
|
|
256
|
+
return sorted(ok, key=key)[0][0]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def rate_consistency(rows: list[dict], rates: Rates, wallet: str, peaks: list, opt) -> dict:
|
|
260
|
+
"""Per peak and source currency: modal processed amount by day versus the source-to-wallet cross-rate."""
|
|
261
|
+
result = {"frozen_peaks": 0, "live_peaks": 0, "frozen_attempts": 0, "tested_attempts": 0, "detail": []}
|
|
262
|
+
for center, _ in peaks:
|
|
263
|
+
by_source: dict[str, dict[str, Counter]] = defaultdict(lambda: defaultdict(Counter))
|
|
264
|
+
for r in rows:
|
|
265
|
+
src = rates.rate(r["day"], r["currency"])
|
|
266
|
+
rt = rates.rate(r["day"], wallet)
|
|
267
|
+
if not src or not rt:
|
|
268
|
+
continue
|
|
269
|
+
local = r["amount"] / src * rt
|
|
270
|
+
if abs(local - center) <= opt.peak_tol * center:
|
|
271
|
+
by_source[r["currency"]][r["day"]][round(r["amount"], 2)] += 1
|
|
272
|
+
for source, by_day in by_source.items():
|
|
273
|
+
days = sorted(d for d in by_day if sum(by_day[d].values()) >= opt.frozen_min_per_day)
|
|
274
|
+
if len(days) < opt.frozen_days:
|
|
275
|
+
continue
|
|
276
|
+
modal = [by_day[d].most_common(1)[0][0] for d in days]
|
|
277
|
+
attempts = sum(sum(by_day[d].values()) for d in days)
|
|
278
|
+
result["tested_attempts"] += attempts
|
|
279
|
+
move = rates.market_move(wallet, days[0], days[-1], source) or 0.0
|
|
280
|
+
distinct = len(set(modal))
|
|
281
|
+
frozen = distinct == 1 and move >= opt.frozen_market_move and source != wallet
|
|
282
|
+
if frozen:
|
|
283
|
+
result["frozen_peaks"] += 1
|
|
284
|
+
result["frozen_attempts"] += attempts
|
|
285
|
+
else:
|
|
286
|
+
result["live_peaks"] += 1
|
|
287
|
+
result["detail"].append({"peak": round(center, 2), "source_currency": source, "days": len(days), "modal_amounts": sorted(set(modal)), "market_move": round(move, 4), "frozen": frozen, "attempts": attempts})
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def retail_scan(rows: list[dict], opt, rng) -> dict:
|
|
292
|
+
"""Find one constant k such that amount / k is retail-shaped for a large share of rows, with many distinct bases."""
|
|
293
|
+
amounts = [r["amount"] for r in rows]
|
|
294
|
+
empty = {"constant": None, "share": 0.0, "control": 0.0, "excess": 0.0, "distinct_bases": 0, "rows": len(amounts)}
|
|
295
|
+
if len(amounts) < opt.retail_min_rows:
|
|
296
|
+
return empty
|
|
297
|
+
best = (1.0, 0.0)
|
|
298
|
+
k = opt.retail_k_min
|
|
299
|
+
while k <= opt.retail_k_max + 1e-9:
|
|
300
|
+
share = sum(retail_shaped(a / k) for a in amounts) / len(amounts)
|
|
301
|
+
if share > best[1]:
|
|
302
|
+
best = (round(k, 4), share)
|
|
303
|
+
k += opt.retail_k_step
|
|
304
|
+
bases = Counter(round(a / best[0], 2) for a in amounts if retail_shaped(a / best[0]))
|
|
305
|
+
distinct = sum(1 for _, c in bases.items() if c >= 3)
|
|
306
|
+
ctrl = sum(retail_shaped(a / best[0]) for a in jitter(amounts, rng, 0.004, 0.02)) / len(amounts)
|
|
307
|
+
return {"constant": best[0], "share": round(best[1], 4), "control": round(ctrl, 4), "excess": round(best[1] - ctrl, 4), "distinct_bases": distinct, "rows": len(amounts)}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def retail_passes(res: dict, opt, strong: bool = False) -> bool:
|
|
311
|
+
if not res["constant"] or res["excess"] < opt.retail_min_excess:
|
|
312
|
+
return False
|
|
313
|
+
if strong:
|
|
314
|
+
return res["share"] >= opt.retail_min_share and res["distinct_bases"] >= opt.retail_strong_distinct
|
|
315
|
+
return res["share"] >= opt.retail_weak_share and res["distinct_bases"] >= opt.retail_min_distinct
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def behaviour(rows: list[dict], opt) -> dict:
|
|
319
|
+
n = len(rows)
|
|
320
|
+
acc = Counter(r["account"] for r in rows if r["account"])
|
|
321
|
+
repeat = sum(1 for v in acc.values() if v >= 2) / len(acc) if acc else None
|
|
322
|
+
# retry after decline within window
|
|
323
|
+
by_acc: dict[str, list] = defaultdict(list)
|
|
324
|
+
for r in rows:
|
|
325
|
+
if r["account"] and r["ts"]:
|
|
326
|
+
by_acc[r["account"]].append(r)
|
|
327
|
+
declines = retries = 0
|
|
328
|
+
approvals = {v for v in opt.approval_values}
|
|
329
|
+
for lst in by_acc.values():
|
|
330
|
+
lst.sort(key=lambda r: r["ts"])
|
|
331
|
+
for i, r in enumerate(lst):
|
|
332
|
+
if opt.response_column and r["response"] not in approvals:
|
|
333
|
+
declines += 1
|
|
334
|
+
if i + 1 < len(lst):
|
|
335
|
+
try:
|
|
336
|
+
gap = (datetime.fromisoformat(lst[i + 1]["ts"]) - datetime.fromisoformat(r["ts"])).total_seconds()
|
|
337
|
+
except ValueError:
|
|
338
|
+
continue
|
|
339
|
+
if gap <= opt.retry_window_seconds:
|
|
340
|
+
retries += 1
|
|
341
|
+
hours = Counter(int(r["ts"][11:13]) for r in rows if len(r["ts"]) >= 13)
|
|
342
|
+
top3 = sum(v for _, v in hours.most_common(3)) / n if hours else None
|
|
343
|
+
# burst regularity: coefficient of variation of gaps inside the busiest hour-day
|
|
344
|
+
gaps = []
|
|
345
|
+
if hours:
|
|
346
|
+
buckets: dict[str, list] = defaultdict(list)
|
|
347
|
+
for r in rows:
|
|
348
|
+
if len(r["ts"]) >= 13:
|
|
349
|
+
buckets[r["ts"][:13]].append(r["ts"])
|
|
350
|
+
busiest = max(buckets.values(), key=len)
|
|
351
|
+
ts = sorted(datetime.fromisoformat(t) for t in busiest if len(t) >= 19)
|
|
352
|
+
gaps = [(ts[i + 1] - ts[i]).total_seconds() for i in range(len(ts) - 1)]
|
|
353
|
+
cv = (statistics.pstdev(gaps) / statistics.mean(gaps)) if len(gaps) >= 5 and statistics.mean(gaps) > 0 else None
|
|
354
|
+
avs = sum(1 for r in rows if r["avs"] and "not checked" not in r["avs"].lower()) / n if opt.avs_column else None
|
|
355
|
+
tds = sum(1 for r in rows if r["auth"] and r["auth"].lower() not in ("none", "", "n/a")) / n if opt.auth_column else None
|
|
356
|
+
prepaid = sum(1 for r in rows if "prepaid" in r["card_type"].lower()) / n if opt.card_type_column else None
|
|
357
|
+
bins = Counter(r["account"][:6] for r in rows if len(r["account"]) >= 6)
|
|
358
|
+
top5 = sum(v for _, v in bins.most_common(5)) / n if bins else None
|
|
359
|
+
approved = sum(1 for r in rows if r["response"] in approvals) / n if opt.response_column else None
|
|
360
|
+
fraud = sum(1 for r in rows if r["response"] in set(opt.suspected_fraud_values)) / n if opt.response_column else None
|
|
361
|
+
return {
|
|
362
|
+
"attempts": n, "cards": len(acc) or None, "repeat_card_share": repeat,
|
|
363
|
+
"retry_after_decline": (retries / declines) if declines else None, "declines": declines,
|
|
364
|
+
"top3_hour_share_utc": top3, "burst_gap_cv": cv, "avs_present_share": avs, "three_ds_share": tds,
|
|
365
|
+
"prepaid_share": prepaid, "top5_bin_share": top5, "approval_rate": approved, "suspected_fraud_share": fraud,
|
|
366
|
+
"first_day": min(r["day"] for r in rows), "last_day": max(r["day"] for r in rows),
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def on_peak_rows(rows: list[dict], rates: Rates, wallet: str, centers: list[float], tol: float) -> tuple[list[dict], list[dict]]:
|
|
371
|
+
inside, outside = [], []
|
|
372
|
+
for r in rows:
|
|
373
|
+
src = rates.rate(r["day"], r["currency"])
|
|
374
|
+
rt = rates.rate(r["day"], wallet)
|
|
375
|
+
if src and rt and any(abs(r["amount"] / src * rt - c) <= tol * c for c in centers):
|
|
376
|
+
inside.append(r)
|
|
377
|
+
else:
|
|
378
|
+
outside.append(r)
|
|
379
|
+
return inside, outside
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def extract_substreams(grows: list[dict], legal: str | None, rates: Rates, opt, rng) -> list[dict]:
|
|
383
|
+
"""Peel streams off a country group: menu (wallet + scheme), retail list, then remainder as off-menu."""
|
|
384
|
+
subs = []
|
|
385
|
+
residual = list(grows)
|
|
386
|
+
for _round in range(opt.max_substreams):
|
|
387
|
+
if len(residual) < opt.min_group:
|
|
388
|
+
break
|
|
389
|
+
retail = retail_scan(residual, opt, rng)
|
|
390
|
+
if retail_passes(retail, opt, strong=True):
|
|
391
|
+
k = retail["constant"]
|
|
392
|
+
inside = [r for r in residual if retail_shaped(r["amount"] / k)]
|
|
393
|
+
residual = [r for r in residual if not retail_shaped(r["amount"] / k)]
|
|
394
|
+
subs.append({"stream": f"retail x{k}", "rows": inside, "retail": retail})
|
|
395
|
+
continue
|
|
396
|
+
cands = [c for c in dict.fromkeys([legal, *opt.candidate_currencies]) if c]
|
|
397
|
+
walls = infer_wallet(residual, rates, cands, opt, rng)
|
|
398
|
+
wallet = choose_wallet(walls, legal, opt)
|
|
399
|
+
if wallet:
|
|
400
|
+
info = walls[wallet]
|
|
401
|
+
scheme, scale, sshare = info["scheme"], info["scheme_scale"], info["scheme_share"]
|
|
402
|
+
use_peaks = info["scheme_peaks"] if sshare >= opt.scheme_min_share else info["peaks"]
|
|
403
|
+
inside, outside = on_peak_rows(residual, rates, wallet, [c for c, _ in use_peaks], opt.peak_tol)
|
|
404
|
+
cons = rate_consistency(inside, rates, wallet, use_peaks, opt)
|
|
405
|
+
frozen = bool(cons["tested_attempts"] and cons["frozen_attempts"] / cons["tested_attempts"] >= opt.frozen_share)
|
|
406
|
+
label = f"{wallet} menu-{scheme if sshare >= opt.scheme_min_share else 'none'}{' frozen' if frozen else ' live'}"
|
|
407
|
+
subs.append({"stream": label, "rows": inside, "wallet": wallet, "coverage": info["coverage"], "day_drift": info["day_drift"], "scheme": scheme,
|
|
408
|
+
"scheme_scale": scale, "scheme_share": round(sshare, 3), "peaks": use_peaks, "all_peaks": info["peaks"], "frozen": frozen, "rate_consistency": cons,
|
|
409
|
+
"alternatives": {c: i["coverage"] for c, i in walls.items() if c != wallet}})
|
|
410
|
+
residual = outside
|
|
411
|
+
continue
|
|
412
|
+
if retail_passes(retail, opt):
|
|
413
|
+
k = retail["constant"]
|
|
414
|
+
inside = [r for r in residual if retail_shaped(r["amount"] / k)]
|
|
415
|
+
residual = [r for r in residual if not retail_shaped(r["amount"] / k)]
|
|
416
|
+
subs.append({"stream": f"retail x{k}", "rows": inside, "retail": retail})
|
|
417
|
+
continue
|
|
418
|
+
break
|
|
419
|
+
if residual:
|
|
420
|
+
subs.append({"stream": "off-menu", "rows": residual})
|
|
421
|
+
return subs
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ----------------------------------------------------------------------------- presentation
|
|
425
|
+
|
|
426
|
+
def fixed_amount_characteristics(rows: list[dict]) -> list[str]:
|
|
427
|
+
charge_days: dict[tuple[str, float], set[str]] = defaultdict(set)
|
|
428
|
+
for row in rows:
|
|
429
|
+
charge_days[(row["currency"], row["amount"])].add(row["day"])
|
|
430
|
+
return ["fixed amount"] if any(len(days) >= 2 for days in charge_days.values()) else []
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def payment_pattern_presentation(sdef: dict, rows: list[dict]) -> dict:
|
|
434
|
+
"""Describe detected rows without splitting or renaming canonical streams."""
|
|
435
|
+
label = sdef["stream"]
|
|
436
|
+
wallet = sdef.get("wallet")
|
|
437
|
+
established_menu = bool(wallet and " menu-" in label and " menu-none" not in label)
|
|
438
|
+
repeated = bool(wallet and " menu-none" in label)
|
|
439
|
+
if label.startswith("retail x"):
|
|
440
|
+
basis = "retail-list price factor"
|
|
441
|
+
elif repeated:
|
|
442
|
+
basis = "unidentified repeated-amount pattern"
|
|
443
|
+
elif established_menu:
|
|
444
|
+
basis = "established menu"
|
|
445
|
+
else:
|
|
446
|
+
basis = label
|
|
447
|
+
frozen_by_source: Counter = Counter()
|
|
448
|
+
for detail in sdef.get("rate_consistency", {}).get("detail", []):
|
|
449
|
+
if detail["frozen"] and detail["source_currency"] != wallet:
|
|
450
|
+
frozen_by_source[detail["source_currency"]] += detail["attempts"]
|
|
451
|
+
parts = []
|
|
452
|
+
for currency, attempts in sorted(Counter(r["currency"] for r in rows).items()):
|
|
453
|
+
if established_menu:
|
|
454
|
+
pattern = "Direct top-up pattern" if currency == wallet else "FX-converted top-up pattern"
|
|
455
|
+
else:
|
|
456
|
+
pattern = "E-commerce pattern" if label.startswith("retail x") else "Other pattern"
|
|
457
|
+
parts.append({"pattern": pattern, "charge_currency": currency,
|
|
458
|
+
"inferred_top_up_currency": wallet if established_menu else None,
|
|
459
|
+
"attempts": attempts, "basis": basis,
|
|
460
|
+
"frozen_fx_attempts": frozen_by_source[currency] if established_menu and currency != wallet else 0})
|
|
461
|
+
return summarize_payment_patterns([{"by_charge_currency": parts,
|
|
462
|
+
"pricing_characteristics": fixed_amount_characteristics(rows)}])
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def summarize_payment_patterns(presentations: list[dict]) -> dict:
|
|
466
|
+
"""Aggregate display counts along the existing raw-label grouping only."""
|
|
467
|
+
combined = {}
|
|
468
|
+
pricing = set()
|
|
469
|
+
for presentation in presentations:
|
|
470
|
+
pricing.update(presentation["pricing_characteristics"])
|
|
471
|
+
for part in presentation["by_charge_currency"]:
|
|
472
|
+
key = (part["pattern"], part["charge_currency"], part["inferred_top_up_currency"], part["basis"])
|
|
473
|
+
if key not in combined:
|
|
474
|
+
combined[key] = {**part, "attempts": 0, "frozen_fx_attempts": 0}
|
|
475
|
+
combined[key]["attempts"] += part["attempts"]
|
|
476
|
+
combined[key]["frozen_fx_attempts"] += part["frozen_fx_attempts"]
|
|
477
|
+
parts = list(combined.values())
|
|
478
|
+
patterns: Counter = Counter()
|
|
479
|
+
currencies: Counter = Counter()
|
|
480
|
+
inferred = set()
|
|
481
|
+
for part in parts:
|
|
482
|
+
patterns[part["pattern"]] += part["attempts"]
|
|
483
|
+
currencies[part["charge_currency"]] += part["attempts"]
|
|
484
|
+
if part["inferred_top_up_currency"]:
|
|
485
|
+
inferred.add(part["inferred_top_up_currency"])
|
|
486
|
+
return {"pattern_counts": dict(sorted(patterns.items())),
|
|
487
|
+
"charge_currencies": dict(sorted(currencies.items())),
|
|
488
|
+
"inferred_top_up_currencies": sorted(inferred),
|
|
489
|
+
"by_charge_currency": parts, "pricing_characteristics": sorted(pricing)}
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def presentation_cells(stream: dict) -> str:
|
|
493
|
+
presentation = stream.get("presentation")
|
|
494
|
+
if not presentation:
|
|
495
|
+
return "n/a | n/a | n/a | n/a"
|
|
496
|
+
patterns = []
|
|
497
|
+
for pattern, attempts in presentation["pattern_counts"].items():
|
|
498
|
+
sources = []
|
|
499
|
+
for part in presentation["by_charge_currency"]:
|
|
500
|
+
if part["pattern"] != pattern:
|
|
501
|
+
continue
|
|
502
|
+
source = f"{part['charge_currency']}: {part['attempts']}"
|
|
503
|
+
if part["frozen_fx_attempts"]:
|
|
504
|
+
source += f" ({part['frozen_fx_attempts']} frozen FX tested attempts)"
|
|
505
|
+
sources.append(source)
|
|
506
|
+
text = f"{pattern}: {attempts} [{', '.join(sources)}]"
|
|
507
|
+
patterns.append(text)
|
|
508
|
+
if any(p["basis"] == "unidentified repeated-amount pattern" for p in presentation["by_charge_currency"]):
|
|
509
|
+
patterns.append("unidentified repeated-amount pattern")
|
|
510
|
+
inferred = ", ".join(presentation["inferred_top_up_currencies"]) or "n/a"
|
|
511
|
+
charges = ", ".join(f"{cur}: {n}" for cur, n in presentation["charge_currencies"].items())
|
|
512
|
+
pricing = ", ".join(presentation["pricing_characteristics"]) or "n/a"
|
|
513
|
+
return f"{'; '.join(patterns)} | {inferred} | {charges} | {pricing}"
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def analyse(rows: list[dict], rates: Rates, opt) -> dict:
|
|
517
|
+
rng = random.Random(opt.seed)
|
|
518
|
+
report = {"parameters": {k: v for k, v in vars(opt).items() if k not in ("csv_path", "output", "markdown", "fetch_rates", "rates")}, "descriptors": []}
|
|
519
|
+
by_desc: dict[str, list] = defaultdict(list)
|
|
520
|
+
for r in rows:
|
|
521
|
+
by_desc[r["descriptor"]].append(r)
|
|
522
|
+
for desc, drows in sorted(by_desc.items(), key=lambda x: -len(x[1])):
|
|
523
|
+
entry = {"descriptor": desc, "attempts": len(drows), "groups": [], "streams": []}
|
|
524
|
+
by_ctry: dict[str, list] = defaultdict(list)
|
|
525
|
+
for r in drows:
|
|
526
|
+
by_ctry[r["country"]].append(r)
|
|
527
|
+
stream_rows: dict[str, list] = defaultdict(list)
|
|
528
|
+
stream_presentations: dict[str, list] = defaultdict(list)
|
|
529
|
+
small = 0
|
|
530
|
+
for ctry, grows in sorted(by_ctry.items(), key=lambda x: -len(x[1])):
|
|
531
|
+
if len(grows) < opt.min_group:
|
|
532
|
+
small += len(grows)
|
|
533
|
+
stream_rows["small groups"].extend(grows)
|
|
534
|
+
stream_presentations["small groups"].append(payment_pattern_presentation({"stream": "small groups"}, grows))
|
|
535
|
+
continue
|
|
536
|
+
legal = COUNTRY_CURRENCY.get(ctry.lower())
|
|
537
|
+
subs = extract_substreams(grows, legal, rates, opt, rng)
|
|
538
|
+
group = {"country": ctry, "attempts": len(grows), "legal_tender": legal, "substreams": []}
|
|
539
|
+
for sdef in subs:
|
|
540
|
+
srows = sdef.pop("rows")
|
|
541
|
+
sdef["attempts"] = len(srows)
|
|
542
|
+
sdef["share_of_group"] = round(len(srows) / len(grows), 3)
|
|
543
|
+
sdef["behaviour"] = behaviour(srows, opt)
|
|
544
|
+
sdef["presentation"] = payment_pattern_presentation(sdef, srows)
|
|
545
|
+
group["substreams"].append(sdef)
|
|
546
|
+
stream_rows[sdef["stream"]].extend(srows)
|
|
547
|
+
stream_presentations[sdef["stream"]].append(sdef["presentation"])
|
|
548
|
+
entry["groups"].append(group)
|
|
549
|
+
for label, srows in sorted(stream_rows.items(), key=lambda kv: -len(kv[1])):
|
|
550
|
+
presentation = summarize_payment_patterns(stream_presentations[label])
|
|
551
|
+
presentation["pricing_characteristics"] = fixed_amount_characteristics(srows)
|
|
552
|
+
entry["streams"].append({"stream": label, "attempts": len(srows), "share_of_descriptor": round(len(srows) / len(drows), 3),
|
|
553
|
+
"countries": [c for c, _ in Counter(r["country"] for r in srows).most_common(6)], "behaviour": behaviour(srows, opt),
|
|
554
|
+
"presentation": presentation})
|
|
555
|
+
report["descriptors"].append(entry)
|
|
556
|
+
report["rates_carried_days"] = dict(rates.carried)
|
|
557
|
+
return report
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def to_markdown(report: dict) -> str:
|
|
561
|
+
fmt = lambda v: "n/a" if v is None else f"{v:.0%}" # noqa: E731
|
|
562
|
+
out = ["# Payment patterns", "",
|
|
563
|
+
"Payment patterns describe amount evidence, not confirmed wallets, deposits, players, casinos or shops. "
|
|
564
|
+
"Inferred top-up currencies are distinct from actual charge currencies. Fixed amount is a supplementary pricing characteristic, "
|
|
565
|
+
"not proof of fixed FX or generated traffic. Raw live labels do not establish floating FX; "
|
|
566
|
+
"frozen FX counts refer only to source-specific cross-rate tests.", ""]
|
|
567
|
+
for d in report["descriptors"]:
|
|
568
|
+
out += [f"## {d['descriptor']} ({d['attempts']} attempts)", "", "| Raw stream | Payment pattern | Inferred top-up currencies | Actual charge currencies (attempts) | Pricing characteristic | Attempts | Share | Countries | Approval | Suspected fraud | Retry after decline | Repeat cards | Top-3-hour share | AVS present |", "|---|---|---|---|---|---|---|---|---|---|---|---|---|---|"]
|
|
569
|
+
for s in d["streams"]:
|
|
570
|
+
b = s["behaviour"]
|
|
571
|
+
out.append(f"| {s['stream']} | {presentation_cells(s)} | {s['attempts']} | {s['share_of_descriptor']:.0%} | {', '.join(s['countries'])} | {fmt(b['approval_rate'])} | {fmt(b['suspected_fraud_share'])} | {fmt(b['retry_after_decline'])} | {fmt(b['repeat_card_share'])} | {fmt(b['top3_hour_share_utc'])} | {fmt(b['avs_present_share'])} |")
|
|
572
|
+
out += ["", "| Country | n | Raw sub-stream | Payment pattern | Inferred top-up currencies | Actual charge currencies (attempts) | Pricing characteristic | Rows | Coverage | Day drift | Scheme share | Peaks |", "|---|---|---|---|---|---|---|---|---|---|---|---|"]
|
|
573
|
+
for g in d["groups"]:
|
|
574
|
+
for sdef in g["substreams"]:
|
|
575
|
+
peaks = " ".join(f"{c:g}x{n}" for c, n in sdef.get("peaks", [])[:6])
|
|
576
|
+
cov = sdef.get("coverage"); dr = sdef.get("day_drift"); ss = sdef.get("scheme_share")
|
|
577
|
+
out.append(f"| {g['country']} | {g['attempts']} | {sdef['stream']} | {presentation_cells(sdef)} | {sdef['attempts']} | {'' if cov is None else f'{cov:.0%}'} | {'' if dr is None else f'{dr:.2%}'} | {'' if ss is None else f'{ss:.0%}'} | {peaks} |")
|
|
578
|
+
out.append("")
|
|
579
|
+
return "\n".join(out)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def main(argv=None) -> None:
|
|
583
|
+
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
584
|
+
p.add_argument("csv_path", type=Path)
|
|
585
|
+
p.add_argument("--rates", type=Path, required=True, help="daily FX cache JSON: {base, rates:{YYYY-MM-DD:{CCY:rate}}}")
|
|
586
|
+
p.add_argument("--fetch-rates", nargs=2, metavar=("START", "END"), help="download Frankfurter v1 rates into --rates before analysis")
|
|
587
|
+
p.add_argument("--amount-column", required=True)
|
|
588
|
+
p.add_argument("--currency-column", required=True)
|
|
589
|
+
p.add_argument("--date-column", required=True, help="timestamp or date column (ISO)")
|
|
590
|
+
p.add_argument("--descriptor-column", required=True)
|
|
591
|
+
p.add_argument("--bin-country-column", required=True)
|
|
592
|
+
p.add_argument("--response-column")
|
|
593
|
+
p.add_argument("--approval-values", nargs="*", default=["A001"])
|
|
594
|
+
p.add_argument("--suspected-fraud-values", nargs="*", default=["D102"])
|
|
595
|
+
p.add_argument("--account-column")
|
|
596
|
+
p.add_argument("--card-type-column")
|
|
597
|
+
p.add_argument("--avs-column")
|
|
598
|
+
p.add_argument("--auth-column")
|
|
599
|
+
p.add_argument("--candidate-currencies", nargs="*", default=["EUR", "USD", "GBP", "CHF"])
|
|
600
|
+
p.add_argument("--min-group", type=int, default=80)
|
|
601
|
+
p.add_argument("--peak-tol", type=float, default=0.012)
|
|
602
|
+
p.add_argument("--peak-min-share", type=float, default=0.02)
|
|
603
|
+
p.add_argument("--top-k", type=int, default=10)
|
|
604
|
+
p.add_argument("--jitter-lo", type=float, default=0.02)
|
|
605
|
+
p.add_argument("--jitter-hi", type=float, default=0.05)
|
|
606
|
+
p.add_argument("--wallet-min-coverage", type=float, default=0.45, help="share of a group on its top-k peaks needed to call it menu-driven")
|
|
607
|
+
p.add_argument("--wallet-min-excess", type=float, default=0.25)
|
|
608
|
+
p.add_argument("--wallet-min-coverage-with-scheme", type=float, default=0.15, help="lower coverage bar when the peaks sit on a known menu")
|
|
609
|
+
p.add_argument("--drift-bucket", type=float, default=0.004, help="day-drift values within one bucket are ties broken by currency priority")
|
|
610
|
+
p.add_argument("--scheme-min-share", type=float, default=0.5)
|
|
611
|
+
p.add_argument("--scheme-tol", type=float, default=0.015, help="relative tolerance when matching peaks to a menu scheme (cashier markups run 0-3%%)")
|
|
612
|
+
p.add_argument("--max-substreams", type=int, default=3)
|
|
613
|
+
p.add_argument("--frozen-days", type=int, default=4)
|
|
614
|
+
p.add_argument("--frozen-min-per-day", type=int, default=3)
|
|
615
|
+
p.add_argument("--frozen-market-move", type=float, default=0.001)
|
|
616
|
+
p.add_argument("--frozen-share", type=float, default=0.5)
|
|
617
|
+
p.add_argument("--retail-k-min", type=float, default=1.0)
|
|
618
|
+
p.add_argument("--retail-k-max", type=float, default=1.15)
|
|
619
|
+
p.add_argument("--retail-k-step", type=float, default=0.0025)
|
|
620
|
+
p.add_argument("--retail-min-excess", type=float, default=0.2)
|
|
621
|
+
p.add_argument("--retail-min-share", type=float, default=0.3)
|
|
622
|
+
p.add_argument("--retail-min-distinct", type=int, default=5, help="distinct retail base prices (>=3 rows each) needed")
|
|
623
|
+
p.add_argument("--retail-strong-distinct", type=int, default=8, help="distinct bases for retail to be tried before wallet inference")
|
|
624
|
+
p.add_argument("--retail-weak-share", type=float, default=0.2)
|
|
625
|
+
p.add_argument("--retail-min-rows", type=int, default=60)
|
|
626
|
+
p.add_argument("--retry-window-seconds", type=int, default=1800)
|
|
627
|
+
p.add_argument("--seed", type=int, default=7)
|
|
628
|
+
p.add_argument("--output", type=Path)
|
|
629
|
+
p.add_argument("--markdown", type=Path)
|
|
630
|
+
opt = p.parse_args(argv)
|
|
631
|
+
cols = {"amount": opt.amount_column, "currency": opt.currency_column, "date": opt.date_column, "descriptor": opt.descriptor_column, "country": opt.bin_country_column,
|
|
632
|
+
"response": opt.response_column, "account": opt.account_column, "card_type": opt.card_type_column, "avs": opt.avs_column, "auth": opt.auth_column}
|
|
633
|
+
rows = parse_rows(opt.csv_path, cols)
|
|
634
|
+
if opt.fetch_rates:
|
|
635
|
+
symbols = sorted({c for c in COUNTRY_CURRENCY.values() if c not in PEGS} | set(opt.candidate_currencies))
|
|
636
|
+
cache = fetch_rates(opt.fetch_rates[0], opt.fetch_rates[1], "USD", [s for s in symbols if s != "USD"])
|
|
637
|
+
opt.rates.write_text(json.dumps(cache, indent=1))
|
|
638
|
+
rates = Rates(json.loads(opt.rates.read_text()))
|
|
639
|
+
report = analyse(rows, rates, opt)
|
|
640
|
+
text = json.dumps(report, indent=1, default=str)
|
|
641
|
+
if opt.output:
|
|
642
|
+
opt.output.write_text(text)
|
|
643
|
+
else:
|
|
644
|
+
sys.stdout.write(text + "\n")
|
|
645
|
+
if opt.markdown:
|
|
646
|
+
opt.markdown.write_text(to_markdown(report))
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
if __name__ == "__main__":
|
|
650
|
+
main()
|