@1aboveio/skills 0.0.0-stage → 1.0.1

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 (633) hide show
  1. package/README.md +30 -2
  2. package/bin/cli.mjs +14 -0
  3. package/bin/select-core.mjs +172 -0
  4. package/package.json +146 -4
  5. package/skills/app-dev/airflow/LICENSE +3 -0
  6. package/skills/app-dev/airflow/SKILL.md +199 -0
  7. package/skills/app-dev/airflow/release-check.mjs +93 -0
  8. package/skills/app-dev/android-foundation/LICENSE +3 -0
  9. package/skills/app-dev/android-foundation/SKILL.md +192 -0
  10. package/skills/app-dev/android-foundation/assets/android-ci-job.yml +217 -0
  11. package/skills/app-dev/android-foundation/references/ci.md +87 -0
  12. package/skills/app-dev/android-foundation/references/gotchas.md +221 -0
  13. package/skills/app-dev/android-foundation/references/setup.md +164 -0
  14. package/skills/app-dev/android-foundation/release-check.mjs +93 -0
  15. package/skills/app-dev/android-foundation/scripts/device-run.sh +176 -0
  16. package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +172 -0
  17. package/skills/app-dev/app-debug/LICENSE +3 -0
  18. package/skills/app-dev/app-debug/SKILL.md +110 -0
  19. package/skills/app-dev/app-debug/references/common-errors.md +128 -0
  20. package/skills/app-dev/app-debug/release-check.mjs +93 -0
  21. package/skills/app-dev/better-auth/LICENSE +3 -0
  22. package/skills/app-dev/better-auth/SKILL.md +598 -0
  23. package/skills/app-dev/better-auth/references/feishu-api.md +270 -0
  24. package/skills/app-dev/better-auth/release-check.mjs +93 -0
  25. package/skills/app-dev/data-catalog/LICENSE +3 -0
  26. package/skills/app-dev/data-catalog/SKILL.md +207 -0
  27. package/skills/app-dev/data-catalog/agents/openai.yaml +3 -0
  28. package/skills/app-dev/data-catalog/references/data-catalog-conventions.md +76 -0
  29. package/skills/app-dev/data-catalog/references/field-definitions.md +123 -0
  30. package/skills/app-dev/data-catalog/references/format.md +130 -0
  31. package/skills/app-dev/data-catalog/release-check.mjs +93 -0
  32. package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +36 -0
  33. package/skills/app-dev/data-catalog/scripts/data_catalog.py +1435 -0
  34. package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +117 -0
  35. package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +17 -0
  36. package/skills/app-dev/data-catalog/scripts/test_data_catalog.py +603 -0
  37. package/skills/app-dev/ios-foundation/LICENSE +3 -0
  38. package/skills/app-dev/ios-foundation/SKILL.md +184 -0
  39. package/skills/app-dev/ios-foundation/references/setup.md +243 -0
  40. package/skills/app-dev/ios-foundation/release-check.mjs +93 -0
  41. package/skills/app-dev/monorepo/LICENSE +3 -0
  42. package/skills/app-dev/monorepo/SKILL.md +462 -0
  43. package/skills/app-dev/monorepo/release-check.mjs +93 -0
  44. package/skills/app-dev/nextjs-fullstack/LICENSE +3 -0
  45. package/skills/app-dev/nextjs-fullstack/SKILL.md +211 -0
  46. package/skills/app-dev/nextjs-fullstack/conventions.md +318 -0
  47. package/skills/app-dev/nextjs-fullstack/frontend-conventions.md +61 -0
  48. package/skills/app-dev/nextjs-fullstack/nextjs16.md +287 -0
  49. package/skills/app-dev/nextjs-fullstack/release-check.mjs +93 -0
  50. package/skills/app-dev/nextjs-fullstack/server-actions.md +409 -0
  51. package/skills/app-dev/prisma-setup/LICENSE +3 -0
  52. package/skills/app-dev/prisma-setup/SKILL.md +181 -0
  53. package/skills/app-dev/prisma-setup/nextjs.md +258 -0
  54. package/skills/app-dev/prisma-setup/release-check.mjs +93 -0
  55. package/skills/app-dev/prisma-setup/turborepo.md +301 -0
  56. package/skills/app-dev/pyspark/LICENSE +3 -0
  57. package/skills/app-dev/pyspark/SKILL.md +127 -0
  58. package/skills/app-dev/pyspark/assets/templates/etl.py +748 -0
  59. package/skills/app-dev/pyspark/assets/templates/utils/__init__.py +1 -0
  60. package/skills/app-dev/pyspark/assets/templates/utils/hudi_metadata.py +14 -0
  61. package/skills/app-dev/pyspark/references/diagnosis-and-profiling.md +169 -0
  62. package/skills/app-dev/pyspark/references/etl-contract.md +129 -0
  63. package/skills/app-dev/pyspark/references/production-validation.md +131 -0
  64. package/skills/app-dev/pyspark/references/reconciliation.md +38 -0
  65. package/skills/app-dev/pyspark/references/transformation-design.md +190 -0
  66. package/skills/app-dev/pyspark/references/velocity-feature-calculation.md +193 -0
  67. package/skills/app-dev/pyspark/release-check.mjs +93 -0
  68. package/skills/app-dev/pyspark/scripts/spark_eventlog_summary.py +223 -0
  69. package/skills/app-dev/python-backend/LICENSE +3 -0
  70. package/skills/app-dev/python-backend/SKILL.md +325 -0
  71. package/skills/app-dev/python-backend/assets/uv-install.sh +48 -0
  72. package/skills/app-dev/python-backend/references/uv-installs.md +83 -0
  73. package/skills/app-dev/python-backend/release-check.mjs +93 -0
  74. package/skills/app-dev/shadcn/LICENSE +3 -0
  75. package/skills/app-dev/shadcn/SKILL.md +120 -0
  76. package/skills/app-dev/shadcn/assets/shadcn-small.png +0 -0
  77. package/skills/app-dev/shadcn/assets/shadcn.png +0 -0
  78. package/skills/app-dev/shadcn/cli.md +411 -0
  79. package/skills/app-dev/shadcn/customization.md +224 -0
  80. package/skills/app-dev/shadcn/mcp.md +101 -0
  81. package/skills/app-dev/shadcn/release-check.mjs +93 -0
  82. package/skills/app-dev/shadcn/rules/base-vs-radix.md +323 -0
  83. package/skills/app-dev/shadcn/rules/component-selection.md +67 -0
  84. package/skills/app-dev/shadcn/rules/composition.md +195 -0
  85. package/skills/app-dev/shadcn/rules/data-table.md +201 -0
  86. package/skills/app-dev/shadcn/rules/forms.md +255 -0
  87. package/skills/app-dev/shadcn/rules/icons.md +103 -0
  88. package/skills/app-dev/shadcn/rules/styling.md +167 -0
  89. package/skills/app-dev/zod-v4/LICENSE +3 -0
  90. package/skills/app-dev/zod-v4/SKILL.md +286 -0
  91. package/skills/app-dev/zod-v4/release-check.mjs +93 -0
  92. package/skills/cicd/cloud-build/LICENSE +3 -0
  93. package/skills/cicd/cloud-build/SKILL.md +707 -0
  94. package/skills/cicd/cloud-build/release-check.mjs +93 -0
  95. package/skills/cicd/cloud-debug/LICENSE +3 -0
  96. package/skills/cicd/cloud-debug/SKILL.md +315 -0
  97. package/skills/cicd/cloud-debug/references/build-failures.md +238 -0
  98. package/skills/cicd/cloud-debug/references/deploy-failures.md +376 -0
  99. package/skills/cicd/cloud-debug/references/pipeline-failures.md +378 -0
  100. package/skills/cicd/cloud-debug/release-check.mjs +93 -0
  101. package/skills/cicd/cloud-deploy/LICENSE +3 -0
  102. package/skills/cicd/cloud-deploy/SKILL.md +230 -0
  103. package/skills/cicd/cloud-deploy/references/config-templates.md +257 -0
  104. package/skills/cicd/cloud-deploy/release-check.mjs +93 -0
  105. package/skills/cicd/docker/LICENSE +3 -0
  106. package/skills/cicd/docker/SKILL.md +151 -0
  107. package/skills/cicd/docker/release-check.mjs +93 -0
  108. package/skills/cicd/github-actions/LICENSE +3 -0
  109. package/skills/cicd/github-actions/SKILL.md +111 -0
  110. package/skills/cicd/github-actions/references/audit-profile-optimize.md +150 -0
  111. package/skills/cicd/github-actions/references/ci-case-studies.md +141 -0
  112. package/skills/cicd/github-actions/references/conditional-ci.md +214 -0
  113. package/skills/cicd/github-actions/references/debugging.md +13 -0
  114. package/skills/cicd/github-actions/references/pipeline.md +87 -0
  115. package/skills/cicd/github-actions/references/post-deploy-and-publishing.md +33 -0
  116. package/skills/cicd/github-actions/references/runners.md +253 -0
  117. package/skills/cicd/github-actions/references/security.md +29 -0
  118. package/skills/cicd/github-actions/references/speed-and-audit.md +115 -0
  119. package/skills/cicd/github-actions/references/two-pass-reading.md +92 -0
  120. package/skills/cicd/github-actions/release-check.mjs +93 -0
  121. package/skills/cicd/github-actions/scripts/ci-run-audit-core.mjs +612 -0
  122. package/skills/cicd/github-actions/scripts/ci-run-audit.mjs +4 -0
  123. package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +637 -0
  124. package/skills/cicd/github-flow/LICENSE +3 -0
  125. package/skills/cicd/github-flow/SKILL.md +123 -0
  126. package/skills/cicd/github-flow/references/ci-conventions.md +14 -0
  127. package/skills/cicd/github-flow/release-check.mjs +93 -0
  128. package/skills/cicd/github-flow/scripts/flow-core.mjs +105 -0
  129. package/skills/cicd/github-flow/scripts/flow.mjs +81 -0
  130. package/skills/cicd/github-flow/scripts/flow.test.mjs +114 -0
  131. package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +117 -0
  132. package/skills/cicd/github-flow/scripts/persist-conventions.mjs +49 -0
  133. package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +108 -0
  134. package/skills/cicd/google-cloud/LICENSE +3 -0
  135. package/skills/cicd/google-cloud/SKILL.md +119 -0
  136. package/skills/cicd/google-cloud/references/gcs.md +469 -0
  137. package/skills/cicd/google-cloud/references/iam.md +451 -0
  138. package/skills/cicd/google-cloud/references/project.md +349 -0
  139. package/skills/cicd/google-cloud/references/secrets.md +336 -0
  140. package/skills/cicd/google-cloud/references/vpc.md +312 -0
  141. package/skills/cicd/google-cloud/release-check.mjs +93 -0
  142. package/skills/cicd/google-cloud/scripts/create-sa.sh +36 -0
  143. package/skills/cicd/google-cloud/scripts/gcp-config.sh +31 -0
  144. package/skills/cicd/google-cloud/scripts/grant-iap.sh +41 -0
  145. package/skills/cicd/google-cloud/scripts/setup-secrets.sh +48 -0
  146. package/skills/cicd/mergify/LICENSE +3 -0
  147. package/skills/cicd/mergify/SKILL.md +102 -0
  148. package/skills/cicd/mergify/assets/templates/mergify.yml +215 -0
  149. package/skills/cicd/mergify/assets/templates/ruleset.json +46 -0
  150. package/skills/cicd/mergify/references/branch-protection.md +282 -0
  151. package/skills/cicd/mergify/references/configuration.md +274 -0
  152. package/skills/cicd/mergify/references/diagnosis.md +109 -0
  153. package/skills/cicd/mergify/references/stacks.md +84 -0
  154. package/skills/cicd/mergify/references/traps.md +119 -0
  155. package/skills/cicd/mergify/references/watch-contract.md +274 -0
  156. package/skills/cicd/mergify/release-check.mjs +93 -0
  157. package/skills/cicd/mergify/scripts/audit-core.mjs +208 -0
  158. package/skills/cicd/mergify/scripts/audit.mjs +4 -0
  159. package/skills/cicd/mergify/scripts/audit.test.mjs +201 -0
  160. package/skills/cicd/mergify/scripts/entrypoints.test.mjs +106 -0
  161. package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +30 -0
  162. package/skills/cicd/mergify/scripts/stack-push-core.mjs +258 -0
  163. package/skills/cicd/mergify/scripts/stack-push.mjs +4 -0
  164. package/skills/cicd/mergify/scripts/stack-push.test.mjs +233 -0
  165. package/skills/cicd/mergify/scripts/watch-pr-delivery-core.mjs +1065 -0
  166. package/skills/cicd/mergify/scripts/watch-pr-delivery.mjs +4 -0
  167. package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +1826 -0
  168. package/skills/cicd/podman/LICENSE +3 -0
  169. package/skills/cicd/podman/SKILL.md +71 -0
  170. package/skills/cicd/podman/agents/openai.yaml +4 -0
  171. package/skills/cicd/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
  172. package/skills/cicd/podman/assets/templates/podman-service-override.conf +3 -0
  173. package/skills/cicd/podman/references/compose-compatibility.md +70 -0
  174. package/skills/cicd/podman/references/networking-and-ports.md +74 -0
  175. package/skills/cicd/podman/references/rootless-services-and-sockets.md +156 -0
  176. package/skills/cicd/podman/references/troubleshooting.md +98 -0
  177. package/skills/cicd/podman/release-check.mjs +93 -0
  178. package/skills/content/article-review/LICENSE +3 -0
  179. package/skills/content/article-review/SKILL.md +66 -0
  180. package/skills/content/article-review/references/report-format.md +68 -0
  181. package/skills/content/article-review/references/template-guide.md +79 -0
  182. package/skills/content/article-review/references/template-wechat-payments.md +125 -0
  183. package/skills/content/article-review/release-check.mjs +93 -0
  184. package/skills/content/article-to-image/LICENSE +3 -0
  185. package/skills/content/article-to-image/SKILL.md +91 -0
  186. package/skills/content/article-to-image/references/formats.md +53 -0
  187. package/skills/content/article-to-image/release-check.mjs +93 -0
  188. package/skills/content/flowchart-svg/LICENSE +3 -0
  189. package/skills/content/flowchart-svg/SKILL.md +43 -0
  190. package/skills/content/flowchart-svg/release-check.mjs +93 -0
  191. package/skills/content/flowchart-svg/scripts/export_png.sh +8 -0
  192. package/skills/content/image-gen/LICENSE +3 -0
  193. package/skills/content/image-gen/SKILL.md +192 -0
  194. package/skills/content/image-gen/release-check.mjs +93 -0
  195. package/skills/content/image-gen/scripts/credentials.mjs +217 -0
  196. package/skills/content/image-gen/scripts/credentials.test.mjs +212 -0
  197. package/skills/content/image-gen/scripts/generate.mjs +240 -0
  198. package/skills/content/image-gen/scripts/options.mjs +293 -0
  199. package/skills/content/image-gen/scripts/options.test.mjs +245 -0
  200. package/skills/content/mermaid/LICENSE +3 -0
  201. package/skills/content/mermaid/SKILL.md +635 -0
  202. package/skills/content/mermaid/release-check.mjs +93 -0
  203. package/skills/content/wechat-article-writer/LICENSE +3 -0
  204. package/skills/content/wechat-article-writer/SKILL.md +92 -0
  205. package/skills/content/wechat-article-writer/references/style-selector.md +133 -0
  206. package/skills/content/wechat-article-writer/references/writing-rules.md +152 -0
  207. package/skills/content/wechat-article-writer/release-check.mjs +93 -0
  208. package/skills/content/wechat-article-writer/templates/wechat-comparison.md +52 -0
  209. package/skills/content/wechat-article-writer/templates/wechat-howto.md +51 -0
  210. package/skills/content/wechat-article-writer/templates/wechat-insight.md +50 -0
  211. package/skills/content/wechat-article-writer/templates/wechat-pitfall.md +52 -0
  212. package/skills/content/wechat-publisher/LICENSE +3 -0
  213. package/skills/content/wechat-publisher/SKILL.md +171 -0
  214. package/skills/content/wechat-publisher/references/api.md +105 -0
  215. package/skills/content/wechat-publisher/references/layout-rules.md +57 -0
  216. package/skills/content/wechat-publisher/references/wechat-safe-template.html +86 -0
  217. package/skills/content/wechat-publisher/release-check.mjs +93 -0
  218. package/skills/content/wechat-publisher/scripts/md_to_wechat_html.py +194 -0
  219. package/skills/content/wechat-publisher/scripts/publish.py +239 -0
  220. package/skills/content/wechat-search/LICENSE +3 -0
  221. package/skills/content/wechat-search/SKILL.md +211 -0
  222. package/skills/content/wechat-search/release-check.mjs +93 -0
  223. package/skills/content/wechat-search/scripts/start-chrome.sh +83 -0
  224. package/skills/content/xai-search/LICENSE +3 -0
  225. package/skills/content/xai-search/SKILL.md +79 -0
  226. package/skills/content/xai-search/release-check.mjs +93 -0
  227. package/skills/content/xai-search/scripts/xai_search.sh +187 -0
  228. package/skills/content/xai-search/tests/smoke_test.sh +36 -0
  229. package/skills/core/distill-writing/LICENSE +3 -0
  230. package/skills/core/distill-writing/SKILL.md +74 -0
  231. package/skills/core/distill-writing/references/extraction.md +95 -0
  232. package/skills/core/distill-writing/references/updating.md +46 -0
  233. package/skills/core/distill-writing/release-check.mjs +93 -0
  234. package/skills/core/doc-to-md/LICENSE +3 -0
  235. package/skills/core/doc-to-md/SKILL.md +78 -0
  236. package/skills/core/doc-to-md/release-check.mjs +93 -0
  237. package/skills/core/doc-to-md/scripts/convert.py +157 -0
  238. package/skills/core/editorial-review/LICENSE +3 -0
  239. package/skills/core/editorial-review/SKILL.md +79 -0
  240. package/skills/core/editorial-review/references/checklist.md +94 -0
  241. package/skills/core/editorial-review/release-check.mjs +93 -0
  242. package/skills/core/house-style/LICENSE +3 -0
  243. package/skills/core/house-style/SKILL.md +33 -0
  244. package/skills/core/house-style/references/defaults.md +66 -0
  245. package/skills/core/house-style/references/rules.md +267 -0
  246. package/skills/core/house-style/references/sources.md +27 -0
  247. package/skills/core/house-style/references/style-en.md +131 -0
  248. package/skills/core/house-style/references/style-zh.md +141 -0
  249. package/skills/core/house-style/references/terminology-zh.md +223 -0
  250. package/skills/core/house-style/release-check.mjs +93 -0
  251. package/skills/core/house-style/templates/brd.md +103 -0
  252. package/skills/core/house-style/templates/commercial-proposal.md +93 -0
  253. package/skills/core/house-style/templates/compliance-policy.md +109 -0
  254. package/skills/core/house-style/templates/gap-analysis.md +105 -0
  255. package/skills/core/house-style/templates/general.md +63 -0
  256. package/skills/core/house-style/templates/integration-guide.md +98 -0
  257. package/skills/core/house-style/templates/prd.md +81 -0
  258. package/skills/core/house-style/templates/risk-operations-procedure.md +94 -0
  259. package/skills/core/house-style/templates/technical-spec.md +96 -0
  260. package/skills/core/polish-cn/LICENSE +3 -0
  261. package/skills/core/polish-cn/SKILL.md +102 -0
  262. package/skills/core/polish-cn/references/brief.md +93 -0
  263. package/skills/core/polish-cn/references/models.md +76 -0
  264. package/skills/core/polish-cn/release-check.mjs +93 -0
  265. package/skills/core/polish-cn/scripts/polish-cn.test.mjs +244 -0
  266. package/skills/core/polish-cn/scripts/polish_blocks.py +489 -0
  267. package/skills/core/reg-parser/LICENSE +3 -0
  268. package/skills/core/reg-parser/SKILL.md +212 -0
  269. package/skills/core/reg-parser/references/checklist-schema.md +223 -0
  270. package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.json +1306 -0
  271. package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.md +469 -0
  272. package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.json +634 -0
  273. package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.md +233 -0
  274. package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.json +2194 -0
  275. package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.md +771 -0
  276. package/skills/core/reg-parser/references/checklists/kyc-aml-checklist.md +258 -0
  277. package/skills/core/reg-parser/references/checklists/visa-vars-checklist.md +1058 -0
  278. package/skills/core/reg-parser/references/mandates/hk-amlo-cap615.md +389 -0
  279. package/skills/core/reg-parser/references/mandates/visa-vars.md +205 -0
  280. package/skills/core/reg-parser/release-check.mjs +93 -0
  281. package/skills/core/reg-parser/scripts/compare_checklist.py +230 -0
  282. package/skills/core/reg-parser/scripts/dump_checklist.py +361 -0
  283. package/skills/core/reg-parser/scripts/preprocess-mandate.sh +187 -0
  284. package/skills/core/reg-parser/scripts/reg-parser.test.mjs +165 -0
  285. package/skills/core/reg-parser/scripts/test_quarantine.sh +105 -0
  286. package/skills/core/reg-parser/scripts/validate_checklist.py +563 -0
  287. package/skills/core/reg-parser/scripts/validate_mandate.py +273 -0
  288. package/skills/core/terminology/LICENSE +3 -0
  289. package/skills/core/terminology/SKILL.md +62 -0
  290. package/skills/core/terminology/references/format.md +50 -0
  291. package/skills/core/terminology/references/init.md +32 -0
  292. package/skills/core/terminology/release-check.mjs +93 -0
  293. package/skills/core/write-doc/LICENSE +3 -0
  294. package/skills/core/write-doc/SKILL.md +141 -0
  295. package/skills/core/write-doc/references/bilingual.md +60 -0
  296. package/skills/core/write-doc/release-check.mjs +93 -0
  297. package/skills/engineering/better-review/LICENSE +3 -0
  298. package/skills/engineering/better-review/SKILL.md +249 -0
  299. package/skills/engineering/better-review/agents/openai.yaml +9 -0
  300. package/skills/engineering/better-review/references/domain-hazards.md +53 -0
  301. package/skills/engineering/better-review/references/passes.md +147 -0
  302. package/skills/engineering/better-review/references/result.md +151 -0
  303. package/skills/engineering/better-review/release-check.mjs +93 -0
  304. package/skills/engineering/better-review/scripts/pr-context.mjs +212 -0
  305. package/skills/engineering/better-review/scripts/pr-context.test.mjs +168 -0
  306. package/skills/engineering/better-review/scripts/rereview-scope.mjs +503 -0
  307. package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +587 -0
  308. package/skills/engineering/better-review/scripts/scan-diff.mjs +365 -0
  309. package/skills/engineering/better-review/scripts/scan-diff.test.mjs +414 -0
  310. package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +96 -0
  311. package/skills/engineering/e2e-test/LICENSE +3 -0
  312. package/skills/engineering/e2e-test/SKILL.md +98 -0
  313. package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +24 -0
  314. package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
  315. package/skills/engineering/e2e-test/references/authoring/playwright-config.md +73 -0
  316. package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +144 -0
  317. package/skills/engineering/e2e-test/references/ci-integration.md +25 -0
  318. package/skills/engineering/e2e-test/references/ci-playwright-container.md +290 -0
  319. package/skills/engineering/e2e-test/references/presentation-sweep.md +104 -0
  320. package/skills/engineering/e2e-test/references/reviewing.md +41 -0
  321. package/skills/engineering/e2e-test/references/route-manifest.md +51 -0
  322. package/skills/engineering/e2e-test/release-check.mjs +93 -0
  323. package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
  324. package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
  325. package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +106 -0
  326. package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
  327. package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +117 -0
  328. package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
  329. package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +69 -0
  330. package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
  331. package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +98 -0
  332. package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +147 -0
  333. package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +71 -0
  334. package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
  335. package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +59 -0
  336. package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +82 -0
  337. package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +76 -0
  338. package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
  339. package/skills/engineering/engineering-runtime/LICENSE +3 -0
  340. package/skills/engineering/engineering-runtime/SKILL.md +11 -0
  341. package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
  342. package/skills/engineering/engineering-runtime/coherence/workflow.json +597 -0
  343. package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +68 -0
  344. package/skills/engineering/engineering-runtime/release-check.mjs +93 -0
  345. package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
  346. package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
  347. package/skills/engineering/engineering-runtime/scripts/invocation-policy.mjs +187 -0
  348. package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +95 -0
  349. package/skills/engineering/engineering-runtime/scripts/main-module.mjs +79 -0
  350. package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +288 -0
  351. package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
  352. package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
  353. package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
  354. package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
  355. package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +96 -0
  356. package/skills/engineering/engineering-runtime/scripts/review-completion.mjs +56 -0
  357. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +595 -0
  358. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +189 -0
  359. package/skills/engineering/engineering-runtime/scripts/yaml-mapping.mjs +18 -0
  360. package/skills/engineering/ensure-coverage/LICENSE +3 -0
  361. package/skills/engineering/ensure-coverage/SKILL.md +129 -0
  362. package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +92 -0
  363. package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +85 -0
  364. package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
  365. package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
  366. package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
  367. package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +19 -0
  368. package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
  369. package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +57 -0
  370. package/skills/engineering/ensure-coverage/references/depth/test-smells.md +39 -0
  371. package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +44 -0
  372. package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +110 -0
  373. package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +35 -0
  374. package/skills/engineering/ensure-coverage/references/presentation/axis.md +64 -0
  375. package/skills/engineering/ensure-coverage/references/process/audit-mode.md +27 -0
  376. package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
  377. package/skills/engineering/ensure-coverage/references/process/test-strategy.md +56 -0
  378. package/skills/engineering/ensure-coverage/release-check.mjs +93 -0
  379. package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +67 -0
  380. package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +140 -0
  381. package/skills/engineering/ensure-coverage/scripts/README.md +99 -0
  382. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +182 -0
  383. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +96 -0
  384. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +105 -0
  385. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +47 -0
  386. package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +137 -0
  387. package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +86 -0
  388. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +674 -0
  389. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +447 -0
  390. package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
  391. package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +552 -0
  392. package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +282 -0
  393. package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +289 -0
  394. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
  395. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +119 -0
  396. package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
  397. package/skills/engineering/golden-path/LICENSE +3 -0
  398. package/skills/engineering/golden-path/SKILL.md +230 -0
  399. package/skills/engineering/golden-path/agents/openai.yaml +3 -0
  400. package/skills/engineering/golden-path/assets/golden-paths.example.json +57 -0
  401. package/skills/engineering/golden-path/references/contract.md +239 -0
  402. package/skills/engineering/golden-path/references/migration.md +111 -0
  403. package/skills/engineering/golden-path/release-check.mjs +93 -0
  404. package/skills/engineering/golden-path/scripts/golden-path-core.mjs +1779 -0
  405. package/skills/engineering/golden-path/scripts/golden-path.mjs +6 -0
  406. package/skills/engineering/golden-path/scripts/golden-path.test.mjs +1457 -0
  407. package/skills/engineering/harness-runtime/LICENSE +3 -0
  408. package/skills/engineering/harness-runtime/SKILL.md +19 -0
  409. package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
  410. package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
  411. package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
  412. package/skills/engineering/harness-runtime/contracts.md +15 -0
  413. package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
  414. package/skills/engineering/harness-runtime/discover-models.test.mjs +363 -0
  415. package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
  416. package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
  417. package/skills/engineering/harness-runtime/harness-runtime.test.mjs +967 -0
  418. package/skills/engineering/harness-runtime/index.mjs +767 -0
  419. package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +829 -0
  420. package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
  421. package/skills/engineering/harness-runtime/model-catalog.test.mjs +366 -0
  422. package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
  423. package/skills/engineering/harness-runtime/references/model-catalog-seed.json +408 -0
  424. package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
  425. package/skills/engineering/harness-runtime/release-check.mjs +93 -0
  426. package/skills/engineering/resolve-issues/LICENSE +3 -0
  427. package/skills/engineering/resolve-issues/SKILL.md +250 -0
  428. package/skills/engineering/resolve-issues/agents/openai.yaml +9 -0
  429. package/skills/engineering/resolve-issues/plugin-hooks.json +17 -0
  430. package/skills/engineering/resolve-issues/references/canary.md +28 -0
  431. package/skills/engineering/resolve-issues/references/ci.md +105 -0
  432. package/skills/engineering/resolve-issues/references/done.md +25 -0
  433. package/skills/engineering/resolve-issues/references/dropbox.md +90 -0
  434. package/skills/engineering/resolve-issues/references/expire.md +318 -0
  435. package/skills/engineering/resolve-issues/references/exploration.md +202 -0
  436. package/skills/engineering/resolve-issues/references/implementation.md +303 -0
  437. package/skills/engineering/resolve-issues/references/preflight.md +445 -0
  438. package/skills/engineering/resolve-issues/references/review.md +228 -0
  439. package/skills/engineering/resolve-issues/references/shared-modules.md +44 -0
  440. package/skills/engineering/resolve-issues/references/task-plan.md +264 -0
  441. package/skills/engineering/resolve-issues/release-check.mjs +93 -0
  442. package/skills/engineering/resolve-issues/scripts/cleanup-runs.mjs +364 -0
  443. package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +280 -0
  444. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
  445. package/skills/engineering/resolve-issues/scripts/dropbox.mjs +473 -0
  446. package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +346 -0
  447. package/skills/engineering/resolve-issues/scripts/findings-reuse.mjs +98 -0
  448. package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +88 -0
  449. package/skills/engineering/resolve-issues/scripts/github-reviews.mjs +384 -0
  450. package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +264 -0
  451. package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +135 -0
  452. package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +79 -0
  453. package/skills/engineering/resolve-issues/scripts/issue-tree.mjs +162 -0
  454. package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +107 -0
  455. package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +14 -0
  456. package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
  457. package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +149 -0
  458. package/skills/engineering/resolve-issues/scripts/next-actions.mjs +856 -0
  459. package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +915 -0
  460. package/skills/engineering/resolve-issues/scripts/plan-sync.mjs +269 -0
  461. package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +156 -0
  462. package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +736 -0
  463. package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +448 -0
  464. package/skills/engineering/resolve-issues/scripts/ready-check.mjs +185 -0
  465. package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +217 -0
  466. package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +98 -0
  467. package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +91 -0
  468. package/skills/engineering/resolve-issues/scripts/review-verdict.mjs +509 -0
  469. package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +1060 -0
  470. package/skills/engineering/resolve-issues/scripts/size-check.mjs +492 -0
  471. package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +458 -0
  472. package/skills/engineering/resolve-release/LICENSE +3 -0
  473. package/skills/engineering/resolve-release/SKILL.md +215 -0
  474. package/skills/engineering/resolve-release/agents/openai.yaml +8 -0
  475. package/skills/engineering/resolve-release/flows/cloud-run-canary.md +13 -0
  476. package/skills/engineering/resolve-release/flows/cloud-run.md +7 -0
  477. package/skills/engineering/resolve-release/flows/root-tag-npm.md +13 -0
  478. package/skills/engineering/resolve-release/flows/workspace-npm.md +22 -0
  479. package/skills/engineering/resolve-release/references/canary.md +51 -0
  480. package/skills/engineering/resolve-release/references/candidate.md +41 -0
  481. package/skills/engineering/resolve-release/references/cut.md +131 -0
  482. package/skills/engineering/resolve-release/references/dark-launch.md +66 -0
  483. package/skills/engineering/resolve-release/references/flows.md +150 -0
  484. package/skills/engineering/resolve-release/references/preflight.md +154 -0
  485. package/skills/engineering/resolve-release/references/prepared-metadata.md +260 -0
  486. package/skills/engineering/resolve-release/references/promotion.md +82 -0
  487. package/skills/engineering/resolve-release/references/publish.md +232 -0
  488. package/skills/engineering/resolve-release/references/workspace-npm.md +103 -0
  489. package/skills/engineering/resolve-release/release-check.mjs +93 -0
  490. package/skills/engineering/resolve-release/scripts/apply.mjs +212 -0
  491. package/skills/engineering/resolve-release/scripts/apply.test.mjs +99 -0
  492. package/skills/engineering/resolve-release/scripts/dark-flags.mjs +134 -0
  493. package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +100 -0
  494. package/skills/engineering/resolve-release/scripts/flow.mjs +500 -0
  495. package/skills/engineering/resolve-release/scripts/flow.test.mjs +340 -0
  496. package/skills/engineering/resolve-release/scripts/green-head.mjs +231 -0
  497. package/skills/engineering/resolve-release/scripts/green-head.test.mjs +101 -0
  498. package/skills/engineering/resolve-release/scripts/npm-version-absence.mjs +138 -0
  499. package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +426 -0
  500. package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +659 -0
  501. package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +317 -0
  502. package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +291 -0
  503. package/skills/engineering/resolve-release/scripts/plan.mjs +806 -0
  504. package/skills/engineering/resolve-release/scripts/plan.test.mjs +428 -0
  505. package/skills/engineering/rush-issues/LICENSE +3 -0
  506. package/skills/engineering/rush-issues/SKILL.md +58 -0
  507. package/skills/engineering/rush-issues/agents/openai.yaml +8 -0
  508. package/skills/engineering/rush-issues/rehydrate.mjs +10 -0
  509. package/skills/engineering/rush-issues/release-check.mjs +93 -0
  510. package/skills/engineering/rush-release/LICENSE +3 -0
  511. package/skills/engineering/rush-release/SKILL.md +35 -0
  512. package/skills/engineering/rush-release/agents/openai.yaml +8 -0
  513. package/skills/engineering/rush-release/release-check.mjs +93 -0
  514. package/skills/engineering/test-quality/LICENSE +3 -0
  515. package/skills/engineering/test-quality/SKILL.md +79 -0
  516. package/skills/engineering/test-quality/references/audit.md +80 -0
  517. package/skills/engineering/test-quality/references/lint.md +24 -0
  518. package/skills/engineering/test-quality/references/principles.md +143 -0
  519. package/skills/engineering/test-quality/references/testing-conventions.md +9 -0
  520. package/skills/engineering/test-quality/release-check.mjs +93 -0
  521. package/skills/engineering/test-quality/scripts/lint-tests.mjs +574 -0
  522. package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +591 -0
  523. package/skills/engineering/test-quality/scripts/mock-policy-config.mjs +176 -0
  524. package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +119 -0
  525. package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +117 -0
  526. package/skills/engineering/test-quality/scripts/persist-conventions.mjs +17 -0
  527. package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +104 -0
  528. package/skills/risk-compliance/compliance-audit/LICENSE +3 -0
  529. package/skills/risk-compliance/compliance-audit/SKILL.md +439 -0
  530. package/skills/risk-compliance/compliance-audit/references/findings-schema.md +293 -0
  531. package/skills/risk-compliance/compliance-audit/references/normalization-rules.md +45 -0
  532. package/skills/risk-compliance/compliance-audit/references/vars-checklist.md +1068 -0
  533. package/skills/risk-compliance/compliance-audit/references/vars-findings-schema.md +398 -0
  534. package/skills/risk-compliance/compliance-audit/release-check.mjs +93 -0
  535. package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +14 -0
  536. package/skills/risk-compliance/compliance-audit/scripts/export_tracker_xlsx.py +221 -0
  537. package/skills/risk-compliance/compliance-audit/scripts/reconcile_tracker.py +458 -0
  538. package/skills/risk-compliance/compliance-audit/scripts/test_export_tracker_xlsx.py +97 -0
  539. package/skills/risk-compliance/compliance-audit/scripts/test_validate_tracker.py +157 -0
  540. package/skills/risk-compliance/compliance-audit/scripts/validate_tracker.py +829 -0
  541. package/skills/risk-compliance/contract-compare/LICENSE +3 -0
  542. package/skills/risk-compliance/contract-compare/SKILL.md +199 -0
  543. package/skills/risk-compliance/contract-compare/references/comparison-dimensions.md +209 -0
  544. package/skills/risk-compliance/contract-compare/release-check.mjs +93 -0
  545. package/skills/risk-compliance/contract-review/LICENSE +3 -0
  546. package/skills/risk-compliance/contract-review/SKILL.md +240 -0
  547. package/skills/risk-compliance/contract-review/references/review-criteria.md +223 -0
  548. package/skills/risk-compliance/contract-review/release-check.mjs +93 -0
  549. package/skills/risk-compliance/doc-authenticity/LICENSE +3 -0
  550. package/skills/risk-compliance/doc-authenticity/SKILL.md +160 -0
  551. package/skills/risk-compliance/doc-authenticity/agents/openai.yaml +8 -0
  552. package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +12 -0
  553. package/skills/risk-compliance/doc-authenticity/references/agreement.md +66 -0
  554. package/skills/risk-compliance/doc-authenticity/references/bank-statement.md +150 -0
  555. package/skills/risk-compliance/doc-authenticity/references/forensic-signals.md +157 -0
  556. package/skills/risk-compliance/doc-authenticity/references/image-signals.md +114 -0
  557. package/skills/risk-compliance/doc-authenticity/references/invoice.md +66 -0
  558. package/skills/risk-compliance/doc-authenticity/references/issuer-profiles.md +86 -0
  559. package/skills/risk-compliance/doc-authenticity/references/verdict-and-report.md +95 -0
  560. package/skills/risk-compliance/doc-authenticity/references/verdict.schema.json +81 -0
  561. package/skills/risk-compliance/doc-authenticity/release-check.mjs +93 -0
  562. package/skills/risk-compliance/doc-authenticity/scripts/image_forensics.py +294 -0
  563. package/skills/risk-compliance/doc-authenticity/scripts/pdf_forensics.py +401 -0
  564. package/skills/risk-compliance/doc-authenticity/scripts/validate_codes.py +224 -0
  565. package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.py +308 -0
  566. package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +212 -0
  567. package/skills/risk-compliance/fraud-analysis/LICENSE +3 -0
  568. package/skills/risk-compliance/fraud-analysis/SKILL.md +114 -0
  569. package/skills/risk-compliance/fraud-analysis/references/archetypes/authorized-payment-scam.md +41 -0
  570. package/skills/risk-compliance/fraud-analysis/references/archetypes/first-party-fraud.md +44 -0
  571. package/skills/risk-compliance/fraud-analysis/references/archetypes/third-party-fraud.md +27 -0
  572. package/skills/risk-compliance/fraud-analysis/references/contexts/bank-transfer.md +24 -0
  573. package/skills/risk-compliance/fraud-analysis/references/contexts/card-payment.md +30 -0
  574. package/skills/risk-compliance/fraud-analysis/references/contexts/payment-collection.md +20 -0
  575. package/skills/risk-compliance/fraud-analysis/references/contexts/payout.md +20 -0
  576. package/skills/risk-compliance/fraud-analysis/references/feature-engineering.md +158 -0
  577. package/skills/risk-compliance/fraud-analysis/references/mechanisms/account-takeover.md +36 -0
  578. package/skills/risk-compliance/fraud-analysis/references/report-rationale.md +45 -0
  579. package/skills/risk-compliance/fraud-analysis/references/report-template.md +190 -0
  580. package/skills/risk-compliance/fraud-analysis/references/review-checklist.md +175 -0
  581. package/skills/risk-compliance/fraud-analysis/references/taxonomy.md +79 -0
  582. package/skills/risk-compliance/fraud-analysis/references/terminology.md +108 -0
  583. package/skills/risk-compliance/fraud-analysis/references/workflow.md +175 -0
  584. package/skills/risk-compliance/fraud-analysis/release-check.mjs +93 -0
  585. package/skills/risk-compliance/payment-analysis/LICENSE +3 -0
  586. package/skills/risk-compliance/payment-analysis/SKILL.md +180 -0
  587. package/skills/risk-compliance/payment-analysis/references/auth-rate-actions.md +30 -0
  588. package/skills/risk-compliance/payment-analysis/references/chargebacks.md +208 -0
  589. package/skills/risk-compliance/payment-analysis/references/chinese-polish.md +51 -0
  590. package/skills/risk-compliance/payment-analysis/references/event-layers.md +127 -0
  591. package/skills/risk-compliance/payment-analysis/references/fx.md +59 -0
  592. package/skills/risk-compliance/payment-analysis/references/journey.md +79 -0
  593. package/skills/risk-compliance/payment-analysis/references/metrics.md +67 -0
  594. package/skills/risk-compliance/payment-analysis/references/report-template.md +119 -0
  595. package/skills/risk-compliance/payment-analysis/references/terminology.md +95 -0
  596. package/skills/risk-compliance/payment-analysis/references/visualization.md +56 -0
  597. package/skills/risk-compliance/payment-analysis/release-check.mjs +93 -0
  598. package/skills/risk-compliance/report-writing/LICENSE +3 -0
  599. package/skills/risk-compliance/report-writing/SKILL.md +220 -0
  600. package/skills/risk-compliance/report-writing/references/audit-report-template.md +121 -0
  601. package/skills/risk-compliance/report-writing/references/external-assessment-template.md +167 -0
  602. package/skills/risk-compliance/report-writing/references/management-report-template.md +145 -0
  603. package/skills/risk-compliance/report-writing/references/vars-report-template.md +224 -0
  604. package/skills/risk-compliance/report-writing/release-check.mjs +93 -0
  605. package/skills/risk-compliance/similar-domain/LICENSE +3 -0
  606. package/skills/risk-compliance/similar-domain/SKILL.md +203 -0
  607. package/skills/risk-compliance/similar-domain/references/entities.md +67 -0
  608. package/skills/risk-compliance/similar-domain/references/fingerprints.md +134 -0
  609. package/skills/risk-compliance/similar-domain/references/pivots.md +82 -0
  610. package/skills/risk-compliance/similar-domain/references/report-template.md +93 -0
  611. package/skills/risk-compliance/similar-domain/references/scoring.md +130 -0
  612. package/skills/risk-compliance/similar-domain/references/terminology.md +41 -0
  613. package/skills/risk-compliance/similar-domain/references/workflow.md +85 -0
  614. package/skills/risk-compliance/similar-domain/release-check.mjs +93 -0
  615. package/skills/risk-compliance/tld-detection/LICENSE +3 -0
  616. package/skills/risk-compliance/tld-detection/SKILL.md +245 -0
  617. package/skills/risk-compliance/tld-detection/agents/openai.yaml +9 -0
  618. package/skills/risk-compliance/tld-detection/references/chinese-polish.md +55 -0
  619. package/skills/risk-compliance/tld-detection/references/clean.md +51 -0
  620. package/skills/risk-compliance/tld-detection/references/clustering.md +235 -0
  621. package/skills/risk-compliance/tld-detection/references/decision.md +189 -0
  622. package/skills/risk-compliance/tld-detection/references/fx-conversion.md +232 -0
  623. package/skills/risk-compliance/tld-detection/references/report-template.md +272 -0
  624. package/skills/risk-compliance/tld-detection/references/streams.md +141 -0
  625. package/skills/risk-compliance/tld-detection/references/terminology.md +95 -0
  626. package/skills/risk-compliance/tld-detection/references/workflow.md +95 -0
  627. package/skills/risk-compliance/tld-detection/release-check.mjs +93 -0
  628. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.py +390 -0
  629. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +14 -0
  630. package/skills/risk-compliance/tld-detection/scripts/detect_streams.py +650 -0
  631. package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +14 -0
  632. package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +134 -0
  633. package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +520 -0
@@ -0,0 +1,1435 @@
1
+ #!/usr/bin/env python3
2
+ """Data catalog engine: read column metadata from table scripts, generate the catalog, audit scripts.
3
+
4
+ Commands:
5
+ init set a repo up: config, this engine, the Field/Table module, CI workflow
6
+ generate write docs/catalog/*.md from the scripts; --check fails when they are stale
7
+ audit report scripts that write tables without complete column definitions (never edits)
8
+ drift local check: code changed since a git base while its definitions did not
9
+ version print the engine identity
10
+
11
+ Metadata carriers:
12
+ Python module-level CATALOG_TABLE = Table(...) and CATALOG_FIELDS = (Field(...), ...),
13
+ read with ast (the script is never imported, so Spark is not needed)
14
+ SQL a comment block of `-- @table <name>` and `-- @field <name>` entries
15
+
16
+ Standard library only. Copied into a repo by `init`; refresh it the same way.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import ast
22
+ import hashlib
23
+ import io
24
+ import json
25
+ import os
26
+ import re
27
+ import subprocess
28
+ import sys
29
+ import tokenize
30
+ from dataclasses import dataclass, field
31
+ from pathlib import Path
32
+
33
+ ENGINE_VERSION = "1.0.0"
34
+ CONFIG_FILE = "data-catalog.json"
35
+ ENGINE_PATH = "scripts/data-catalog/data_catalog.py"
36
+ WORKFLOW_PATH = ".github/workflows/data-catalog.yml"
37
+ GENERATED_MARK = "<!-- generated by data_catalog.py: edit the table script, then run generate -->"
38
+ WORKFLOW_MARK = "# generated by data_catalog.py init: edit data-catalog.json and rerun init"
39
+ TABLE_VAR = "CATALOG_TABLE"
40
+ FIELDS_VAR = "CATALOG_FIELDS"
41
+ DEFAULT_CONFIG = {
42
+ "version": 1,
43
+ "scripts": [],
44
+ "exclude": [],
45
+ "output": "docs/catalog",
46
+ "languages": ["en"],
47
+ "contracts": ["PUBLISHED_COLUMNS", "OUTPUT_COLUMNS"],
48
+ "fieldsModule": None,
49
+ "tableNames": [],
50
+ "schemaNames": [],
51
+ }
52
+ FIELD_EXTRAS = ("source", "window", "unit")
53
+ LANGUAGE_LABELS = {"en": "English", "zh": "中文", "zh_hant": "繁體中文", "ja": "日本語", "ko": "한국어"}
54
+ LANGUAGE_RE = re.compile(r"^[a-z]{2}(_[a-z]+)?$")
55
+ SKIP_DIRS = {"node_modules", "__pycache__", "venv"}
56
+ # Assignments (module or class body) whose string value names the table a script writes,
57
+ # and the schema/database that may sit beside it (`dst_db = "dwd"`, `dst_tbl = "orders"`).
58
+ TABLE_NAME_RE = re.compile(r"^(?:\w*_)?(?:target|output|sink|dest|destination|dst)_(?:table|tbl)(?:_name)?$|^table_name$", re.I)
59
+ SCHEMA_NAME_RE = re.compile(r"^(?:target|output|sink|dest|destination|dst)_(?:db|database|schema)(?:_name)?$", re.I)
60
+ SQL_KEYWORDS = {
61
+ "end", "from", "where", "and", "or", "not", "null", "then", "else", "when", "case", "as",
62
+ "on", "join", "by", "asc", "desc", "over", "distinct", "is", "in", "like", "between", "with", "select",
63
+ "union", "all", "partition", "rows", "range", "interval", "true", "false",
64
+ }
65
+
66
+
67
+ class CatalogError(Exception):
68
+ """A metadata or config problem, reported as `path:line: message`."""
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Config and file discovery
73
+ # ---------------------------------------------------------------------------
74
+
75
+ def read_config_file(root: Path) -> dict:
76
+ try:
77
+ data = json.loads((root / CONFIG_FILE).read_text(encoding="utf-8"))
78
+ except json.JSONDecodeError as exc:
79
+ raise CatalogError(f"{CONFIG_FILE}: invalid JSON: {exc}") from exc
80
+ if not isinstance(data, dict):
81
+ raise CatalogError(f"{CONFIG_FILE}: must be a JSON object")
82
+ return data
83
+
84
+
85
+ def load_config(root: Path) -> dict:
86
+ if not (root / CONFIG_FILE).exists():
87
+ raise CatalogError(f"{CONFIG_FILE} not found in {root}; run `init` first")
88
+ config = {**DEFAULT_CONFIG, **read_config_file(root)}
89
+ validate_config(config)
90
+ return config
91
+
92
+
93
+ def validate_config(config: dict) -> None:
94
+ config["output"] = str(config.get("output") or DEFAULT_CONFIG["output"]).strip().strip("/") or DEFAULT_CONFIG["output"]
95
+ if not config["scripts"]:
96
+ raise CatalogError(f"{CONFIG_FILE}: `scripts` must list at least one glob")
97
+ langs = config["languages"]
98
+ if not langs or not all(isinstance(lang, str) and LANGUAGE_RE.match(lang) for lang in langs):
99
+ raise CatalogError(f"{CONFIG_FILE}: `languages` must be codes like \"en\" or \"zh\", got {langs!r}")
100
+ clash = [lang for lang in langs if lang in FIELD_EXTRAS or lang in ("name", "grain", "notes")]
101
+ if clash:
102
+ raise CatalogError(f"{CONFIG_FILE}: language codes clash with field arguments: {clash}")
103
+
104
+
105
+ def glob_regex(pattern: str) -> re.Pattern:
106
+ out, i = [], 0
107
+ while i < len(pattern):
108
+ if pattern.startswith("**/", i):
109
+ out.append("(?:.*/)?"); i += 3
110
+ elif pattern.startswith("**", i):
111
+ out.append(".*"); i += 2
112
+ elif pattern[i] == "*":
113
+ out.append("[^/]*"); i += 1
114
+ elif pattern[i] == "?":
115
+ out.append("[^/]"); i += 1
116
+ else:
117
+ out.append(re.escape(pattern[i])); i += 1
118
+ return re.compile("".join(out) + r"\Z")
119
+
120
+
121
+ def find_scripts(root: Path, patterns: list[str], exclude: list[str]) -> list[Path]:
122
+ """One pruned walk: hidden directories and node_modules are never entered."""
123
+ wanted = [glob_regex(p) for p in patterns]
124
+ skipped = [glob_regex(p) for p in exclude]
125
+ found = []
126
+ for dirpath, dirnames, filenames in os.walk(root):
127
+ dirnames[:] = sorted(d for d in dirnames if not d.startswith(".") and d not in SKIP_DIRS)
128
+ for name in filenames:
129
+ if not name.endswith((".py", ".sql")):
130
+ continue
131
+ path = Path(dirpath) / name
132
+ relpath = rel(root, path)
133
+ if any(r.match(relpath) for r in wanted) and not any(r.match(relpath) for r in skipped):
134
+ found.append(path)
135
+ return sorted(found)
136
+
137
+
138
+ def rel(root: Path, path: Path) -> str:
139
+ return path.relative_to(root).as_posix()
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Metadata model
144
+ # ---------------------------------------------------------------------------
145
+
146
+ @dataclass
147
+ class FieldDoc:
148
+ name: str
149
+ text: dict
150
+ source: str
151
+ window: str | None
152
+ unit: str | None
153
+ line: int
154
+
155
+
156
+ @dataclass
157
+ class TableDoc:
158
+ name: str
159
+ script: str
160
+ grain: dict # language -> what one row is
161
+ text: dict
162
+ notes: dict # language -> [shared rules]
163
+ fields: list
164
+ line: int
165
+ contracts: dict = field(default_factory=dict) # name -> list[str] | "derived" | "unchecked"
166
+
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # Python carrier (ast, never imported)
170
+ # ---------------------------------------------------------------------------
171
+
172
+ def module_assignments(tree: ast.Module) -> dict:
173
+ out = {}
174
+ for node in tree.body:
175
+ if isinstance(node, ast.Assign):
176
+ for target in node.targets:
177
+ if isinstance(target, ast.Name):
178
+ out[target.id] = node.value
179
+ elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.value is not None:
180
+ out[node.target.id] = node.value
181
+ return out
182
+
183
+
184
+ def call_name(node: ast.AST) -> str | None:
185
+ if not isinstance(node, ast.Call):
186
+ return None
187
+ if isinstance(node.func, ast.Name):
188
+ return node.func.id
189
+ if isinstance(node.func, ast.Attribute):
190
+ return node.func.attr
191
+ return None
192
+
193
+
194
+ def eval_text(node: ast.AST, names: dict, where: str, seen: frozenset = frozenset()):
195
+ """Evaluate a string, tuple of strings, or None built from literals and module constants."""
196
+ if isinstance(node, ast.Constant) and (isinstance(node.value, str) or node.value is None):
197
+ return node.value
198
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
199
+ left = eval_text(node.left, names, where, seen)
200
+ right = eval_text(node.right, names, where, seen)
201
+ if isinstance(left, str) and isinstance(right, str):
202
+ return left + right
203
+ if isinstance(left, tuple) and isinstance(right, tuple):
204
+ return left + right
205
+ if isinstance(node, (ast.Tuple, ast.List)):
206
+ return tuple(eval_text(elt, names, where, seen) for elt in node.elts)
207
+ if isinstance(node, ast.Name) and node.id in names and node.id not in seen:
208
+ return eval_text(names[node.id], names, where, seen | {node.id})
209
+ raise CatalogError(f"{where}:{getattr(node, 'lineno', '?')}: metadata must be a string literal "
210
+ "(or module-level string constant), not an expression")
211
+
212
+
213
+ def resolve_sequence(node: ast.AST, names: dict, where: str, seen: frozenset = frozenset()) -> list:
214
+ """Flatten a tuple/list (with *NAME, A + B, NAME) into its element nodes."""
215
+ if isinstance(node, (ast.Tuple, ast.List)):
216
+ out = []
217
+ for elt in node.elts:
218
+ if isinstance(elt, ast.Starred):
219
+ out.extend(resolve_sequence(elt.value, names, where, seen))
220
+ else:
221
+ out.append(elt)
222
+ return out
223
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
224
+ return resolve_sequence(node.left, names, where, seen) + resolve_sequence(node.right, names, where, seen)
225
+ if isinstance(node, ast.Name) and node.id in names and node.id not in seen:
226
+ return resolve_sequence(names[node.id], names, where, seen | {node.id})
227
+ raise CatalogError(f"{where}:{getattr(node, 'lineno', '?')}: {FIELDS_VAR} must be a tuple of Field(...) "
228
+ "built in this module (literals, *NAME or A + B); imported lists cannot be read")
229
+
230
+
231
+ def call_kwargs(call: ast.Call, names: dict, where: str, kind: str) -> dict:
232
+ if len(call.args) > 1 or any(isinstance(a, ast.Starred) for a in call.args):
233
+ raise CatalogError(f"{where}:{call.lineno}: pass only the name positionally to {kind}(...); "
234
+ "everything else by keyword")
235
+ values = {}
236
+ if call.args:
237
+ values["name"] = eval_text(call.args[0], names, where)
238
+ for kw in call.keywords:
239
+ if kw.arg is None:
240
+ raise CatalogError(f"{where}:{call.lineno}: **kwargs is not readable in {kind}(...)")
241
+ values[kw.arg] = eval_text(kw.value, names, where)
242
+ return values
243
+
244
+
245
+ def require_text(values: dict, key: str, where: str, line: int, label: str) -> str:
246
+ value = values.get(key)
247
+ if not isinstance(value, str) or not value.strip():
248
+ raise CatalogError(f"{where}:{line}: {label} is missing `{key}`")
249
+ return value.strip()
250
+
251
+
252
+ def build_field(values: dict, languages: list, where: str, line: int) -> FieldDoc:
253
+ name = require_text(values, "name", where, line, "Field")
254
+ known = {"name", *FIELD_EXTRAS, *languages}
255
+ unknown = sorted(set(values) - known)
256
+ if unknown:
257
+ raise CatalogError(f"{where}:{line}: Field {name!r} has unknown argument(s) {unknown}; "
258
+ f"languages configured: {languages}")
259
+ text = {lang: require_text(values, lang, where, line, f"Field {name!r}") for lang in languages}
260
+ source = require_text(values, "source", where, line, f"Field {name!r}")
261
+ return FieldDoc(name, text, source, (values.get("window") or None), (values.get("unit") or None), line)
262
+
263
+
264
+ def build_table(values: dict, fields: list, languages: list, where: str, line: int, script: str) -> TableDoc:
265
+ name = require_text(values, "name", where, line, "Table")
266
+ if "grain" in values or "notes" in values:
267
+ keys = ", ".join(f"grain_{lang}=..., notes_{lang}=(...)" for lang in languages)
268
+ raise CatalogError(f"{where}:{line}: Table {name!r}: grain and notes are written per language: {keys}")
269
+ known = {"name", *languages, *(f"grain_{lang}" for lang in languages), *(f"notes_{lang}" for lang in languages)}
270
+ unknown = sorted(set(values) - known)
271
+ if unknown:
272
+ raise CatalogError(f"{where}:{line}: Table {name!r} has unknown argument(s) {unknown}")
273
+ text = {lang: require_text(values, lang, where, line, f"Table {name!r}") for lang in languages}
274
+ grain = {lang: require_text(values, f"grain_{lang}", where, line, f"Table {name!r}") for lang in languages}
275
+ notes = {}
276
+ for lang in languages:
277
+ raw = values.get(f"notes_{lang}") or ()
278
+ raw = (raw,) if isinstance(raw, str) else raw
279
+ notes[lang] = [n.strip() for n in raw if n and n.strip()]
280
+ counts = {lang: len(n) for lang, n in notes.items()}
281
+ if len(set(counts.values())) > 1:
282
+ raise CatalogError(f"{where}:{line}: Table {name!r}: every language needs the same notes, got {counts}")
283
+ return TableDoc(name, script, grain, text, notes, fields, line)
284
+
285
+
286
+ def struct_fields(node: ast.AST, names: dict):
287
+ """Column names of a literal StructType([StructField("name", ...), ...]), or None."""
288
+ if call_name(node) != "StructType" or not node.args:
289
+ return None
290
+ try:
291
+ items = resolve_sequence(node.args[0], names, "", frozenset())
292
+ except CatalogError:
293
+ return None
294
+ out = []
295
+ for item in items:
296
+ if call_name(item) != "StructField" or not item.args:
297
+ return None
298
+ try:
299
+ value = eval_text(item.args[0], names, "")
300
+ except CatalogError:
301
+ return None
302
+ if not isinstance(value, str):
303
+ return None
304
+ out.append(value)
305
+ return out
306
+
307
+
308
+ def contract_state(node: ast.AST, names: dict):
309
+ if any(isinstance(n, ast.Name) and n.id == FIELDS_VAR for n in ast.walk(node)):
310
+ return "derived"
311
+ columns = struct_fields(node, names)
312
+ if columns is not None:
313
+ return columns
314
+ try:
315
+ items = resolve_sequence(node, names, "", frozenset())
316
+ values = [eval_text(item, names, "") for item in items]
317
+ except CatalogError:
318
+ return "unchecked"
319
+ if all(isinstance(v, str) for v in values):
320
+ return values
321
+ return "unchecked"
322
+
323
+
324
+ def parse_python(text: str, where: str, languages: list, contracts: list) -> TableDoc | None:
325
+ try:
326
+ tree = ast.parse(text)
327
+ except SyntaxError as exc:
328
+ raise CatalogError(f"{where}:{exc.lineno}: cannot parse: {exc.msg}") from exc
329
+ names = module_assignments(tree)
330
+ table_node, fields_node = names.get(TABLE_VAR), names.get(FIELDS_VAR)
331
+ if table_node is None and fields_node is None:
332
+ return None
333
+ if table_node is None or fields_node is None:
334
+ missing = TABLE_VAR if table_node is None else FIELDS_VAR
335
+ raise CatalogError(f"{where}:1: has one of {TABLE_VAR}/{FIELDS_VAR} but not {missing}")
336
+ if call_name(table_node) != "Table":
337
+ raise CatalogError(f"{where}:{table_node.lineno}: {TABLE_VAR} must be Table(...)")
338
+ fields = []
339
+ for item in resolve_sequence(fields_node, names, where):
340
+ if call_name(item) != "Field":
341
+ raise CatalogError(f"{where}:{getattr(item, 'lineno', '?')}: every {FIELDS_VAR} entry must be Field(...)")
342
+ fields.append(build_field(call_kwargs(item, names, where, "Field"), languages, where, item.lineno))
343
+ table = build_table(call_kwargs(table_node, names, where, "Table"), fields, languages, where, table_node.lineno, where)
344
+ for contract in contracts:
345
+ if contract in names:
346
+ table.contracts[contract] = contract_state(names[contract], names)
347
+ return table
348
+
349
+
350
+ # ---------------------------------------------------------------------------
351
+ # SQL carrier: `-- @table` / `-- @field` comment blocks
352
+ # ---------------------------------------------------------------------------
353
+
354
+ SQL_DIRECTIVE_RE = re.compile(r"^@(table|field)\s+(\S+)\s*$")
355
+ SQL_DIRECTIVE_START_RE = re.compile(r"^@(table|field)\b")
356
+ SQL_KEY_RE = re.compile(r"^ ([a-z][a-z_]*):\s?(.*)$")
357
+ SQL_CONTINUE_RE = re.compile(r"^ {4,}(\S.*)$")
358
+
359
+
360
+ def parse_sql(text: str, where: str, languages: list) -> TableDoc | None:
361
+ entries = [] # [kind, name, values, line, last_key]
362
+ current = None
363
+ for lineno, raw in enumerate(text.splitlines(), 1):
364
+ stripped = raw.lstrip()
365
+ if not stripped.startswith("--"):
366
+ if stripped:
367
+ break # the block sits above the code; a directive after the first statement is ignored
368
+ current = None
369
+ continue
370
+ body = stripped[2:]
371
+ body = body[1:] if body.startswith(" ") else body
372
+ directive = SQL_DIRECTIVE_RE.match(body)
373
+ if not directive and SQL_DIRECTIVE_START_RE.match(body):
374
+ raise CatalogError(f"{where}:{lineno}: write `-- @table <name>` or `-- @field <name>` with nothing after the name")
375
+ if directive:
376
+ current = [directive.group(1), directive.group(2), {}, lineno, None]
377
+ entries.append(current)
378
+ continue
379
+ if current is None:
380
+ continue
381
+ key = SQL_KEY_RE.match(body)
382
+ cont = SQL_CONTINUE_RE.match(body)
383
+ if key:
384
+ current[4] = key.group(1)
385
+ current[2][key.group(1)] = key.group(2).strip()
386
+ elif cont and current[4]:
387
+ prev = current[2][current[4]]
388
+ current[2][current[4]] = f"{prev} {cont.group(1).strip()}".strip()
389
+ else:
390
+ current = None
391
+ if not entries:
392
+ return None
393
+ tables = [e for e in entries if e[0] == "table"]
394
+ if len(tables) != 1:
395
+ raise CatalogError(f"{where}:{entries[0][3]}: a SQL file needs exactly one `-- @table`, found {len(tables)}")
396
+ fields = []
397
+ for kind, name, values, line, _ in entries:
398
+ if kind == "field":
399
+ if any(k.startswith(("notes", "grain")) for k in values):
400
+ raise CatalogError(f"{where}:{line}: `grain_*` and `notes_*` belong on @table, not @field")
401
+ fields.append(build_field({"name": name, **values}, languages, where, line))
402
+ _, name, values, line, _ = tables[0]
403
+ values = {"name": name, **{k: tuple(n.strip() for n in re.split(r"[;;]", v)) if k.startswith("notes_") else v
404
+ for k, v in values.items()}}
405
+ return build_table(values, fields, languages, where, line, where)
406
+
407
+
408
+ def parse_script(path: Path, root: Path, config: dict) -> TableDoc | None:
409
+ where = rel(root, path)
410
+ text = path.read_text(encoding="utf-8")
411
+ if path.suffix == ".py":
412
+ return parse_python(text, where, config["languages"], config["contracts"])
413
+ return parse_sql(text, where, config["languages"])
414
+
415
+
416
+ # ---------------------------------------------------------------------------
417
+ # Validation and rendering
418
+ # ---------------------------------------------------------------------------
419
+
420
+ def collect(root: Path, config: dict):
421
+ """Parse every configured script; returns (tables, errors, warnings)."""
422
+ tables, errors, warnings = [], [], []
423
+ for path in find_scripts(root, config["scripts"], config["exclude"]):
424
+ try:
425
+ table = parse_script(path, root, config)
426
+ except CatalogError as exc:
427
+ errors.append(str(exc))
428
+ continue
429
+ except (UnicodeDecodeError, OSError) as exc:
430
+ errors.append(f"{rel(root, path)}:1: cannot read: {exc}")
431
+ continue
432
+ if table is None:
433
+ continue
434
+ seen = {}
435
+ for f in table.fields:
436
+ if f.name in seen:
437
+ errors.append(f"{table.script}:{f.line}: field {f.name!r} is defined twice (first at line {seen[f.name]})")
438
+ seen.setdefault(f.name, f.line)
439
+ for contract, state in table.contracts.items():
440
+ if isinstance(state, list):
441
+ missing = [c for c in state if c not in seen]
442
+ extra = [n for n in seen if n not in state]
443
+ if missing:
444
+ errors.append(f"{table.script}:{table.line}: {contract} has column(s) with no Field: {', '.join(missing)}")
445
+ if extra:
446
+ warnings.append(f"{table.script}: Field(s) not in {contract}: {', '.join(extra)} "
447
+ "(fine for partition or provenance columns)")
448
+ warnings.append(f"{table.script}: {contract} is a literal list; derive it from {FIELDS_VAR} "
449
+ "so code and docs cannot drift")
450
+ if not table.contracts:
451
+ check_select_contract(path, table, seen, errors, warnings)
452
+ tables.append(table)
453
+ by_name = {}
454
+ for table in tables:
455
+ if table.name in by_name:
456
+ errors.append(f"{table.script}:{table.line}: table {table.name!r} is also documented in {by_name[table.name]}")
457
+ by_name.setdefault(table.name, table.script)
458
+ errors += page_collisions([t for t in tables if by_name[t.name] == t.script])
459
+ return sorted(tables, key=lambda t: t.name), errors, warnings
460
+
461
+
462
+ def select_of(path: Path, table_name: str):
463
+ """(columns, complete) of the SELECT that writes table_name, or None when no such SELECT is found."""
464
+ text = path.read_text(encoding="utf-8")
465
+ chunks = [text] if path.suffix == ".sql" else python_strings(ast.parse(text))
466
+ writes = [w for chunk in chunks for w in sql_writes(chunk)]
467
+ exact = [w for w in writes if same_table(w[0], table_name)]
468
+ chosen = exact or (writes if len(writes) == 1 and is_dynamic(writes[0][0]) else [])
469
+ if not chosen or not chosen[0][1]:
470
+ return None
471
+ return chosen[0][1], chosen[0][2]
472
+
473
+
474
+ def check_select_contract(path: Path, table: TableDoc, documented: dict, errors: list, warnings: list) -> None:
475
+ """With no column contract, the SELECT that writes the table is the contract for its comments."""
476
+ found = select_of(path, table.name)
477
+ if found is None:
478
+ warnings.append(f"{table.script}: no column contract and no readable SELECT writing {table.name}; "
479
+ "columns are not checked")
480
+ return
481
+ columns, complete = found
482
+ missing = [c for c in columns if c not in documented]
483
+ if missing:
484
+ errors.append(f"{table.script}:{table.line}: the SELECT writing {table.name} outputs column(s) with no "
485
+ f"definition: {', '.join(missing)}")
486
+ extra = [n for n in documented if n not in columns]
487
+ if extra and complete:
488
+ errors.append(f"{table.script}:{table.line}: definition(s) for column(s) the SELECT does not output: "
489
+ f"{', '.join(extra)}")
490
+ elif not complete:
491
+ warnings.append(f"{table.script}: the SELECT writing {table.name} has `*` or unnamed items; "
492
+ "only its named columns are checked")
493
+
494
+
495
+ def cell(text: str | None) -> str:
496
+ if not text:
497
+ return ""
498
+ return text.replace("\\", "\\\\").replace("|", "\\|").replace("\r\n", "\n").replace("\n", "<br>")
499
+
500
+
501
+ SECTION_LABELS = {"en": ("Grain:", "Notes:"), "zh": ("粒度:", "说明:"), "zh_hant": ("粒度:", "說明:")}
502
+
503
+
504
+ def label(lang: str) -> str:
505
+ return LANGUAGE_LABELS.get(lang, lang)
506
+
507
+
508
+ def page_name(table: TableDoc) -> str:
509
+ return re.sub(r"[^A-Za-z0-9_.-]", "_", table.name) + ".md"
510
+
511
+
512
+ def page_collisions(tables: list) -> list:
513
+ errors, by_page = [], {}
514
+ for t in tables:
515
+ page = page_name(t)
516
+ if page.lower() == "readme.md":
517
+ errors.append(f"{t.script}:{t.line}: table name {t.name!r} would overwrite the catalog index")
518
+ elif page.lower() in by_page:
519
+ other = by_page[page.lower()]
520
+ errors.append(f"{t.script}:{t.line}: tables {t.name!r} and {other.name!r} ({other.script}) map to the same page {page}")
521
+ by_page.setdefault(page.lower(), t)
522
+ return errors
523
+
524
+
525
+ def link_to(root: Path, output: str, target: str) -> str:
526
+ return Path(os.path.relpath(root / target, root / output)).as_posix()
527
+
528
+
529
+ def render_table(table: TableDoc, root: Path, config: dict) -> str:
530
+ langs = config["languages"]
531
+ out = [GENERATED_MARK, "", f"# `{table.name}`", ""]
532
+ out.append(f"- **Script:** [{table.script}]({link_to(root, config['output'], table.script)})")
533
+ out.append(f"- **Fields:** {len(table.fields)}")
534
+ out.append("")
535
+ for lang in langs:
536
+ grain_label, notes_label = SECTION_LABELS.get(lang, SECTION_LABELS["en"])
537
+ if len(langs) > 1:
538
+ out += [f"## {label(lang)}", ""]
539
+ out += [cell(table.text[lang]), "", f"- **{grain_label}** {cell(table.grain[lang])}"]
540
+ if table.notes[lang]:
541
+ out.append(f"- **{notes_label}**")
542
+ out.extend(f" - {cell(note)}" for note in table.notes[lang])
543
+ out.append("")
544
+ if len(langs) > 1:
545
+ out += ["## Fields", ""]
546
+ has_window = any(f.window for f in table.fields)
547
+ has_unit = any(f.unit for f in table.fields)
548
+ head = ["Field", *(label(lang) for lang in langs), "Source"]
549
+ head += ["Window"] if has_window else []
550
+ head += ["Unit"] if has_unit else []
551
+ out.append("| " + " | ".join(head) + " |")
552
+ out.append("|" + "---|" * len(head))
553
+ for f in table.fields:
554
+ row = [f"`{f.name}`", *(cell(f.text[lang]) for lang in langs), cell(f.source)]
555
+ row += [cell(f.window)] if has_window else []
556
+ row += [cell(f.unit)] if has_unit else []
557
+ out.append("| " + " | ".join(row) + " |")
558
+ return "\n".join(out) + "\n"
559
+
560
+
561
+ def render_index(tables: list, root: Path, config: dict) -> str:
562
+ lang = config["languages"][0]
563
+ engine = ENGINE_PATH
564
+ out = [
565
+ GENERATED_MARK, "", "# Data catalog", "",
566
+ "Table and column definitions, generated from the metadata in each table script. "
567
+ f"To change a definition, edit `{TABLE_VAR}` / `{FIELDS_VAR}` (or the `-- @field` block in a .sql file), "
568
+ f"run `python3 {engine} generate`, and commit the script and these pages together.",
569
+ "",
570
+ ]
571
+ if not tables:
572
+ out.append("No table script carries catalog metadata yet. Run "
573
+ f"`python3 {engine} audit` to see which scripts need it.")
574
+ return "\n".join(out) + "\n"
575
+ out += ["| Table | Summary | Grain | Fields | Script |", "|---|---|---|---|---|"]
576
+ for t in tables:
577
+ script = f"[{t.script}]({link_to(root, config['output'], t.script)})"
578
+ out.append(f"| [`{t.name}`]({page_name(t)}) | {cell(t.text[lang])} | {cell(t.grain[lang])} | {len(t.fields)} | {script} |")
579
+ return "\n".join(out) + "\n"
580
+
581
+
582
+ def generation_plan(root: Path, config: dict):
583
+ tables, errors, warnings = collect(root, config)
584
+ plan = {}
585
+ if not errors:
586
+ out_dir = config["output"]
587
+ plan[f"{out_dir}/README.md"] = render_index(tables, root, config)
588
+ for table in tables:
589
+ plan[f"{out_dir}/{page_name(table)}"] = render_table(table, root, config)
590
+ return plan, errors, warnings
591
+
592
+
593
+ def orphan_pages(root: Path, config: dict, plan: dict) -> list:
594
+ out_dir = root / config["output"]
595
+ if not out_dir.is_dir():
596
+ return []
597
+ orphans = []
598
+ for path in sorted(out_dir.glob("*.md")):
599
+ key = rel(root, path)
600
+ if key in plan:
601
+ continue
602
+ try:
603
+ first = path.read_text(encoding="utf-8").split("\n", 1)[0]
604
+ except (UnicodeDecodeError, OSError):
605
+ continue
606
+ if first == GENERATED_MARK:
607
+ orphans.append(key)
608
+ return orphans
609
+
610
+
611
+ def cmd_generate(root: Path, check: bool) -> int:
612
+ config = load_config(root)
613
+ plan, errors, warnings = generation_plan(root, config)
614
+ for warning in warnings:
615
+ print(f"warning: {warning}", file=sys.stderr)
616
+ if errors:
617
+ for error in errors:
618
+ print(f"error: {error}", file=sys.stderr)
619
+ print(f"data-catalog: {len(errors)} metadata error(s); nothing written", file=sys.stderr)
620
+ return 1
621
+ stale = [p for p, text in plan.items() if read_or_none(root / p) != text]
622
+ orphans = orphan_pages(root, config, plan)
623
+ if check:
624
+ if stale or orphans:
625
+ for p in stale:
626
+ print(f"stale: {p}", file=sys.stderr)
627
+ for p in orphans:
628
+ print(f"orphan: {p} (its table no longer has metadata)", file=sys.stderr)
629
+ print(f"data-catalog: the catalog does not match the table scripts. Run `python3 {ENGINE_PATH} generate` "
630
+ "and commit the result.", file=sys.stderr)
631
+ return 1
632
+ print(f"data-catalog: catalog is current ({len(plan) - 1} table(s))")
633
+ return 0
634
+ for p in stale:
635
+ target = root / p
636
+ target.parent.mkdir(parents=True, exist_ok=True)
637
+ target.write_text(plan[p], encoding="utf-8")
638
+ print(f"wrote {p}")
639
+ for p in orphans:
640
+ (root / p).unlink()
641
+ print(f"removed {p}")
642
+ print(f"data-catalog: {len(plan) - 1} table(s); {len(stale)} written, {len(orphans)} removed")
643
+ return 0
644
+
645
+
646
+ def read_or_none(path: Path) -> str | None:
647
+ try:
648
+ return path.read_text(encoding="utf-8")
649
+ except FileNotFoundError:
650
+ return None
651
+
652
+
653
+ # ---------------------------------------------------------------------------
654
+ # Audit: what scripts write, which columns, what is documented
655
+ # ---------------------------------------------------------------------------
656
+
657
+ WRITE_RES = [
658
+ re.compile(r"\binsert\s+(?:overwrite|into)\s+(?:table\s+)?([`\"\w.{}$-]+)", re.I),
659
+ re.compile(r"\bcreate\s+(?:or\s+replace\s+)?(?:temporary\s+|temp\s+)?table\s+(?:if\s+not\s+exists\s+)?([`\"\w.{}$-]+)[^;]*?\bas\b\s*(?=with\b|select\b|\()", re.I | re.S),
660
+ re.compile(r"\bmerge\s+into\s+([`\"\w.{}$-]+)", re.I),
661
+ ]
662
+ API_WRITE_RES = [
663
+ re.compile(r"""\.(?:saveAsTable|insertInto)\(\s*[fr]?["']([^"']+)["']"""),
664
+ re.compile(r"""["']hoodie\.table\.name["']\s*[:,]\s*[fr]?["']([^"']+)["']"""),
665
+ ]
666
+
667
+
668
+ def mask_sql(sql: str, strings: bool = True) -> str:
669
+ """Blank out comments (and string literals unless strings=False), keeping offsets."""
670
+ out, i, n = [], 0, len(sql)
671
+ while i < n:
672
+ two = sql[i:i + 2]
673
+ if two == "--":
674
+ j = sql.find("\n", i)
675
+ j = n if j == -1 else j
676
+ out.append(" " * (j - i)); i = j
677
+ elif two == "/*":
678
+ j = sql.find("*/", i + 2)
679
+ j = n if j == -1 else j + 2
680
+ out.append(re.sub(r"[^\n]", " ", sql[i:j])); i = j
681
+ elif sql[i] == "'":
682
+ j = i + 1
683
+ while j < n and not (sql[j] == "'" and sql[j + 1:j + 2] != "'"):
684
+ j += 2 if sql[j:j + 2] in ("''", "\\'", "\\\\") else 1
685
+ j = min(j + 1, n)
686
+ out.append(sql[i:j] if not strings else "'" + " " * (j - i - 2) + "'" if j - i >= 2 else sql[i:j]); i = j
687
+ else:
688
+ out.append(sql[i]); i += 1
689
+ return "".join(out)
690
+
691
+
692
+ def split_top(text: str, sep: str = ",") -> list:
693
+ parts, depth, start = [], 0, 0
694
+ for i, ch in enumerate(text):
695
+ if ch in "([{":
696
+ depth += 1
697
+ elif ch in ")]}":
698
+ depth -= 1
699
+ elif ch == sep and depth == 0:
700
+ parts.append(text[start:i]); start = i + 1
701
+ parts.append(text[start:])
702
+ return parts
703
+
704
+
705
+ def top_level_word(text: str, word: str, start: int = 0) -> int:
706
+ depth, quote = 0, None
707
+ pattern = re.compile(rf"{word}\b", re.I)
708
+ i = start
709
+ while i < len(text):
710
+ ch = text[i]
711
+ if quote:
712
+ quote = None if ch == quote else quote
713
+ elif ch in "`\"":
714
+ quote = ch
715
+ elif ch in "([{":
716
+ depth += 1
717
+ elif ch in ")]}":
718
+ depth -= 1
719
+ elif depth == 0 and pattern.match(text, i) and (i == 0 or not (text[i - 1].isalnum() or text[i - 1] == "_")):
720
+ if not (word.lower() == "from" and re.search(r"\bdistinct\s*$", text[:i], re.I)):
721
+ return i
722
+ i += 1
723
+ return -1
724
+
725
+
726
+ IDENT = r"(?:`[^`]+`|\"[^\"]+\"|[A-Za-z_]\w*)"
727
+
728
+
729
+ def unquote(ident: str) -> str:
730
+ return ident[1:-1] if ident[:1] in "`\"" else ident
731
+
732
+
733
+ def select_items(statement: str, original: str | None = None):
734
+ """Outermost SELECT items as [(name or None, expression)], plus (select_end, body_end) offsets.
735
+
736
+ `statement` is masked text; `original` (same offsets) supplies expressions with literals intact.
737
+ """
738
+ original = statement if original is None else original
739
+ at = top_level_word(statement, "select")
740
+ if at == -1:
741
+ return None
742
+ body_start = at + len("select")
743
+ end = top_level_word(statement, "from", body_start)
744
+ end = len(statement) if end == -1 else end
745
+ body = statement[body_start:end]
746
+ lead = re.match(r"\s*(?:distinct|all)\b", body, re.I)
747
+ offset = body_start + (lead.end() if lead else 0)
748
+ items, pos = [], offset
749
+ for part in split_top(statement[offset:end]):
750
+ expr_masked = " ".join(part.split())
751
+ expr = " ".join(original[pos:pos + len(part)].split())
752
+ pos += len(part) + 1
753
+ if not expr_masked:
754
+ continue
755
+ items.append((item_name(expr_masked), expr))
756
+ return items, at, end
757
+
758
+
759
+ def item_name(item: str) -> str | None:
760
+ """The output name of one SELECT item, or None when it has no name the engine can read for sure."""
761
+ if item == "*" or item.endswith(".*") or "{" in item:
762
+ return None
763
+ alias = re.search(rf"\bas\s+({IDENT})\s*$", item, re.I)
764
+ if alias:
765
+ return unquote(alias.group(1))
766
+ ref = rf"(?:{IDENT}\.)*({IDENT})"
767
+ bare = re.fullmatch(ref, item)
768
+ if bare:
769
+ return None if bare.group(1).lower() in SQL_KEYWORDS else unquote(bare.group(1))
770
+ implicit = re.fullmatch(rf"(.*\S)\s+({IDENT})", item) # `sum(x) total`, `t.col c`, `CASE ... END flag`
771
+ if implicit:
772
+ head, name = implicit.group(1), unquote(implicit.group(2))
773
+ plain = re.fullmatch(ref, head)
774
+ head_ok = head.endswith(")") or re.match(r"(?is)case\b.*\bend$", head) \
775
+ or (plain and plain.group(1).lower() not in SQL_KEYWORDS)
776
+ if head_ok and name.lower() not in SQL_KEYWORDS:
777
+ return name
778
+ return None
779
+
780
+
781
+ def select_columns(statement: str):
782
+ """Output column names of a statement's outermost SELECT; (names, complete)."""
783
+ found = select_items(statement)
784
+ if found is None:
785
+ return [], False
786
+ items = found[0]
787
+ names = [n for n, _ in items if n]
788
+ return names, len(names) == len(items)
789
+
790
+
791
+ def statement_at(masked: str, start: int) -> str:
792
+ end = masked.find(";", start)
793
+ return masked[start:end if end != -1 else len(masked)]
794
+
795
+
796
+ def sql_writes(sql: str):
797
+ """Tables a SQL text writes, each with the columns of its SELECT."""
798
+ masked = mask_sql(sql)
799
+ found = []
800
+ for pattern in WRITE_RES:
801
+ for match in pattern.finditer(masked):
802
+ table = match.group(1).strip("`\"")
803
+ if table.lower() in ("table", "select", "with", "partition", "if", "directory", "local"):
804
+ continue
805
+ names, complete = select_columns(statement_at(masked, match.end()))
806
+ found.append((table, names, complete))
807
+ return found
808
+
809
+
810
+ def python_strings(tree: ast.Module) -> list:
811
+ out = []
812
+ fragments = {id(v) for node in ast.walk(tree) if isinstance(node, ast.JoinedStr) for v in node.values}
813
+ docstrings = {id(n.body[0].value) for n in ast.walk(tree)
814
+ if isinstance(n, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef))
815
+ and n.body and isinstance(n.body[0], ast.Expr) and isinstance(n.body[0].value, ast.Constant)}
816
+ for node in ast.walk(tree):
817
+ if id(node) in fragments or id(node) in docstrings:
818
+ continue
819
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
820
+ out.append(node.value)
821
+ elif isinstance(node, ast.JoinedStr):
822
+ parts = [v.value if isinstance(v, ast.Constant) else "{expr}" for v in node.values]
823
+ out.append("".join(str(p) for p in parts))
824
+ return out
825
+
826
+
827
+ @dataclass
828
+ class ScriptAudit:
829
+ script: str
830
+ writes: list = field(default_factory=list) # [(table, evidence)]
831
+ columns: list = field(default_factory=list)
832
+ columns_from: str | None = None # "PUBLISHED_COLUMNS" | "sql select" | None
833
+ columns_complete: bool = False
834
+ table: TableDoc | None = None
835
+ findings: list = field(default_factory=list) # [(kind, message)]
836
+
837
+
838
+ def declared_tables(tree: ast.Module, config: dict) -> list:
839
+ """Table names assigned in module or class bodies, joined to a sibling schema name."""
840
+ table_names, schema_names = set(config.get("tableNames") or []), set(config.get("schemaNames") or [])
841
+ found = []
842
+ scopes = [tree.body] + [n.body for n in ast.walk(tree) if isinstance(n, ast.ClassDef)]
843
+ for body in scopes:
844
+ strings = {}
845
+ for node in body:
846
+ targets = node.targets if isinstance(node, ast.Assign) else [node.target] if isinstance(node, ast.AnnAssign) else []
847
+ value = getattr(node, "value", None)
848
+ if isinstance(value, ast.Constant) and isinstance(value.value, str):
849
+ for target in targets:
850
+ if isinstance(target, ast.Name):
851
+ strings[target.id] = value.value
852
+ schema = next((v for k, v in strings.items() if k in schema_names or SCHEMA_NAME_RE.match(k)), None)
853
+ for key, value in strings.items():
854
+ if (key in table_names or TABLE_NAME_RE.match(key)) and value.strip():
855
+ full = value if "." in value or not schema else f"{schema}.{value}"
856
+ found.append((full, key))
857
+ return found
858
+
859
+
860
+ def strip_py_comments(text: str) -> str:
861
+ out = []
862
+ try:
863
+ for tok in tokenize.generate_tokens(io.StringIO(text).readline):
864
+ if tok.type != tokenize.COMMENT:
865
+ out.append(tok)
866
+ return tokenize.untokenize(out)
867
+ except (tokenize.TokenError, IndentationError, SyntaxError):
868
+ return text
869
+
870
+
871
+ def audit_python(text: str, where: str, config: dict, audit: ScriptAudit) -> None:
872
+ tree = ast.parse(text)
873
+ names = module_assignments(tree)
874
+ audit.writes.extend(declared_tables(tree, config))
875
+ code = strip_py_comments(text)
876
+ for pattern in API_WRITE_RES:
877
+ for match in pattern.finditer(code):
878
+ audit.writes.append((match.group(1), "DataFrame write"))
879
+ for contract in config["contracts"]:
880
+ if contract in names:
881
+ state = contract_state(names[contract], names)
882
+ if isinstance(state, list):
883
+ audit.columns, audit.columns_from, audit.columns_complete = state, contract, True
884
+ break
885
+ if state == "derived" and audit.table is not None:
886
+ audit.columns = [f.name for f in audit.table.fields]
887
+ audit.columns_from, audit.columns_complete = f"{contract} (derived)", True
888
+ break
889
+ for chunk in python_strings(tree):
890
+ for table, cols, complete in sql_writes(chunk):
891
+ audit.writes.append((table, "SQL string"))
892
+ if audit.columns_from is None and cols:
893
+ audit.columns, audit.columns_from, audit.columns_complete = cols, "SQL select", complete
894
+
895
+
896
+ def audit_sql(text: str, audit: ScriptAudit) -> None:
897
+ for table, cols, complete in sql_writes(text):
898
+ audit.writes.append((table, "SQL statement"))
899
+ if audit.columns_from is None and cols:
900
+ audit.columns, audit.columns_from, audit.columns_complete = cols, "SQL select", complete
901
+
902
+
903
+ RELATIVE_RE = re.compile(r"^\s*(同上|同前|如上|same as (the )?(above|previous)|as above|ditto|see above)", re.I)
904
+
905
+
906
+ CODE_IN_TEXT_RE = re.compile(
907
+ r"\b[a-z_][a-z0-9_]+\.[a-z_][a-z0-9_]+\b" # schema.table or table.column (not "a.m.", "e.g.")
908
+ r"|<>|!=|<=|>=|\s=\s*'" # comparisons
909
+ r"|\b(?:inner|left|right|full|cross)\s+join\b"
910
+ r"|\b(?:date_sub|date_add|coalesce|count|sum|avg|min|max|cast|nvl|if|case)\s*\(", re.I)
911
+ CJK_RE = re.compile(r"[\u3400-\u9fff]")
912
+ LONG_WORDS, LONG_CJK = 30, 60
913
+
914
+
915
+ def text_size(text: str) -> tuple[int, int]:
916
+ """(latin words, CJK characters) in a definition."""
917
+ cjk = len(CJK_RE.findall(text))
918
+ return len(CJK_RE.sub(" ", text).split()), cjk
919
+
920
+
921
+ def shingles(text: str) -> set:
922
+ """Word 5-grams, plus 8-character runs of CJK text: the unit a repeated phrase is measured in."""
923
+ words = re.findall(r"[a-z0-9_']+", CJK_RE.sub(" ", text.lower()))
924
+ grams = {" ".join(words[i:i + 5]) for i in range(len(words) - 4)}
925
+ for run in re.findall(r"[\u3400-\u9fff]{8,}", text):
926
+ grams |= {run[i:i + 8] for i in range(len(run) - 7)}
927
+ return grams
928
+
929
+
930
+ def repeated_phrase(fields: list):
931
+ """The phrase shared by the most rows, when at least 3 rows and half of them carry it."""
932
+ if len(fields) < 3:
933
+ return None
934
+ counts = {}
935
+ for f in fields:
936
+ for gram in set().union(*(shingles(t) for t in (*f.text.values(), f.source))):
937
+ counts[gram] = counts.get(gram, 0) + 1
938
+ best = max(counts.items(), key=lambda kv: (kv[1], len(kv[0])), default=None)
939
+ if best and best[1] >= 3 and best[1] * 2 >= len(fields):
940
+ return best
941
+ return None
942
+
943
+
944
+ def restates_name(name: str, text: str) -> bool:
945
+ words = re.sub(r"[\W_]+", " ", text).strip().lower()
946
+ return words == re.sub(r"[\W_]+", " ", name).strip().lower() or len(text.strip()) < 4
947
+
948
+
949
+ def audit_script(path: Path, root: Path, config: dict) -> ScriptAudit:
950
+ where = rel(root, path)
951
+ audit = ScriptAudit(where)
952
+ try:
953
+ text = path.read_text(encoding="utf-8")
954
+ except (UnicodeDecodeError, OSError) as exc:
955
+ audit.findings.append(("unreadable", str(exc)))
956
+ return audit
957
+ try:
958
+ audit.table = parse_script(path, root, config)
959
+ except CatalogError as exc:
960
+ audit.findings.append(("invalid-metadata", str(exc)))
961
+ if ": cannot parse:" in str(exc):
962
+ return audit
963
+ try:
964
+ if path.suffix == ".py":
965
+ audit_python(text, where, config, audit)
966
+ else:
967
+ audit_sql(text, audit)
968
+ except SyntaxError as exc:
969
+ audit.findings.append(("unreadable", f"cannot parse Python: {exc.msg} (line {exc.lineno})"))
970
+ return audit
971
+ seen, unique = set(), []
972
+ for table, evidence in audit.writes:
973
+ if table not in seen:
974
+ seen.add(table); unique.append((table, evidence))
975
+ audit.writes = unique
976
+ return audit
977
+
978
+
979
+ def short_name(table: str) -> str:
980
+ return table.split(".")[-1].strip("`\"").lower()
981
+
982
+
983
+ def is_dynamic(table: str) -> bool:
984
+ return "{" in table or "$" in table
985
+
986
+
987
+ def same_table(a: str, b: str) -> bool:
988
+ """Equal names; when one side has no schema, equal short names. Templated names match nothing."""
989
+ if is_dynamic(a) or is_dynamic(b):
990
+ return False
991
+ a, b = a.strip("`\"").lower(), b.strip("`\"").lower()
992
+ return a == b or (("." not in a or "." not in b) and short_name(a) == short_name(b))
993
+
994
+
995
+ def lookup(table: str, by_table: dict):
996
+ return [v for k, v in by_table.items() if same_table(k, table)]
997
+
998
+
999
+ def judge(audit: ScriptAudit, documented_in: dict, writers_of: dict) -> None:
1000
+ """Findings for one script; documented_in/writers_of are keyed by short table name."""
1001
+ t = audit.table
1002
+ targets = [w[0] for w in audit.writes]
1003
+ if t is None:
1004
+ open_targets = [x for x in targets if not is_dynamic(x) and not lookup(x, documented_in)]
1005
+ if open_targets and not any(k == "invalid-metadata" for k, _ in audit.findings):
1006
+ others = sorted({s for x in open_targets for group in lookup(x, writers_of) for s in group if s != audit.script})
1007
+ also = f"; also written by {', '.join(others)} (put the metadata in the script that builds the table)" if others else ""
1008
+ audit.findings.append(("no-catalog", f"writes {', '.join(open_targets)} but carries no {TABLE_VAR}/{FIELDS_VAR} "
1009
+ f"(or -- @table block){also}"))
1010
+ if audit.columns and open_targets:
1011
+ audit.findings.append(("undocumented", f"{len(audit.columns)} column(s) with no definition"))
1012
+ return
1013
+ documented = [f.name for f in t.fields]
1014
+ static = [x for x in targets if not is_dynamic(x)]
1015
+ if static and not any(same_table(t.name, x) for x in static):
1016
+ audit.findings.append(("table-mismatch", f"metadata names {t.name} but the script writes {', '.join(static)}"))
1017
+ if audit.columns:
1018
+ missing = [c for c in audit.columns if c not in documented]
1019
+ if missing:
1020
+ audit.findings.append(("undocumented", f"{len(missing)} column(s) with no Field: {preview(missing)}"))
1021
+ if audit.columns_complete and audit.columns_from == "SQL select":
1022
+ extra = [n for n in documented if n not in audit.columns]
1023
+ if extra:
1024
+ audit.findings.append(("not-in-output", f"Field(s) not in the SELECT output: {preview(extra)}"))
1025
+ else:
1026
+ audit.findings.append(("columns-unknown", "output columns could not be read statically; declare a "
1027
+ f"contract tuple derived from {FIELDS_VAR}, or compare against the warehouse schema"))
1028
+ for contract, state in t.contracts.items():
1029
+ if isinstance(state, list):
1030
+ audit.findings.append(("contract-not-derived", f"{contract} is a literal list; derive it from {FIELDS_VAR}"))
1031
+ relative = [f.name for f in t.fields if any(RELATIVE_RE.match(x) for x in (*f.text.values(), f.source))]
1032
+ if relative:
1033
+ audit.findings.append(("relative-definition", f"definition or source points at another row (\"same as above\"); "
1034
+ f"each row must stand alone: {preview(relative)}"))
1035
+ thin = [f.name for f in t.fields if f.name not in relative and (
1036
+ any(restates_name(f.name, f.text[lang]) for lang in f.text) or restates_name(f.name, f.source))]
1037
+ repeated = repeated_phrase(t.fields)
1038
+ if repeated:
1039
+ audit.findings.append(("repeated-text", f"\"{repeated[0]}…\" appears in {repeated[1]} of {len(t.fields)} rows; "
1040
+ "a fact true for every column belongs once in the table notes"))
1041
+ coded = [f.name for f in t.fields if any(CODE_IN_TEXT_RE.search(x) for x in f.text.values())]
1042
+ if coded:
1043
+ audit.findings.append(("code-in-description", "table names, operators or SQL functions in the meaning text; "
1044
+ f"move lineage to source: {preview(coded)}"))
1045
+ long = [f.name for f in t.fields if any(w > LONG_WORDS or c > LONG_CJK for w, c in map(text_size, f.text.values()))]
1046
+ if long:
1047
+ audit.findings.append(("long-definition", f"over {LONG_WORDS} words or {LONG_CJK} Chinese characters; "
1048
+ f"usually a table-level fact or lineage: {preview(long)}"))
1049
+ if thin:
1050
+ audit.findings.append(("thin-definition", f"definition or source only restates the name: {preview(thin)}"))
1051
+
1052
+
1053
+ def preview(items: list, limit: int = 12) -> str:
1054
+ shown = ", ".join(items[:limit])
1055
+ return shown + (f", … (+{len(items) - limit})" if len(items) > limit else "")
1056
+
1057
+
1058
+ def cmd_audit(root: Path, globs: list, as_json: bool, verbose: bool, table_names: list, schema_names: list,
1059
+ contracts: list) -> int:
1060
+ config_path = root / CONFIG_FILE
1061
+ config = load_config(root) if config_path.exists() else dict(DEFAULT_CONFIG)
1062
+ config = {**config, "tableNames": [*config["tableNames"], *table_names],
1063
+ "schemaNames": [*config["schemaNames"], *schema_names],
1064
+ "contracts": [*config["contracts"], *contracts]}
1065
+ if globs:
1066
+ patterns, exclude = globs, config.get("exclude", [])
1067
+ elif config_path.exists():
1068
+ patterns, exclude = config["scripts"], config["exclude"]
1069
+ else:
1070
+ raise CatalogError(f"no {CONFIG_FILE}: pass globs to audit, e.g. `audit 'dags/**/*.py' '**/*.sql'`")
1071
+ audits = [audit_script(p, root, config) for p in find_scripts(root, patterns, exclude)]
1072
+ documented_in, writers_of = {}, {}
1073
+ for a in audits:
1074
+ if a.table:
1075
+ documented_in.setdefault(a.table.name.lower(), a.script)
1076
+ for table, _ in a.writes:
1077
+ if not is_dynamic(table):
1078
+ writers_of.setdefault(table.strip("`\"").lower(), []).append(a.script)
1079
+ for a in audits:
1080
+ judge(a, documented_in, writers_of)
1081
+ relevant = [a for a in audits if a.writes or a.table or a.findings]
1082
+ writers = [a for a in audits if a.writes or a.table]
1083
+ tables = sorted(set(documented_in) | {k for k in writers_of if not lookup(k, documented_in)})
1084
+ complete = sorted({a.table.name.lower() for a in writers if a.table
1085
+ and not any(k in ("undocumented", "invalid-metadata") for k, _ in a.findings)})
1086
+ if as_json:
1087
+ print(json.dumps({
1088
+ "engine": ENGINE_VERSION,
1089
+ "scanned": len(audits),
1090
+ "writers": len(writers),
1091
+ "tables": len(tables),
1092
+ "documentedTables": len(complete),
1093
+ "scripts": [{
1094
+ "script": a.script,
1095
+ "writes": [{"table": t, "evidence": e} for t, e in a.writes],
1096
+ "documentedTable": a.table.name if a.table else None,
1097
+ "documentedFields": len(a.table.fields) if a.table else 0,
1098
+ "columns": a.columns,
1099
+ "columnsFrom": a.columns_from,
1100
+ "columnsComplete": a.columns_complete,
1101
+ "findings": [{"kind": k, "message": m} for k, m in a.findings],
1102
+ } for a in relevant],
1103
+ }, ensure_ascii=False, indent=2))
1104
+ return 0
1105
+ print(f"data-catalog audit: {len(audits)} script(s) scanned; {len(writers)} write {len(tables)} table(s); "
1106
+ f"{len(complete)} table(s) fully documented")
1107
+ for a in sorted(relevant, key=lambda x: (x.table is not None, x.script)):
1108
+ if not a.findings and not verbose:
1109
+ continue
1110
+ print(f"\n{a.script}")
1111
+ if a.writes:
1112
+ print(" writes: " + ", ".join(f"{t} ({e})" for t, e in a.writes))
1113
+ cols = f"{len(a.columns)} from {a.columns_from}" if a.columns_from else "unknown"
1114
+ if a.columns_from and not a.columns_complete:
1115
+ cols += " (partial: wildcard or dynamic SELECT items)"
1116
+ print(f" columns: {cols}")
1117
+ print(f" catalog: {a.table.name + f' ({len(a.table.fields)} fields)' if a.table else 'none'}")
1118
+ for kind, message in a.findings:
1119
+ print(f" - {kind}: {message}")
1120
+ quiet = len(audits) - len(relevant)
1121
+ if quiet:
1122
+ print(f"\n{quiet} script(s) write no table the audit can see (helpers, DAGs, tests).")
1123
+ return 0
1124
+
1125
+
1126
+ # ---------------------------------------------------------------------------
1127
+ # drift: code changed since a git base while its definitions did not
1128
+ # ---------------------------------------------------------------------------
1129
+
1130
+ def git(root: Path, *args: str) -> tuple[int, str]:
1131
+ try:
1132
+ r = subprocess.run(["git", *args], cwd=root, capture_output=True, text=True)
1133
+ except FileNotFoundError as exc:
1134
+ raise CatalogError("drift needs git on PATH") from exc
1135
+ return r.returncode, r.stdout
1136
+
1137
+
1138
+ def statement_shape(text: str, suffix: str, table_name: str):
1139
+ """({column: expression}, rest) for the statement writing table_name; rest is its FROM/WHERE/JOIN/CTE text."""
1140
+ try:
1141
+ chunks = [text] if suffix == ".sql" else python_strings(ast.parse(text))
1142
+ except SyntaxError:
1143
+ return None
1144
+ for chunk in chunks:
1145
+ masked, readable = mask_sql(chunk), mask_sql(chunk, strings=False)
1146
+ for pattern in WRITE_RES:
1147
+ for match in pattern.finditer(masked):
1148
+ if not same_table(match.group(1).strip("`\""), table_name):
1149
+ continue
1150
+ stop = masked.find(";", match.end())
1151
+ stop = len(masked) if stop == -1 else stop
1152
+ found = select_items(masked[match.end():stop], readable[match.end():stop])
1153
+ if found is None:
1154
+ continue
1155
+ items, at, end = found
1156
+ base = match.end()
1157
+ rest = " ".join((readable[match.start():base + at] + " " + readable[base + end:stop]).split())
1158
+ return {n: e for n, e in items if n}, rest
1159
+ return None
1160
+
1161
+
1162
+ def code_without_metadata(text: str, suffix: str) -> str:
1163
+ if suffix == ".sql":
1164
+ return " ".join(mask_sql(text, strings=False).split())
1165
+ try:
1166
+ tree = ast.parse(text)
1167
+ except SyntaxError:
1168
+ return text
1169
+ tree.body = [n for n in tree.body if not (isinstance(n, (ast.Assign, ast.AnnAssign)) and any(
1170
+ isinstance(t, ast.Name) and t.id in (TABLE_VAR, FIELDS_VAR)
1171
+ for t in (n.targets if isinstance(n, ast.Assign) else [n.target])))]
1172
+ return ast.dump(tree)
1173
+
1174
+
1175
+ def definitions(table: TableDoc | None) -> tuple[dict, tuple]:
1176
+ if table is None:
1177
+ return {}, ()
1178
+ fields = {f.name: (tuple(sorted(f.text.items())), f.source, f.window, f.unit) for f in table.fields}
1179
+ notes = tuple((lang, tuple(n)) for lang, n in sorted(table.notes.items()))
1180
+ return fields, (tuple(sorted(table.grain.items())), tuple(sorted(table.text.items())), notes)
1181
+
1182
+
1183
+ def drift_items(root: Path, config: dict, base: str) -> list:
1184
+ code, _ = git(root, "rev-parse", "--verify", f"{base}^{{commit}}")
1185
+ if code != 0:
1186
+ raise CatalogError(f"drift: {base!r} is not a commit in this git repository")
1187
+ _, out = git(root, "diff", "--name-only", base, "--")
1188
+ changed = set(out.split())
1189
+ items = []
1190
+ for path in find_scripts(root, config["scripts"], config["exclude"]):
1191
+ where = rel(root, path)
1192
+ if where not in changed:
1193
+ continue
1194
+ code, old_text = git(root, "show", f"{base}:{where}")
1195
+ if code != 0:
1196
+ continue # a new script: generate checks that every column is defined
1197
+ new_text = path.read_text(encoding="utf-8")
1198
+ parse = parse_python if path.suffix == ".py" else None
1199
+ try:
1200
+ new_t = (parse(new_text, where, config["languages"], config["contracts"]) if parse
1201
+ else parse_sql(new_text, where, config["languages"]))
1202
+ except CatalogError:
1203
+ continue # generate reports invalid metadata
1204
+ if new_t is None:
1205
+ continue # undocumented: audit reports it
1206
+ try:
1207
+ old_t = (parse(old_text, where, config["languages"], config["contracts"]) if parse
1208
+ else parse_sql(old_text, where, config["languages"]))
1209
+ except CatalogError:
1210
+ old_t = None
1211
+ old_fields, old_table = definitions(old_t)
1212
+ new_fields, new_table = definitions(new_t)
1213
+ edited = {n for n, d in new_fields.items() if old_fields.get(n) != d}
1214
+ any_edit = bool(edited) or old_table != new_table
1215
+ old_shape = statement_shape(old_text, path.suffix, new_t.name)
1216
+ new_shape = statement_shape(new_text, path.suffix, new_t.name)
1217
+ found = []
1218
+ if old_shape and new_shape:
1219
+ for name, expr in new_shape[0].items():
1220
+ if name in old_shape[0] and old_shape[0][name] != expr and name in new_fields and name not in edited:
1221
+ found.append(f"{name}: its SELECT expression changed; its definition did not")
1222
+ if old_shape[1] != new_shape[1] and not any_edit:
1223
+ found.append("filters, joins or CTEs changed; no definition changed (they shape every column)")
1224
+ elif code_without_metadata(old_text, path.suffix) != code_without_metadata(new_text, path.suffix) and not any_edit:
1225
+ found.append("code changed and no definition changed; the columns could not be read, so check each one")
1226
+ if found:
1227
+ items.append((where, new_t.name, found))
1228
+ return items
1229
+
1230
+
1231
+ def cmd_drift(root: Path, base: str) -> int:
1232
+ config = load_config(root)
1233
+ items = drift_items(root, config, base)
1234
+ if not items:
1235
+ print(f"data-catalog drift since {base}: none")
1236
+ return 0
1237
+ count = sum(len(f) for _, _, f in items)
1238
+ print(f"data-catalog drift since {base}: {count} item(s) to confirm")
1239
+ for where, table, found in items:
1240
+ print(f"\n{where} ({table})")
1241
+ for line in found:
1242
+ print(f" - {line}")
1243
+ print("\nUpdate each definition to match the code, or confirm it still holds and say so in the PR.")
1244
+ return 1
1245
+
1246
+
1247
+ # ---------------------------------------------------------------------------
1248
+ # init: config, engine copy, Field/Table module, workflow
1249
+ # (the CONTRIBUTING.md section and AGENTS.md pointer come from persist-conventions.mjs)
1250
+ # ---------------------------------------------------------------------------
1251
+
1252
+ def render_fields_module(languages: list) -> str:
1253
+ lang_fields = "\n".join(f" {lang}: str" for lang in languages)
1254
+ grain_fields = "\n".join(f" grain_{lang}: str" for lang in languages)
1255
+ notes_fields = "\n".join(f" notes_{lang}: tuple[str, ...] = ()" for lang in languages)
1256
+ return f'''"""Column metadata types for the data catalog.
1257
+
1258
+ Generated by data_catalog.py init from data-catalog.json; rerun init after changing
1259
+ `languages` there, never edit this file. Table scripts declare:
1260
+
1261
+ CATALOG_TABLE = Table("schema.table", {", ".join(f"{lang}=..., grain_{lang}=..." for lang in languages)})
1262
+ {FIELDS_VAR} = (Field("column", {", ".join(f"{lang}=..." for lang in languages)}, source=...), ...)
1263
+ PUBLISHED_COLUMNS = tuple(f.name for f in {FIELDS_VAR})
1264
+ """
1265
+ from __future__ import annotations
1266
+
1267
+ from dataclasses import dataclass
1268
+
1269
+
1270
+ @dataclass(frozen=True)
1271
+ class Field:
1272
+ name: str
1273
+ {lang_fields}
1274
+ source: str
1275
+ window: str | None = None
1276
+ unit: str | None = None
1277
+
1278
+
1279
+ @dataclass(frozen=True)
1280
+ class Table:
1281
+ name: str
1282
+ {lang_fields}
1283
+ {grain_fields}
1284
+ {notes_fields}
1285
+ '''
1286
+
1287
+
1288
+ def render_workflow(config: dict) -> str:
1289
+ paths = [CONFIG_FILE, ENGINE_PATH, f"{config['output']}/**", *config["scripts"]]
1290
+ if config.get("fieldsModule"):
1291
+ paths.append(config["fieldsModule"])
1292
+ path_lines = "\n".join(" - '" + p.replace("'", "''") + "'" for p in dict.fromkeys(paths))
1293
+ return f"""{WORKFLOW_MARK}
1294
+ name: Data catalog
1295
+
1296
+ on:
1297
+ pull_request:
1298
+ paths:
1299
+ {path_lines}
1300
+ workflow_dispatch:
1301
+
1302
+ permissions:
1303
+ contents: read
1304
+
1305
+ concurrency:
1306
+ group: data-catalog-${{{{ github.ref }}}}
1307
+ cancel-in-progress: true
1308
+
1309
+ jobs:
1310
+ catalog:
1311
+ name: Data catalog
1312
+ runs-on: ubuntu-latest
1313
+ timeout-minutes: 5
1314
+ steps:
1315
+ - uses: actions/checkout@v4
1316
+ - uses: actions/setup-python@v5
1317
+ with:
1318
+ python-version: '3.12'
1319
+ - name: Check the catalog matches the table scripts
1320
+ run: python3 {ENGINE_PATH} generate --check
1321
+ """
1322
+
1323
+
1324
+ def default_fields_module(root: Path, scripts: list) -> str:
1325
+ py = [p for p in find_scripts(root, scripts, []) if p.suffix == ".py"]
1326
+ if not py:
1327
+ return "catalog_fields.py"
1328
+ common = os.path.commonpath([str(p.parent) for p in py])
1329
+ return rel(root, Path(common) / "catalog_fields.py")
1330
+
1331
+
1332
+ def init_plan(root: Path, args) -> tuple[dict, list]:
1333
+ path = root / CONFIG_FILE
1334
+ config = {**DEFAULT_CONFIG, **(read_config_file(root) if path.exists() else {})}
1335
+ if args.scripts:
1336
+ config["scripts"] = args.scripts
1337
+ if args.exclude:
1338
+ config["exclude"] = args.exclude
1339
+ if args.languages:
1340
+ config["languages"] = [x.strip() for x in args.languages.split(",") if x.strip()]
1341
+ if args.output:
1342
+ config["output"] = args.output.rstrip("/")
1343
+ if args.fields_module:
1344
+ config["fieldsModule"] = args.fields_module
1345
+ validate_config(config)
1346
+ if not config.get("fieldsModule"):
1347
+ config["fieldsModule"] = default_fields_module(root, config["scripts"])
1348
+ plan = [
1349
+ (CONFIG_FILE, json.dumps(config, ensure_ascii=False, indent=2) + "\n", "config"),
1350
+ (ENGINE_PATH, Path(__file__).read_text(encoding="utf-8"), f"engine {ENGINE_VERSION}"),
1351
+ (config["fieldsModule"], render_fields_module(config["languages"]), "Field/Table types for table scripts"),
1352
+ (WORKFLOW_PATH, render_workflow(config), "CI: fail a PR whose catalog is stale"),
1353
+ ]
1354
+ return config, plan
1355
+
1356
+
1357
+ def cmd_init(root: Path, args) -> int:
1358
+ config, plan = init_plan(root, args)
1359
+ changed = [(p, text, why) for p, text, why in plan if read_or_none(root / p) != text]
1360
+ for p, text, why in plan:
1361
+ state = "unchanged" if (p, text, why) not in changed else ("write" if args.write else "would write")
1362
+ print(f"{state:12} {p} ({why})")
1363
+ if not args.write:
1364
+ print("\ndry run: rerun with --write to apply")
1365
+ return 0
1366
+ for p, text, _ in changed:
1367
+ target = root / p
1368
+ target.parent.mkdir(parents=True, exist_ok=True)
1369
+ target.write_text(text, encoding="utf-8")
1370
+ print()
1371
+ return cmd_generate(root, check=False)
1372
+
1373
+
1374
+ # ---------------------------------------------------------------------------
1375
+ # CLI
1376
+ # ---------------------------------------------------------------------------
1377
+
1378
+ def engine_identity() -> str:
1379
+ digest = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()[:12]
1380
+ return f"{ENGINE_VERSION}+{digest}"
1381
+
1382
+
1383
+ def find_root(start: Path) -> Path:
1384
+ for candidate in [start, *start.parents]:
1385
+ if (candidate / CONFIG_FILE).exists() or (candidate / ".git").exists():
1386
+ return candidate
1387
+ return start
1388
+
1389
+
1390
+ def main(argv: list | None = None) -> int:
1391
+ parser = argparse.ArgumentParser(prog="data_catalog.py", description=__doc__.split("\n\n")[0])
1392
+ parser.add_argument("--root", type=Path, help="repo root (default: nearest dir with data-catalog.json or .git)")
1393
+ sub = parser.add_subparsers(dest="command", required=True)
1394
+ p_init = sub.add_parser("init", help="set the repo up (dry run unless --write)")
1395
+ p_init.add_argument("--scripts", action="append", help="glob of table scripts (.py/.sql); repeatable")
1396
+ p_init.add_argument("--exclude", action="append", help="glob to skip; repeatable")
1397
+ p_init.add_argument("--languages", help="comma-separated codes, e.g. en,zh")
1398
+ p_init.add_argument("--output", help="catalog directory (default docs/catalog)")
1399
+ p_init.add_argument("--fields-module", help="path for the generated Field/Table module")
1400
+ p_init.add_argument("--write", action="store_true")
1401
+ p_gen = sub.add_parser("generate", help="write the catalog pages")
1402
+ p_gen.add_argument("--check", action="store_true", help="write nothing; exit 1 when stale or invalid")
1403
+ p_audit = sub.add_parser("audit", help="report scripts with missing or weak column definitions")
1404
+ p_audit.add_argument("globs", nargs="*", help="globs to scan (default: config scripts)")
1405
+ p_audit.add_argument("--json", action="store_true")
1406
+ p_audit.add_argument("--verbose", action="store_true", help="also list scripts with no findings")
1407
+ p_audit.add_argument("--table-name", action="append", default=[], metavar="NAME",
1408
+ help="extra variable/attribute whose string value is the table written; repeatable")
1409
+ p_audit.add_argument("--schema-name", action="append", default=[], metavar="NAME",
1410
+ help="extra variable/attribute holding that table's schema; repeatable")
1411
+ p_audit.add_argument("--contract", action="append", default=[], metavar="NAME",
1412
+ help="extra column-contract name (tuple of names or StructType); repeatable")
1413
+ p_drift = sub.add_parser("drift", help="local check: code changed since a git base while its definitions did not")
1414
+ p_drift.add_argument("--base", default="HEAD", help="git ref to compare with (default HEAD: uncommitted work)")
1415
+ sub.add_parser("version", help="print the engine identity")
1416
+ args = parser.parse_args(argv)
1417
+ root = (args.root or find_root(Path.cwd())).resolve()
1418
+ try:
1419
+ if args.command == "version":
1420
+ print(engine_identity())
1421
+ return 0
1422
+ if args.command == "init":
1423
+ return cmd_init(root, args)
1424
+ if args.command == "generate":
1425
+ return cmd_generate(root, args.check)
1426
+ if args.command == "drift":
1427
+ return cmd_drift(root, args.base)
1428
+ return cmd_audit(root, args.globs, args.json, args.verbose, args.table_name, args.schema_name, args.contract)
1429
+ except CatalogError as exc:
1430
+ print(f"data-catalog: {exc}", file=sys.stderr)
1431
+ return 2
1432
+
1433
+
1434
+ if __name__ == "__main__":
1435
+ sys.exit(main())