@1aboveio/skills 1.0.10 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.codex-plugin/resources.json +297 -216
  3. package/README.md +3 -3
  4. package/package.json +10 -8
  5. package/skills/app-dev/airflow/SKILL.md +1 -1
  6. package/skills/app-dev/android-foundation/SKILL.md +35 -37
  7. package/skills/app-dev/android-foundation/references/delivery-focus.md +4 -2
  8. package/skills/app-dev/android-foundation/references/feature-work.md +50 -0
  9. package/skills/app-dev/android-foundation/references/setup.md +24 -9
  10. package/skills/app-dev/android-foundation/references/workspace.md +81 -0
  11. package/skills/app-dev/android-foundation/scripts/workspace-runtime/bin/workspaces.mjs +52 -0
  12. package/skills/app-dev/android-foundation/scripts/workspace-runtime/native-workspace.mjs +239 -0
  13. package/skills/app-dev/android-foundation/scripts/workspace-runtime/workspace-lifecycle.mjs +2154 -0
  14. package/skills/app-dev/app-debug/SKILL.md +1 -1
  15. package/skills/app-dev/better-auth/SKILL.md +1 -1
  16. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  17. package/skills/app-dev/data-quality/SKILL.md +1 -1
  18. package/skills/app-dev/ios-foundation/SKILL.md +15 -10
  19. package/skills/app-dev/ios-foundation/references/setup.md +25 -9
  20. package/skills/app-dev/ios-foundation/references/workspace.md +82 -0
  21. package/skills/app-dev/ios-foundation/scripts/workspace-runtime/bin/workspaces.mjs +52 -0
  22. package/skills/app-dev/ios-foundation/scripts/workspace-runtime/native-workspace.mjs +239 -0
  23. package/skills/app-dev/ios-foundation/scripts/workspace-runtime/workspace-lifecycle.mjs +2154 -0
  24. package/skills/app-dev/monorepo/SKILL.md +1 -1
  25. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  26. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  27. package/skills/app-dev/pyspark/SKILL.md +1 -1
  28. package/skills/app-dev/python-backend/SKILL.md +1 -1
  29. package/skills/app-dev/shadcn/SKILL.md +1 -1
  30. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  31. package/skills/cicd/cloud-build/SKILL.md +1 -1
  32. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  33. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  34. package/skills/cicd/docker/SKILL.md +1 -1
  35. package/skills/cicd/github-actions/SKILL.md +1 -1
  36. package/skills/cicd/github-flow/SKILL.md +1 -1
  37. package/skills/cicd/google-cloud/SKILL.md +1 -1
  38. package/skills/cicd/mergify/SKILL.md +1 -1
  39. package/skills/cicd/podman/SKILL.md +1 -1
  40. package/skills/content/article-review/SKILL.md +1 -1
  41. package/skills/content/article-to-image/SKILL.md +1 -1
  42. package/skills/content/flowchart-svg/SKILL.md +1 -1
  43. package/skills/content/image-gen/SKILL.md +1 -1
  44. package/skills/content/mermaid/SKILL.md +1 -1
  45. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  46. package/skills/content/wechat-publisher/SKILL.md +1 -1
  47. package/skills/content/wechat-search/SKILL.md +1 -1
  48. package/skills/content/xai-search/SKILL.md +1 -1
  49. package/skills/core/distill-writing/SKILL.md +3 -3
  50. package/skills/core/distill-writing/references/extraction.md +1 -1
  51. package/skills/core/doc-to-md/SKILL.md +1 -1
  52. package/skills/core/editorial-review/SKILL.md +82 -12
  53. package/skills/core/editorial-review/references/checklist.md +26 -1
  54. package/skills/core/house-style/SKILL.md +8 -3
  55. package/skills/core/house-style/references/analytical-reporting.md +123 -0
  56. package/skills/core/house-style/references/analytical-writing-patterns.md +79 -0
  57. package/skills/core/house-style/references/analytical-writing-research.md +129 -0
  58. package/skills/core/{write-doc → house-style}/references/bilingual.md +7 -5
  59. package/skills/core/house-style/references/definitions-and-metrics.md +86 -0
  60. package/skills/core/house-style/references/document-workflow.md +193 -0
  61. package/skills/core/house-style/references/initialization.md +66 -0
  62. package/skills/core/house-style/references/persistence.md +9 -4
  63. package/skills/core/house-style/references/rules.md +11 -4
  64. package/skills/core/house-style/scripts/init-guidance.mjs +213 -0
  65. package/skills/core/house-style/templates/analytical-report.md +54 -0
  66. package/skills/core/house-style/templates/gap-analysis.md +1 -1
  67. package/skills/core/plain-language/SKILL.md +2 -2
  68. package/skills/core/polish-cn/SKILL.md +9 -2
  69. package/skills/core/reg-parser/SKILL.md +3 -3
  70. package/skills/core/terminology/SKILL.md +8 -4
  71. package/skills/core/write-doc/SKILL.md +40 -151
  72. package/skills/core/write-report/SKILL.md +129 -0
  73. package/skills/core/{write-doc → write-report}/references/compliance-reports.md +4 -4
  74. package/skills/engineering/better-review/SKILL.md +37 -37
  75. package/skills/engineering/better-review/references/passes.md +35 -41
  76. package/skills/engineering/better-review/references/result.md +10 -9
  77. package/skills/engineering/e2e-test/SKILL.md +17 -17
  78. package/skills/engineering/e2e-test/references/authoring/playwright-config.md +11 -13
  79. package/skills/engineering/e2e-test/references/presentation-sweep.md +7 -7
  80. package/skills/engineering/e2e-test/references/reviewing.md +9 -11
  81. package/skills/engineering/e2e-test/references/route-manifest.md +1 -1
  82. package/skills/engineering/engineering/SKILL.md +124 -0
  83. package/skills/engineering/engineering/references/engineering-conventions.md +32 -0
  84. package/skills/engineering/engineering/references/feature-delivery.md +8 -0
  85. package/skills/engineering/engineering/references/init-scope.md +37 -0
  86. package/skills/engineering/engineering/scripts/init.mjs +160 -0
  87. package/skills/engineering/{contributing → engineering}/scripts/persist-conventions.mjs +1 -1
  88. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  89. package/skills/engineering/engineering-runtime/coherence/workflow.json +19 -19
  90. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -1
  91. package/skills/engineering/ensure-coverage/SKILL.md +22 -109
  92. package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +7 -5
  93. package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +4 -4
  94. package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +3 -3
  95. package/skills/engineering/ensure-coverage/references/depth/characterization.md +4 -65
  96. package/skills/engineering/ensure-coverage/references/depth/grading.md +5 -7
  97. package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +6 -12
  98. package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +1 -1
  99. package/skills/engineering/ensure-coverage/references/presentation/axis.md +6 -6
  100. package/skills/engineering/ensure-coverage/references/process/audit-mode.md +7 -23
  101. package/skills/engineering/ensure-coverage/references/process/output-template.md +18 -133
  102. package/skills/engineering/ensure-coverage/references/process/test-strategy.md +7 -12
  103. package/skills/engineering/ensure-coverage/scripts/README.md +12 -4
  104. package/skills/engineering/golden-path/SKILL.md +1 -1
  105. package/skills/engineering/harness-runtime/SKILL.md +3 -1
  106. package/skills/engineering/harness-runtime/bin/workspaces.mjs +52 -0
  107. package/skills/engineering/harness-runtime/native-workspace.mjs +239 -0
  108. package/skills/engineering/harness-runtime/references/workspaces.md +78 -0
  109. package/skills/engineering/harness-runtime/workspace-lifecycle.mjs +2154 -0
  110. package/skills/engineering/resolve-issues/SKILL.md +40 -61
  111. package/skills/engineering/resolve-issues/references/ci.md +13 -3
  112. package/skills/engineering/resolve-issues/references/delivery-progress.md +4 -3
  113. package/skills/engineering/resolve-issues/references/done.md +11 -2
  114. package/skills/engineering/resolve-issues/references/expire.md +9 -2
  115. package/skills/engineering/resolve-issues/references/implementation.md +33 -17
  116. package/skills/engineering/resolve-issues/references/preflight.md +3 -1
  117. package/skills/engineering/resolve-issues/references/principles.md +52 -0
  118. package/skills/engineering/resolve-issues/references/review.md +25 -15
  119. package/skills/engineering/resolve-issues/references/task-plan.md +64 -27
  120. package/skills/engineering/resolve-issues/references/workspaces.md +164 -0
  121. package/skills/engineering/resolve-issues/scripts/cleanup-runs.mjs +185 -317
  122. package/skills/engineering/resolve-issues/scripts/next-actions.mjs +44 -7
  123. package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +36 -2
  124. package/skills/engineering/resolve-issues/scripts/review-verdict.mjs +9 -8
  125. package/skills/engineering/resolve-issues/scripts/workspace-lifecycle.mjs +1 -0
  126. package/skills/engineering/resolve-release/SKILL.md +1 -1
  127. package/skills/engineering/rush-issues/SKILL.md +7 -5
  128. package/skills/engineering/rush-release/SKILL.md +1 -1
  129. package/skills/engineering/test-quality/SKILL.md +19 -4
  130. package/skills/engineering/test-quality/references/lint.md +1 -1
  131. package/skills/engineering/test-quality/references/principles.md +2 -2
  132. package/skills/engineering/test-quality/references/testing-conventions.md +1 -1
  133. package/skills/risk-compliance/compliance-audit/SKILL.md +4 -8
  134. package/skills/risk-compliance/compliance-audit/references/audience-and-reports.md +12 -0
  135. package/skills/risk-compliance/contract-compare/SKILL.md +17 -1
  136. package/skills/risk-compliance/contract-review/SKILL.md +6 -1
  137. package/skills/risk-compliance/contract-review/references/report-stage.md +17 -0
  138. package/skills/risk-compliance/doc-authenticity/SKILL.md +11 -1
  139. package/skills/risk-compliance/fraud-detection/LICENSE +3 -0
  140. package/skills/risk-compliance/fraud-detection/SKILL.md +148 -0
  141. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/archetypes/first-party-fraud.md +3 -2
  142. package/skills/risk-compliance/fraud-detection/references/archetypes/third-party-fraud.md +47 -0
  143. package/skills/risk-compliance/fraud-detection/references/collection-payout-features.md +151 -0
  144. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/contexts/bank-transfer.md +4 -3
  145. package/skills/risk-compliance/fraud-detection/references/contexts/card-payment.md +70 -0
  146. package/skills/risk-compliance/fraud-detection/references/contexts/payment-collection.md +34 -0
  147. package/skills/risk-compliance/fraud-detection/references/contexts/payout.md +34 -0
  148. package/skills/risk-compliance/fraud-detection/references/enrichment.md +74 -0
  149. package/skills/risk-compliance/fraud-detection/references/entities.md +48 -0
  150. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/feature-engineering.md +69 -33
  151. package/skills/risk-compliance/fraud-detection/references/feature-sql.md +150 -0
  152. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/report-rationale.md +4 -2
  153. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/report-template.md +45 -23
  154. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/review-checklist.md +60 -20
  155. package/skills/risk-compliance/fraud-detection/references/workflow.md +216 -0
  156. package/skills/risk-compliance/fraud-detection/release-check.mjs +95 -0
  157. package/skills/risk-compliance/payment-analysis/SKILL.md +36 -17
  158. package/skills/risk-compliance/payment-analysis/references/auth-rate-actions.md +2 -2
  159. package/skills/risk-compliance/payment-analysis/references/report-template.md +10 -2
  160. package/skills/risk-compliance/payment-analysis/references/visualization.md +11 -1
  161. package/skills/risk-compliance/portfolio-payment-analysis/LICENSE +3 -0
  162. package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +170 -0
  163. package/skills/risk-compliance/portfolio-payment-analysis/references/identity-and-reconciliation.md +46 -0
  164. package/skills/risk-compliance/portfolio-payment-analysis/references/metrics-and-reporting.md +170 -0
  165. package/skills/risk-compliance/portfolio-payment-analysis/references/risk-history.md +89 -0
  166. package/skills/risk-compliance/portfolio-payment-analysis/release-check.mjs +95 -0
  167. package/skills/risk-compliance/portfolio-payment-analysis/scripts/analyze.py +636 -0
  168. package/skills/risk-compliance/portfolio-payment-analysis/scripts/build_report.py +386 -0
  169. package/skills/risk-compliance/portfolio-payment-analysis/scripts/comparison_charts.py +425 -0
  170. package/skills/risk-compliance/portfolio-payment-analysis/scripts/csv_export.py +52 -0
  171. package/skills/risk-compliance/portfolio-payment-analysis/scripts/render.py +160 -0
  172. package/skills/risk-compliance/portfolio-payment-analysis/scripts/risk_history.py +435 -0
  173. package/skills/risk-compliance/similar-domain/SKILL.md +9 -4
  174. package/skills/risk-compliance/tld-detection/SKILL.md +124 -239
  175. package/skills/risk-compliance/tld-detection/agents/openai.yaml +1 -1
  176. package/skills/risk-compliance/tld-detection/references/clean.md +18 -51
  177. package/skills/risk-compliance/tld-detection/references/fx-conversion.md +56 -219
  178. package/skills/risk-compliance/tld-detection/references/metric-contract.md +109 -0
  179. package/skills/risk-compliance/tld-detection/references/pattern.md +99 -0
  180. package/skills/risk-compliance/tld-detection/references/report-template.md +49 -272
  181. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.py +46 -13
  182. package/skills/risk-compliance/tld-detection/scripts/detect_patterns.py +346 -0
  183. package/skills/risk-compliance/tld-detection/scripts/detect_streams.py +51 -19
  184. package/skills/risk-compliance/tld-detection/scripts/profile_groups.py +109 -0
  185. package/skills/risk-compliance/tld-detection/scripts/render_charts.py +218 -0
  186. package/skills/risk-compliance/tld-detection/scripts/tld_common.py +243 -0
  187. package/skills/engineering/contributing/SKILL.md +0 -70
  188. package/skills/risk-compliance/fraud-analysis/SKILL.md +0 -114
  189. package/skills/risk-compliance/fraud-analysis/references/archetypes/third-party-fraud.md +0 -27
  190. package/skills/risk-compliance/fraud-analysis/references/contexts/card-payment.md +0 -30
  191. package/skills/risk-compliance/fraud-analysis/references/contexts/payment-collection.md +0 -20
  192. package/skills/risk-compliance/fraud-analysis/references/contexts/payout.md +0 -20
  193. package/skills/risk-compliance/fraud-analysis/references/workflow.md +0 -175
  194. package/skills/risk-compliance/tld-detection/references/chinese-polish.md +0 -55
  195. package/skills/risk-compliance/tld-detection/references/clustering.md +0 -235
  196. package/skills/risk-compliance/tld-detection/references/decision.md +0 -189
  197. package/skills/risk-compliance/tld-detection/references/streams.md +0 -141
  198. package/skills/risk-compliance/tld-detection/references/terminology.md +0 -95
  199. package/skills/risk-compliance/tld-detection/references/workflow.md +0 -95
  200. /package/skills/{engineering/contributing → core/write-report}/LICENSE +0 -0
  201. /package/skills/core/{write-doc → write-report}/references/reports/audit-report-template.md +0 -0
  202. /package/skills/core/{write-doc → write-report}/references/reports/external-assessment-template.md +0 -0
  203. /package/skills/core/{write-doc → write-report}/references/reports/management-report-template.md +0 -0
  204. /package/skills/core/{write-doc → write-report}/references/reports/vars-report-template.md +0 -0
  205. /package/skills/{engineering/contributing → core/write-report}/release-check.mjs +0 -0
  206. /package/skills/{risk-compliance/fraud-analysis → engineering/engineering}/LICENSE +0 -0
  207. /package/skills/engineering/{contributing → engineering}/references/tracker-conventions.md +0 -0
  208. /package/skills/{risk-compliance/fraud-analysis → engineering/engineering}/release-check.mjs +0 -0
  209. /package/skills/engineering/{contributing → engineering}/scripts/persist-conventions-core.mjs +0 -0
  210. /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/archetypes/authorized-payment-scam.md +0 -0
  211. /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/mechanisms/account-takeover.md +0 -0
  212. /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/taxonomy.md +0 -0
  213. /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/terminology.md +0 -0
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env python3
2
+ """Render a saved group profile as standalone, accessible offline HTML."""
3
+ import argparse
4
+ import html
5
+ import json
6
+ import math
7
+ from datetime import timedelta
8
+
9
+ from tld_common import _period, write_text
10
+
11
+
12
+ def esc(value):
13
+ return html.escape(str(value), quote=True)
14
+
15
+
16
+ def display(value, percent=False):
17
+ if value is None:
18
+ return 'N/A'
19
+ return f'{value * 100:.2f}%' if percent else str(value)
20
+
21
+
22
+ def validate_profile(profile):
23
+ """Check the dimensions and scalar values consumed by the renderer."""
24
+ def count(value, field):
25
+ if type(value) is not int or value < 0:
26
+ raise ValueError(f'{field} must be a nonnegative integer')
27
+
28
+ def rate(value, field):
29
+ if value is not None and (type(value) not in (int, float) or not 0 <= value <= 1 or not math.isfinite(value)):
30
+ raise ValueError(f'{field} must be null or a finite rate between 0 and 1')
31
+
32
+ def counts(values, length, field):
33
+ if not isinstance(values, list) or len(values) != length:
34
+ raise ValueError(f'{field} must contain {length} counts')
35
+ for value in values:
36
+ count(value, field)
37
+
38
+ def matches(value, expected, field):
39
+ if expected is None:
40
+ if value is not None:
41
+ raise ValueError(f'{field} must be null')
42
+ elif value is None or not math.isclose(value, expected, rel_tol=1e-9, abs_tol=1e-12):
43
+ raise ValueError(f'{field} is inconsistent with its counts')
44
+
45
+ def status(value, field, denominator):
46
+ if not isinstance(value, dict):
47
+ raise ValueError(f'{field} must be an object')
48
+ for key in ('numerator', 'denominator', 'unknown'):
49
+ count(value.get(key), f'{field}.{key}')
50
+ for key in ('rate', 'coverage'):
51
+ rate(value.get(key), f'{field}.{key}')
52
+ if key not in value:
53
+ raise ValueError(f'{field}.{key} is required')
54
+ n, yes, unknown = value['denominator'], value['numerator'], value['unknown']
55
+ if n != denominator or yes + unknown > n:
56
+ raise ValueError(f'{field} counts are inconsistent')
57
+ matches(value['rate'], yes / n if n and not unknown else None, f'{field}.rate')
58
+ matches(value['coverage'], (n - unknown) / n if n else None, f'{field}.coverage')
59
+
60
+ if not isinstance(profile, dict) or type(profile.get('schema_version')) is not int or profile['schema_version'] != 1:
61
+ raise ValueError('unsupported profile schema')
62
+ _, start, end, _ = _period(profile.get('period'))
63
+ groups = profile.get('groups')
64
+ if not isinstance(groups, list):
65
+ raise ValueError('groups must be an array')
66
+ for group in groups:
67
+ if not isinstance(group, dict) or not isinstance(group.get('id'), str) or not isinstance(group.get('scope'), dict):
68
+ raise ValueError('group requires an id and scope')
69
+ tiers = group.get('tiers')
70
+ tiers = tiers.get('tiers') if isinstance(tiers, dict) else None
71
+ if not isinstance(tiers, list) or not tiers or any(not isinstance(t, dict) or not isinstance(t.get('label'), str) for t in tiers):
72
+ raise ValueError('tiers must contain labelled tiers')
73
+ for name in ('group', 'out_group'):
74
+ pop = group.get(name)
75
+ if not isinstance(pop, dict) or not isinstance(pop.get('daily'), list):
76
+ raise ValueError(f'{name} requires daily records')
77
+ daily = pop['daily']
78
+ if len(daily) != (end - start).days + 1:
79
+ raise ValueError(f'{name} daily records must cover the period')
80
+ counts(pop.get('hours'), 24, f'{name}.hours')
81
+ total = pop.get('total')
82
+ if not isinstance(total, dict):
83
+ raise ValueError(f'{name}.total must be an object')
84
+ count(total.get('count'), f'{name}.total.count')
85
+ counts(total.get('tier_counts'), len(tiers), f'{name}.total.tier_counts')
86
+ for key in ('success', 'fraud'):
87
+ status(total.get(key), f'{name}.total.{key}', total['count'])
88
+ for i, day in enumerate(daily):
89
+ if not isinstance(day, dict) or day.get('date') != (start + timedelta(days=i)).isoformat():
90
+ raise ValueError(f'{name} daily dates must align with the period')
91
+ count(day.get('count'), f'{name}.daily.count')
92
+ if 'share' not in day:
93
+ raise ValueError(f'{name}.daily.share is required')
94
+ rate(day['share'], f'{name}.daily.share')
95
+ matches(day['share'], day['count'] / total['count'] if total['count'] else None, f'{name}.daily.share')
96
+ counts(day.get('hours'), 24, f'{name}.daily.hours')
97
+ counts(day.get('tier_counts'), len(tiers), f'{name}.daily.tier_counts')
98
+ if sum(day['hours']) != day['count'] or sum(day['tier_counts']) != day['count']:
99
+ raise ValueError(f'{name} daily counts do not reconcile')
100
+ for key in ('success', 'fraud'):
101
+ status(day.get(key), f'{name}.daily.{key}', day['count'])
102
+ if (sum(day['count'] for day in daily) != total['count']
103
+ or any(sum(day['hours'][i] for day in daily) != pop['hours'][i] for i in range(24))
104
+ or any(sum(day['tier_counts'][i] for day in daily) != total['tier_counts'][i] for i in range(len(tiers)))):
105
+ raise ValueError(f'{name} totals do not reconcile')
106
+ for key in ('success', 'fraud'):
107
+ for field in ('numerator', 'unknown'):
108
+ if sum(day[key][field] for day in daily) != total[key][field]:
109
+ raise ValueError(f'{name}.{key}.{field} does not reconcile')
110
+
111
+
112
+ def chart(title, labels, series, percent=False, tier_count=None, context=None):
113
+ """Draw supplied values on one shared scale; missing values break lines."""
114
+ max_value = 100 if percent else max([1] + [v for _, values in series for v in values if v is not None])
115
+ width, height, left, top, bottom = max(800, 100 * len(labels) + 80), 260, 60, 20, 200
116
+ def x(i):
117
+ return left + i * (width - left - 20) / max(1, len(labels) - 1)
118
+ def y(v):
119
+ return bottom - (v * (100 if percent else 1)) / max_value * (bottom - top)
120
+ def style(index):
121
+ # The tier controls hue; the population controls line and point shape.
122
+ n = tier_count or len(series)
123
+ color = f'hsl({index % n * 360 / n:.8f}, 80%, 35%)'
124
+ return color, '6 4' if tier_count and index // n else 'none', bool(tier_count and index // n)
125
+
126
+ def marker(color, square, x, y, title=''):
127
+ if square:
128
+ return f'<rect x="{x - 3:.2f}" y="{y - 3:.2f}" width="6" height="6" fill="{color}">{title}</rect>'
129
+ return f'<circle cx="{x:.2f}" cy="{y:.2f}" r="3" fill="{color}">{title}</circle>'
130
+
131
+ def legend(index, name):
132
+ color, dash, square = style(index)
133
+ return (f'<li><svg class="legend-key" viewBox="0 0 32 12" aria-hidden="true">'
134
+ f'<line x1="0" y1="6" x2="32" y2="6" stroke="{color}" stroke-dasharray="{dash}"/>'
135
+ + marker(color, square, 16, 6) + f'</svg>{esc(name)}</li>')
136
+
137
+ out = [f'<section><h3>{esc(title)}</h3>', f'<p>{esc(context)}</p>' if context else '', '<ul class="legend" aria-label="Series legend">',
138
+ *[legend(i, name) for i, (name, _) in enumerate(series)],
139
+ '</ul>', f'<div class="scroll" role="region" aria-label="{esc(title)} chart" tabindex="0">',
140
+ f'<svg style="min-width:{width}px" viewBox="0 0 {width} {height}" role="img" aria-label="{esc(title)}"><title>{esc(title)}</title>',
141
+ f'<line x1="{left}" y1="{bottom}" x2="{width-20}" y2="{bottom}" stroke="black"/>',
142
+ f'<line x1="{left}" y1="{top}" x2="{left}" y2="{bottom}" stroke="black"/>',
143
+ f'<text x="2" y="{top+5}">{esc(display(max_value / 100 if percent else max_value, percent))}</text><text x="35" y="{bottom}">0</text>']
144
+ for i, label in enumerate(labels):
145
+ out.append(f'<text x="{x(i):.2f}" y="220" text-anchor="middle" font-size="9">{esc(label)}</text>')
146
+ for index, (name, values) in enumerate(series):
147
+ color, dash, square = style(index)
148
+ segment = []
149
+ def flush():
150
+ if len(segment) > 1:
151
+ points = ' '.join(segment)
152
+ out.append(f'<polyline fill="none" stroke="{color}" stroke-dasharray="{dash}" points="{points}"/>')
153
+ segment.clear()
154
+ for i, value in enumerate(values):
155
+ if value is None:
156
+ flush()
157
+ continue
158
+ px, py = x(i), y(value)
159
+ segment.append(f'{px:.2f},{py:.2f}')
160
+ out.append(marker(color, square, px, py, f'<title>{esc(name)}: {esc(labels[i])}: {esc(display(value, percent))}</title>'))
161
+ flush()
162
+ out.append('</svg></div><div class="scroll" role="region" aria-label="' + esc(title) + ' data table" tabindex="0"><table><caption>' + esc(title) + '</caption><thead><tr><th scope="col">Period</th>' + ''.join(f'<th scope="col">{esc(name)}</th>' for name, _ in series) + '</tr></thead><tbody>')
163
+ for i, label in enumerate(labels):
164
+ out.append(f'<tr><th scope="row">{esc(label)}</th>' + ''.join(f'<td>{esc(display(values[i], percent))}</td>' for _, values in series) + '</tr>')
165
+ out.append('</tbody></table></div></section>')
166
+ return ''.join(out)
167
+
168
+
169
+ def render(profile):
170
+ validate_profile(profile)
171
+ period = profile['period']
172
+ out = ['<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Group profiles</title>',
173
+ '<style>body{font:16px sans-serif;margin:1em}svg{display:block;width:100%;height:auto}.scroll{max-width:100%;overflow-x:auto}.scroll:focus{outline:2px solid #005a9c;outline-offset:2px}table{border-collapse:collapse;margin:1em 0}th,td{border:1px solid #888;padding:.3em;text-align:left;white-space:nowrap}section{margin:2em 0}.legend{display:flex;flex-wrap:wrap;gap:.5em 1em;list-style:none;padding:0}.legend-key{display:inline-block;width:2em;height:.75em;margin-right:.4em;vertical-align:middle}</style></head><body>',
174
+ f'<h1>Group profiles</h1><p>{esc(period["start"])} to {esc(period["end"])} inclusive; timezone {esc(period["timezone"])}</p>']
175
+ for group in profile['groups']:
176
+ out.append(f'<article><h2>Group {esc(group["id"])}</h2><p>Scope: {esc(json.dumps(group["scope"], ensure_ascii=False, sort_keys=True))}</p>')
177
+ populations = [('Group', group['group']), ('Out-group', group['out_group'])]
178
+ days = [d['date'] for d in group['group']['daily']]
179
+ tiers = group['tiers']['tiers']
180
+ series = [(f'{name}: {tier["label"]}', [day['tier_counts'][i] for day in pop['daily']]) for name, pop in populations for i, tier in enumerate(tiers)]
181
+ out.append(chart('Daily amount-tier counts', days, series, tier_count=len(tiers)))
182
+ for key, title, percent in [('count', 'Daily transaction counts', False), ('share', 'Daily shares', True)]:
183
+ context = (f"Daily share = daily count / that population's own whole-period count. "
184
+ f"Group total: {group['group']['total']['count']}; out-group total: {group['out_group']['total']['count']}."
185
+ if key == 'share' else None)
186
+ out.append(chart(title, days, [(name, [d[key] for d in pop['daily']]) for name, pop in populations], percent, context=context))
187
+ for key, title in [('success', 'Daily success rates'), ('fraud', 'Daily suspected-fraud rates')]:
188
+ context = ('Daily rate = status numerator / the same population’s daily transaction count; '
189
+ 'N/A if the count is zero or any status is unknown. Denominators are in the status detail table.')
190
+ out.append(chart(title, days, [(name, [d[key]['rate'] for d in pop['daily']]) for name, pop in populations], True, context=context))
191
+ out.append(chart(title + ' status coverage', days, [(name, [d[key]['coverage'] for d in pop['daily']]) for name, pop in populations], True, context='Coverage = known statuses / the same population’s daily transaction count; N/A for an empty population.'))
192
+ out.append('<div class="scroll" role="region" aria-label="' + esc(title) + ' status detail" tabindex="0"><table><caption>' + esc(title) + ' status detail</caption><thead><tr><th scope="col">Date</th><th scope="col">Population</th><th scope="col">Numerator</th><th scope="col">Denominator</th><th scope="col">Unknown</th></tr></thead><tbody>')
193
+ for name, pop in populations:
194
+ for day in pop['daily']:
195
+ r = day[key]
196
+ out.append(f'<tr><th scope="row">{esc(day["date"])}</th><td>{esc(name)}</td><td>{esc(r["numerator"])}</td><td>{esc(r["denominator"])}</td><td>{esc(r["unknown"])}</td></tr>')
197
+ out.append('</tbody></table></div>')
198
+ hours = [f'{i:02d}' for i in range(24)]
199
+ out.append(chart('24-hour transaction counts', hours, [(name, pop['hours']) for name, pop in populations]))
200
+ out.append('<div class="scroll" role="region" aria-label="Date-by-hour counts" tabindex="0"><table><caption>Date-by-hour counts</caption><thead><tr><th scope="col">Date</th><th scope="col">Population</th>' + ''.join(f'<th scope="col">{h}</th>' for h in hours) + '</tr></thead><tbody>')
201
+ for name, pop in populations:
202
+ for day in pop['daily']:
203
+ out.append(f'<tr><th scope="row">{esc(day["date"])}</th><td>{esc(name)}</td>' + ''.join(f'<td>{esc(n)}</td>' for n in day['hours']) + '</tr>')
204
+ out.append('</tbody></table></div></article>')
205
+ return ''.join(out) + '</body></html>'
206
+
207
+
208
+ def main():
209
+ parser = argparse.ArgumentParser(description=__doc__)
210
+ parser.add_argument('profile'); parser.add_argument('output'); parser.add_argument('--overwrite', action='store_true')
211
+ args = parser.parse_args()
212
+ with open(args.profile, encoding='utf-8') as f:
213
+ page = render(json.load(f))
214
+ write_text(args.output, page, overwrite=args.overwrite)
215
+
216
+
217
+ if __name__ == '__main__':
218
+ main()
@@ -0,0 +1,243 @@
1
+ """Validated transaction and membership boundary shared by TLD command-line tools."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import tempfile
8
+ from datetime import date, datetime, timezone
9
+ from decimal import Decimal, InvalidOperation
10
+ from pathlib import Path
11
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
12
+
13
+ VIEWS = ("usd", "bin_country")
14
+ REQUIRED = ("transaction_id", "transaction_time", "currency", "amount", "mcc",
15
+ "billing_descriptor", "mid", "card_scheme", "card_bin", "card_issuer",
16
+ "card_bin_country")
17
+
18
+
19
+ def _string(value, field):
20
+ if not isinstance(value, str) or not value.strip():
21
+ raise ValueError(f"{field} must be a nonempty string")
22
+ return value.strip()
23
+
24
+
25
+ def decimal_string(value, field="amount"):
26
+ if not isinstance(value, str):
27
+ raise ValueError(f"{field} must be a decimal string")
28
+ try:
29
+ amount = Decimal(value)
30
+ except InvalidOperation as exc:
31
+ raise ValueError(f"{field} must be a finite nonnegative decimal") from exc
32
+ if not amount.is_finite() or amount < 0:
33
+ raise ValueError(f"{field} must be a finite nonnegative decimal")
34
+ if not amount:
35
+ return "0"
36
+ text = format(amount, "f")
37
+ return text.rstrip("0").rstrip(".") if "." in text else text
38
+
39
+
40
+ def _currency(value, field="currency"):
41
+ value = _string(value, field).upper()
42
+ if len(value) != 3 or not value.isascii() or not value.isalpha():
43
+ raise ValueError(f"{field} must be a three-letter currency code")
44
+ return value
45
+
46
+
47
+ def normalize_descriptor(value):
48
+ return " ".join(_string(value, "billing_descriptor").split()).casefold()
49
+
50
+
51
+ def _period(value):
52
+ if not isinstance(value, dict) or set(value) != {"start", "end", "timezone"}:
53
+ raise ValueError("period requires start, end and timezone")
54
+ try:
55
+ start = date.fromisoformat(value["start"])
56
+ end = date.fromisoformat(value["end"])
57
+ zone = ZoneInfo(value["timezone"])
58
+ except (ValueError, TypeError, KeyError, ZoneInfoNotFoundError) as exc:
59
+ raise ValueError("period requires ISO dates and an IANA timezone") from exc
60
+ if start.isoformat() != value["start"] or end.isoformat() != value["end"] or end < start:
61
+ raise ValueError("period dates must be ordered YYYY-MM-DD dates")
62
+ return dict(value), start, end, zone
63
+
64
+
65
+ def _timestamp(value):
66
+ value = _string(value, "transaction_time")
67
+ try:
68
+ dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
69
+ except ValueError as exc:
70
+ raise ValueError("transaction_time must be an ISO timestamp") from exc
71
+ if dt.tzinfo is None or dt.utcoffset() is None:
72
+ raise ValueError("transaction_time must include an explicit UTC offset")
73
+ return dt.astimezone(timezone.utc)
74
+
75
+
76
+ def _transaction(raw):
77
+ if not isinstance(raw, dict):
78
+ raise ValueError("transaction must be an object")
79
+ for field in REQUIRED:
80
+ if field not in raw:
81
+ raise ValueError(f"missing required field: {field}")
82
+ row = {field: _string(raw[field], field) for field in REQUIRED
83
+ if field not in ("card_bin_country", "amount", "currency", "transaction_time")}
84
+ row["billing_descriptor"] = normalize_descriptor(row["billing_descriptor"])
85
+ row["transaction_time"] = _timestamp(raw["transaction_time"]).isoformat()
86
+ row["currency"] = _currency(raw["currency"])
87
+ row["amount"] = decimal_string(raw["amount"])
88
+ country = raw["card_bin_country"]
89
+ row["card_bin_country"] = _string(country, "card_bin_country").upper() if country is not None else None
90
+ for status in ("authorized", "suspected_fraud"):
91
+ value = raw.get(status)
92
+ if value is not None and type(value) is not bool:
93
+ raise ValueError(f"{status} must be true, false or null")
94
+ row[status] = value
95
+ views = raw.get("views")
96
+ if not isinstance(views, dict) or set(views) != set(VIEWS):
97
+ raise ValueError("views must contain exactly usd and bin_country")
98
+ row["views"] = {}
99
+ for name in VIEWS:
100
+ view = views[name]
101
+ if not isinstance(view, dict):
102
+ raise ValueError(f"{name} view must be an amount or explicit exclusion")
103
+ if set(view) == {"excluded_reason"}:
104
+ row["views"][name] = {"excluded_reason": _string(view["excluded_reason"], "excluded_reason")}
105
+ continue
106
+ if set(view) != {"currency", "amount"}:
107
+ raise ValueError(f"{name} view requires currency and amount, or excluded_reason")
108
+ currency = _currency(view["currency"])
109
+ if name == "usd" and currency != "USD":
110
+ raise ValueError("usd view must target USD")
111
+ if name == "bin_country" and country is None:
112
+ raise ValueError("missing BIN country requires an excluded bin_country view")
113
+ row["views"][name] = {"currency": currency, "amount": decimal_string(view["amount"])}
114
+ return row
115
+
116
+
117
+ def dataset_fingerprint(data):
118
+ bound = {"schema_version": 1, "period": data["period"],
119
+ "transactions": sorted(data["transactions"], key=lambda row: row["transaction_id"])}
120
+ encoded = json.dumps(bound, sort_keys=True, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
121
+ return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
122
+
123
+
124
+ def validate_dataset(payload):
125
+ if not isinstance(payload, dict) or type(payload.get("schema_version")) is not int or payload["schema_version"] != 1:
126
+ raise ValueError("unsupported dataset schema_version")
127
+ period, start, end, zone = _period(payload.get("period"))
128
+ source = payload.get("transactions")
129
+ if not isinstance(source, list):
130
+ raise ValueError("transactions must be an array")
131
+ by_id = {}
132
+ duplicates = 0
133
+ for raw in source:
134
+ row = _transaction(raw)
135
+ key = row["transaction_id"]
136
+ if key in by_id:
137
+ if by_id[key] != row:
138
+ raise ValueError(f"conflicting records for transaction ID {key}")
139
+ duplicates += 1
140
+ else:
141
+ by_id[key] = row
142
+ selected = [row for row in by_id.values()
143
+ if start <= _timestamp(row["transaction_time"]).astimezone(zone).date() <= end]
144
+ selected.sort(key=lambda row: row["transaction_id"])
145
+ exclusions = {view: {} for view in VIEWS}
146
+ for row in selected:
147
+ for view in VIEWS:
148
+ reason = row["views"][view].get("excluded_reason")
149
+ if reason:
150
+ exclusions[view][reason] = exclusions[view].get(reason, 0) + 1
151
+ data = {"schema_version": 1, "period": period, "transactions": selected,
152
+ "census": {"source_rows": len(source), "duplicate_rows": duplicates,
153
+ "out_of_period": len(by_id) - len(selected), "transactions": len(selected),
154
+ "view_exclusions": exclusions}}
155
+ data["dataset_sha256"] = dataset_fingerprint(data)
156
+ return data
157
+
158
+
159
+ def load_dataset(path):
160
+ return validate_dataset(json.loads(Path(path).read_text(encoding="utf-8")))
161
+
162
+
163
+ def view_rows(data):
164
+ zone = ZoneInfo(data["period"]["timezone"])
165
+ output = []
166
+ for tx in data["transactions"]:
167
+ local = _timestamp(tx["transaction_time"]).astimezone(zone)
168
+ for name in VIEWS:
169
+ converted = tx["views"][name]
170
+ if "excluded_reason" in converted:
171
+ continue
172
+ row = {key: value for key, value in tx.items() if key != "views"}
173
+ row.update({"view": name, "currency": converted["currency"], "amount": converted["amount"],
174
+ "charge_currency": tx["currency"], "charge_amount": tx["amount"],
175
+ "descriptor": tx["billing_descriptor"], "date": local.date().isoformat(), "hour": local.hour})
176
+ output.append(row)
177
+ return output
178
+
179
+
180
+ def scope_of(row):
181
+ return {key: row[key] for key in ("mid", "descriptor", "view", "currency")}
182
+
183
+
184
+ def validate_groups(data, payload):
185
+ if not isinstance(payload, dict) or type(payload.get("schema_version")) is not int or payload["schema_version"] != 1:
186
+ raise ValueError("unsupported group schema_version")
187
+ if payload.get("period") != data["period"] or payload.get("dataset_sha256") != dataset_fingerprint(data):
188
+ raise ValueError("group manifest period or dataset digest is stale or mismatched")
189
+ groups = payload.get("groups")
190
+ if not isinstance(groups, list):
191
+ raise ValueError("groups must be an array")
192
+ eligible = {}
193
+ for row in view_rows(data):
194
+ scope = scope_of(row)
195
+ key = tuple(scope[k] for k in ("mid", "descriptor", "view", "currency"))
196
+ eligible.setdefault(key, set()).add(row["transaction_id"])
197
+ seen = set()
198
+ for group in groups:
199
+ if not isinstance(group, dict):
200
+ raise ValueError("group must be an object")
201
+ gid = _string(group.get("id"), "group id")
202
+ if gid in seen:
203
+ raise ValueError(f"duplicate group ID {gid}")
204
+ seen.add(gid)
205
+ if group.get("pattern") not in ("concentration", "escalating", "pricing"):
206
+ raise ValueError("unsupported group pattern")
207
+ scope = group.get("scope")
208
+ if not isinstance(scope, dict) or set(scope) != {"mid", "descriptor", "view", "currency"}:
209
+ raise ValueError("group scope requires mid, descriptor, view and currency")
210
+ fields = ("mid", "descriptor", "view", "currency")
211
+ key = tuple(_string(scope[k], k) for k in fields)
212
+ if any(scope[field] != value for field, value in zip(fields, key)):
213
+ raise ValueError("group scope must use canonical values without surrounding whitespace")
214
+ members = group.get("member_ids")
215
+ if not isinstance(members, list) or not members or any(not isinstance(m, str) or not m for m in members):
216
+ raise ValueError("group member_ids must be a nonempty list of IDs")
217
+ if len(set(members)) != len(members):
218
+ raise ValueError("duplicate member IDs in group")
219
+ if not set(members) <= eligible.get(key, set()):
220
+ raise ValueError(f"group {gid} has unknown, out-of-scope or excluded member IDs")
221
+ return groups
222
+
223
+
224
+ def write_json(path, payload, overwrite=False):
225
+ text = json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False) + "\n"
226
+ write_text(path, text, overwrite)
227
+
228
+
229
+ def write_text(path, text, overwrite=False):
230
+ path = Path(path)
231
+ path.parent.mkdir(parents=True, exist_ok=True)
232
+ temporary = None
233
+ try:
234
+ with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent, delete=False) as handle:
235
+ temporary = Path(handle.name)
236
+ handle.write(text)
237
+ if overwrite:
238
+ os.replace(temporary, path)
239
+ else:
240
+ os.link(temporary, path)
241
+ finally:
242
+ if temporary is not None:
243
+ temporary.unlink(missing_ok=True)
@@ -1,70 +0,0 @@
1
- ---
2
- name: contributing
3
- description: "The conventions a repo's CONTRIBUTING.md holds, starting with tracker routing: which tracker an issue belongs in (GitHub for engineering, Linear for product), duplicate search, and linking back to the source requirement. Use before creating any issue, spec, epic, ticket or sub-issue on GitHub or Linear, including inside /to-spec, /to-tickets and /triage, and whenever work that started from a Linear issue turns into engineering work. Also writes the rules as the Issue tracking section of the repo's CONTRIBUTING.md. NOT for delivering a PR (github-flow) or splitting work into slices (to-tickets)."
4
- release-version: 0.44.1
5
- ---
6
- **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
- # Contributing
8
-
9
- Rules every engineering skill follows, whatever skill or harness is running, kept as sections of the
10
- repo's `CONTRIBUTING.md`. Each rule is the authority; another skill's "publish to the configured
11
- tracker" means the tracker these rules pick.
12
-
13
- ## First use in a repo
14
-
15
- Scripts live under **this skill's directory**: set `SKILL_DIR` to the directory you read this file
16
- from, never your CWD. On the first trigger in a repo, write the `## Issue tracking` section into its
17
- `CONTRIBUTING.md`, creating the file when it is missing:
18
-
19
- ```sh
20
- node "$SKILL_DIR/scripts/persist-conventions.mjs" --repo-root <repo-root> --if-missing --write \
21
- [--linear-project "<Linear project>"]... [--github-repo <owner/name>]
22
- ```
23
-
24
- - **Pass every Linear project whose resources link to this repo.** Example: the `Checkout Risk`
25
- project lists `https://github.com/acme/widgets`, so pass `--linear-project "Checkout Risk"`.
26
- - **Pass `--github-repo` only when `origin` is not the GitHub repo.** Example: exit 2 with
27
- `no GitHub origin remote` means pass it.
28
- - **`--if-missing` never touches an existing section.** To rewrite it, run without `--if-missing`.
29
- - **`AGENTS.md` indexes `CONTRIBUTING.md`.** The script adds a `## Contributing` pointer unless
30
- `AGENTS.md` already names `CONTRIBUTING.md`, creates a missing `AGENTS.md`, and creates a missing
31
- `CLAUDE.md` as a symlink to `AGENTS.md`, even when the section already exists.
32
- - Report each file's result line; commit with the change you are making, or leave it for the user
33
- when you make no other change.
34
-
35
- ## Tracker routing
36
-
37
- | Work item | Tracker |
38
- |---|---|
39
- | Product goals, business requirements, priorities, milestones | Linear |
40
- | Consulting engagements, reports, research deliverables | Linear |
41
- | Engineering specs and implementation epics | GitHub |
42
- | Implementation tickets, technical investigations, refactors, engineering bugs | GitHub |
43
- | Delivery progress for stakeholders | A summary in Linear that links to GitHub |
44
-
45
- Before you create an issue:
46
-
47
- 1. **Classify the work; the tracker follows the work, not where the request came from.** Example:
48
- `/to-spec` run on a conversation about DEV-123 publishes a GitHub issue.
49
- 2. **Resolve the GitHub repo in this order: the repo's `CONTRIBUTING.md` Issue tracking section, the
50
- GitHub links in the source Linear project's resources, then ask the user. Never fall back to
51
- Linear.** Example: a project linking two repos gets the repo the work changes; a project linking
52
- none gets a question, not a Linear ticket.
53
- 3. **Search before you create.** Example: `gh issue list --repo <owner/name> --state all --search
54
- "<keywords>"`; reuse or comment on a match.
55
- 4. **A Linear issue is a source reference, never a parent for implementation tickets.** Example: put
56
- `Source: <Linear URL>` in the GitHub epic body and file the tickets as GitHub sub-issues of the
57
- epic.
58
- 5. **Link back once per epic.** Example: add the GitHub epic URL to the Linear requirement as a
59
- link; never copy the GitHub tickets into Linear.
60
- 6. **Report progress in Linear as a summary; acceptance stays with the requester.** Example: when the
61
- last sub-issue closes, comment `Engineering complete: <epic URL>` on the Linear requirement and
62
- leave its status alone.
63
- 7. **An explicit destination from the user wins.** Example: "file this in Linear" files it in Linear.
64
-
65
- ## Common wrong answers
66
-
67
- - "The request came from Linear, so the tickets go in Linear." The work type decides, rule 1.
68
- - "No tracker is configured, so use the one the conversation used." Resolve the repo, rule 2.
69
- - "Mirror the epic into Linear so stakeholders see it." Post a summary with a link, rule 5.
70
- - "All sub-issues closed, so the Linear requirement is done." Engineering complete is not accepted, rule 6.
@@ -1,114 +0,0 @@
1
- ---
2
- name: fraud-analysis
3
- description: >
4
- Analyze tabular payment, transfer, collection, payout, account, or fraud-case
5
- data (CSV, XLSX, TXT) to find interpretable features and operable risk rules.
6
- Supports third-party fraud (including stolen credentials and account takeover),
7
- first-party fraud or deliberate account abuse, and authorized payment scams
8
- (including APP scams), across card payments, collections, bank transfers, and
9
- payouts. Produces temporal holdout metrics, domain-appropriate actions, strategy
10
- impact, and a business report. Use for fraud, scam, mule, account-takeover,
11
- chargeback, dispute, transfer, collection, or payout risk analysis even when the
12
- user does not name this skill.
13
- release-version: 0.44.1
14
- ---
15
- **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
16
- # Fraud Analysis
17
-
18
- Analyze fraud through composable dimensions rather than treating every case as
19
- card fraud:
20
-
21
- - **Archetype** — the genuine customer's relationship to the fraud
22
- - **Mechanism** — how compromise, deception, or abuse occurred
23
- - **Context** — the product or money-movement surface where a rule fires
24
-
25
- Run this **10-step** loop:
26
-
27
- 0. Pre-flight → 1. Profile → 2. Label → 3. Time split → 4. Engineer features →
28
- 5. Shortlist → 6. Score rules → 7. Map actions + packages → 8. Write report →
29
- 9. Review
30
-
31
- **Produce:** operable rules and portfolio impact in a business report.
32
- **Do not produce:** a black-box model as the main answer.
33
-
34
- > Paths under `references/` are relative to this skill directory. Step detail:
35
- > [references/workflow.md](references/workflow.md).
36
-
37
- ## Required references
38
-
39
- | File | When |
40
- |---|---|
41
- | [references/workflow.md](references/workflow.md) | Every step — full procedure |
42
- | [references/taxonomy.md](references/taxonomy.md) | Step 1; select archetype, mechanism, and context |
43
- | [references/feature-engineering.md](references/feature-engineering.md) | Step 4; shared decision-time rules |
44
- | [references/report-template.md](references/report-template.md) | Step 8; common report contract and overlay selection |
45
- | [references/terminology.md](references/terminology.md) | Step 8; fixed terms and language proofread |
46
- | [references/review-checklist.md](references/review-checklist.md) | Step 9; common and selected-profile checks |
47
-
48
- After Step 1, read only the selected profile references:
49
-
50
- | Dimension | References |
51
- |---|---|
52
- | Archetype | `references/archetypes/<archetype>.md` |
53
- | Mechanism | `references/mechanisms/<mechanism>.md` when present |
54
- | Context | `references/contexts/<context>.md` for every selected context |
55
-
56
- Current packs: archetypes `third-party-fraud`, `first-party-fraud`,
57
- `authorized-payment-scam`; mechanism `account-takeover`; contexts
58
- `card-payment`, `payment-collection`, `bank-transfer`, `payout`.
59
-
60
- ## Progress
61
-
62
- Track a task plan. One item is `in_progress` at a time.
63
-
64
- 1. Pass pre-flight
65
- 2. Select analysis profile
66
- 3. Formalize label
67
- 4. Freeze time split
68
- 5. Engineer candidate features
69
- 6. Select shortlist
70
- 7. Score candidate rules
71
- 8. Map actions and packages
72
- 9. Write business report and proofread language
73
- 10. Independent review
74
-
75
- Suggested notes: `preflight=…`, `profile=…`, `label=…`, `cut=…`,
76
- `allowed/excluded=…`, `shortlist=…`, `N rules, holdout|in-sample`,
77
- `packages=…`, `report=…`, `verdict=…`.
78
-
79
- ## Input
80
-
81
- CSV / XLSX / TXT; one row per event or case; optional outcome column. A dataset
82
- may contain more than one event type, but every scored rule needs a declared
83
- event grain and decision point.
84
-
85
- ## Workflow
86
-
87
- Follow [references/workflow.md](references/workflow.md) for each step. The map:
88
-
89
- | Step | Do | Gate |
90
- |---|---|---|
91
- | 0 Pre-flight | Label provenance; pass / needs-user-input / descriptive-only / blocked | No supervised metrics until pass |
92
- | 1 Profile | Archetype + mechanism(s) + context(s) from taxonomy | Read selected packs before Step 2 |
93
- | 2 Label | Formalize positive / negative / unlabeled | Both classes defined before scoring |
94
- | 3 Time split | Freeze train/holdout before features | No random shuffle as headline |
95
- | 4 Features | Engineer candidates at decision time | Train-only thresholds |
96
- | 5 Shortlist | Lift / support / stability / operability | Train only |
97
- | 6 Score | occurrence → precision → recall on holdout | Headline = holdout |
98
- | 7 Actions | Map to context packs + strategy packages | Freeze packages before report |
99
- | 8 Report | Business report per template; grep terminology banned strings | No review process in the file |
100
- | 9 Review | Checklist + selected packs | Prefer fresh independent reviewer |
101
-
102
- ## Output contract
103
-
104
- | Item | Requirement |
105
- |---|---|
106
- | Analysis profile | Archetype, role, consent, technical authentication, mechanism, context, confidence, grain, decision point |
107
- | Rule metrics | occurrence → precision → recall; headline window identified |
108
- | Strategy coverage | triggered count + rate and amount + share, with currency |
109
- | Domain impact | Measures and denominators from selected context packs only |
110
- | Uncertainty | Proxy labels, immature outcomes, weak identity links, and unresolved attribution stated |
111
- | Report file | Business content only; no reviewer model, verdict, or repair log |
112
-
113
- Steps 0–7 are analysis and its entry gate. Step 8 writes the report. Step 9
114
- reviews it.