@1aboveio/skills 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +204 -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 +75 -0
  29. package/skills/app-dev/data-catalog/references/field-definitions.md +116 -0
  30. package/skills/app-dev/data-catalog/references/format.md +121 -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 +1392 -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 +571 -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,294 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = ["Pillow>=10"]
4
+ # ///
5
+ """Extract metadata, container, and recompression signals from a raster image.
6
+
7
+ Usage:
8
+ uv run image_forensics.py input.jpg --out outdir/
9
+
10
+ Writes image-forensics.json and, for JPEG inputs, ela.png. The output is
11
+ evidence for analyst review, not an automatic authenticity decision.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import hashlib
18
+ import io
19
+ import json
20
+ import re
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from PIL import ExifTags, Image, ImageChops, ImageEnhance, ImageStat
25
+
26
+ EDITOR_PATTERNS = {
27
+ "Adobe Photoshop": rb"(?i)(adobe\s+photoshop|photoshop\s+3\.0|8BIM)",
28
+ "Adobe Lightroom": rb"(?i)(adobe\s+lightroom|lightroom)",
29
+ "GIMP": rb"(?i)(\bgimp\b|gimp-save)",
30
+ "Canva": rb"(?i)\bcanva\b",
31
+ "Affinity": rb"(?i)\baffinity(?:\s+photo)?\b",
32
+ "Pixelmator": rb"(?i)\bpixelmator\b",
33
+ "ImageMagick": rb"(?i)(imagemagick|magick)",
34
+ "Microsoft image tools": rb"(?i)(microsoft.*(?:photo|image)|windows photo editor)",
35
+ }
36
+
37
+ CAPTURE_PATTERNS = {
38
+ "Apple": rb"(?i)apple iphone|apple screenshot",
39
+ "Google": rb"(?i)google pixel",
40
+ "Samsung": rb"(?i)samsung",
41
+ "Canon": rb"(?i)canon",
42
+ "Nikon": rb"(?i)nikon",
43
+ "Sony": rb"(?i)sony",
44
+ "Fujifilm": rb"(?i)fujifilm",
45
+ }
46
+
47
+
48
+ def serializable(value: Any) -> Any:
49
+ if value is None or isinstance(value, (bool, int, float, str)):
50
+ return value
51
+ if isinstance(value, bytes):
52
+ return value[:256].decode("utf-8", "replace")
53
+ if isinstance(value, (list, tuple)):
54
+ return [serializable(v) for v in value]
55
+ if isinstance(value, dict):
56
+ return {str(k): serializable(v) for k, v in value.items()}
57
+ return str(value)
58
+
59
+
60
+ def extract_exif(image: Image.Image) -> dict[str, Any]:
61
+ out: dict[str, Any] = {}
62
+ try:
63
+ for tag, value in image.getexif().items():
64
+ out[ExifTags.TAGS.get(tag, str(tag))] = serializable(value)
65
+ except Exception as exc: # noqa: BLE001
66
+ out["_error"] = str(exc)
67
+ return out
68
+
69
+
70
+ def jpeg_markers(data: bytes) -> dict[str, Any]:
71
+ names = {
72
+ 0xD8: "SOI",
73
+ 0xD9: "EOI",
74
+ 0xDA: "SOS",
75
+ 0xDB: "DQT",
76
+ 0xC0: "SOF0",
77
+ 0xC2: "SOF2",
78
+ 0xC4: "DHT",
79
+ 0xDD: "DRI",
80
+ 0xE0: "APP0",
81
+ 0xE1: "APP1",
82
+ 0xE2: "APP2",
83
+ 0xED: "APP13",
84
+ 0xEE: "APP14",
85
+ 0xFE: "COM",
86
+ }
87
+ markers: list[dict[str, Any]] = []
88
+ if not data.startswith(b"\xff\xd8"):
89
+ return {"valid_soi": False, "markers": markers}
90
+ markers.append({"name": "SOI", "offset": 0, "length": 2})
91
+ pos = 2
92
+ while pos + 1 < len(data):
93
+ if data[pos] != 0xFF:
94
+ pos += 1
95
+ continue
96
+ while pos < len(data) and data[pos] == 0xFF:
97
+ pos += 1
98
+ if pos >= len(data):
99
+ break
100
+ code = data[pos]
101
+ marker_offset = pos - 1
102
+ pos += 1
103
+ if code == 0xD9:
104
+ markers.append({"name": "EOI", "offset": marker_offset, "length": 2})
105
+ break
106
+ if code in range(0xD0, 0xD8) or code == 0x01:
107
+ markers.append(
108
+ {
109
+ "name": names.get(code, f"0xFF{code:02X}"),
110
+ "offset": marker_offset,
111
+ "length": 2,
112
+ }
113
+ )
114
+ continue
115
+ if pos + 2 > len(data):
116
+ break
117
+ segment_length = int.from_bytes(data[pos : pos + 2], "big")
118
+ payload = data[pos + 2 : pos + segment_length]
119
+ entry: dict[str, Any] = {
120
+ "name": names.get(code, f"0xFF{code:02X}"),
121
+ "offset": marker_offset,
122
+ "length": segment_length + 2,
123
+ }
124
+ if code in (0xE0, 0xE1, 0xE2, 0xED, 0xEE, 0xFE):
125
+ entry["identifier"] = (
126
+ payload[:80].split(b"\x00", 1)[0].decode("latin-1", "replace")
127
+ )
128
+ markers.append(entry)
129
+ pos += segment_length
130
+ if code == 0xDA:
131
+ eoi = data.rfind(b"\xff\xd9")
132
+ if eoi >= 0:
133
+ markers.append({"name": "EOI", "offset": eoi, "length": 2})
134
+ break
135
+ eoi_offset = data.rfind(b"\xff\xd9")
136
+ return {
137
+ "valid_soi": True,
138
+ "markers": markers,
139
+ "eoi_offset": eoi_offset if eoi_offset >= 0 else None,
140
+ "bytes_after_eoi": len(data) - eoi_offset - 2 if eoi_offset >= 0 else None,
141
+ "has_exif_app1": b"Exif\x00\x00" in data,
142
+ "has_xmp": b"http://ns.adobe.com/xap/1.0/" in data,
143
+ "has_photoshop_app13": b"Photoshop 3.0" in data or b"8BIM" in data,
144
+ "has_adobe_app14": any(m["name"] == "APP14" for m in markers),
145
+ }
146
+
147
+
148
+ def estimate_jpeg_quality(image: Image.Image) -> int | None:
149
+ source = getattr(image, "quantization", None)
150
+ if not source or 0 not in source:
151
+ return None
152
+ source_luma = source[0]
153
+ probe = Image.new("RGB", (8, 8), "gray")
154
+ best: tuple[int, int] | None = None
155
+ for quality in range(30, 101):
156
+ buf = io.BytesIO()
157
+ probe.save(buf, format="JPEG", quality=quality)
158
+ buf.seek(0)
159
+ with Image.open(buf) as candidate:
160
+ table = candidate.quantization.get(0, [])
161
+ if len(table) != len(source_luma):
162
+ continue
163
+ distance = sum(abs(a - b) for a, b in zip(source_luma, table))
164
+ if best is None or distance < best[0]:
165
+ best = (distance, quality)
166
+ return best[1] if best else None
167
+
168
+
169
+ def recompression_analysis(image: Image.Image, outdir: Path) -> dict[str, Any]:
170
+ quality = estimate_jpeg_quality(image) or 90
171
+ rgb = image.convert("RGB")
172
+ if max(rgb.size) > 5000:
173
+ rgb.thumbnail((5000, 5000))
174
+ buf = io.BytesIO()
175
+ rgb.save(buf, format="JPEG", quality=quality)
176
+ buf.seek(0)
177
+ with Image.open(buf) as recompressed:
178
+ diff = ImageChops.difference(rgb, recompressed.convert("RGB"))
179
+ gray = diff.convert("L")
180
+ global_mean = ImageStat.Stat(gray).mean[0]
181
+ tile_size = max(32, min(rgb.size) // 20)
182
+ tiles: list[dict[str, Any]] = []
183
+ for top in range(0, rgb.height, tile_size):
184
+ for left in range(0, rgb.width, tile_size):
185
+ box = (
186
+ left,
187
+ top,
188
+ min(left + tile_size, rgb.width),
189
+ min(top + tile_size, rgb.height),
190
+ )
191
+ mean = ImageStat.Stat(gray.crop(box)).mean[0]
192
+ tiles.append({"box": list(box), "mean_error": round(mean, 4)})
193
+ values = sorted(t["mean_error"] for t in tiles)
194
+ p95 = values[min(len(values) - 1, int(len(values) * 0.95))] if values else 0
195
+ max_tile = max(tiles, key=lambda t: t["mean_error"], default=None)
196
+ amplified = ImageEnhance.Contrast(diff).enhance(8)
197
+ amplified = ImageEnhance.Brightness(amplified).enhance(8)
198
+ amplified.save(outdir / "ela.png")
199
+ return {
200
+ "method": "same-quality JPEG recompression difference",
201
+ "estimated_quality": quality,
202
+ "analysis_size": list(rgb.size),
203
+ "tile_size": tile_size,
204
+ "global_mean_error": round(global_mean, 4),
205
+ "p95_tile_error": p95,
206
+ "max_tile": max_tile,
207
+ "highest_error_tiles": sorted(
208
+ tiles, key=lambda t: t["mean_error"], reverse=True
209
+ )[:10],
210
+ "ela_path": str(outdir / "ela.png"),
211
+ "caution": "ELA is heuristic; naturally detailed regions differ from flat regions, and a full-image re-export can hide local edits.",
212
+ }
213
+
214
+
215
+ def main() -> int:
216
+ parser = argparse.ArgumentParser()
217
+ parser.add_argument("input", type=Path)
218
+ parser.add_argument("--out", required=True, type=Path)
219
+ args = parser.parse_args()
220
+ args.out.mkdir(parents=True, exist_ok=True)
221
+
222
+ data = args.input.read_bytes()
223
+ with Image.open(args.input) as image:
224
+ image.verify()
225
+ with Image.open(args.input) as image:
226
+ exif = extract_exif(image)
227
+ info = {
228
+ str(k): serializable(v)
229
+ for k, v in image.info.items()
230
+ if k not in {"exif", "icc_profile"}
231
+ }
232
+ text_blob = (
233
+ data + json.dumps({"exif": exif, "info": info}, ensure_ascii=False).encode()
234
+ )
235
+ editor_matches = [
236
+ name
237
+ for name, pattern in EDITOR_PATTERNS.items()
238
+ if re.search(pattern, text_blob)
239
+ ]
240
+ capture_matches = [
241
+ name
242
+ for name, pattern in CAPTURE_PATTERNS.items()
243
+ if re.search(pattern, text_blob)
244
+ ]
245
+ result: dict[str, Any] = {
246
+ "input": str(args.input.resolve()),
247
+ "file_size": len(data),
248
+ "size_bytes": len(data),
249
+ "sha256": hashlib.sha256(data).hexdigest(),
250
+ "media_type": Image.MIME.get(image.format, "application/octet-stream"),
251
+ "extension_magic_match": args.input.suffix.lower()
252
+ in {
253
+ suffix.lower()
254
+ for suffix, detected_format in Image.registered_extensions().items()
255
+ if detected_format == image.format
256
+ },
257
+ "format": image.format,
258
+ "mode": image.mode,
259
+ "width": image.width,
260
+ "height": image.height,
261
+ "frames": getattr(image, "n_frames", 1),
262
+ "exif": exif,
263
+ "image_info": info,
264
+ "editor_matches": editor_matches,
265
+ "capture_device_matches": capture_matches,
266
+ "provenance": {
267
+ "marker_scan_only": True,
268
+ "cryptographic_validation": "not_performed",
269
+ "has_c2pa_or_jumbf_marker": any(
270
+ token in data.lower()
271
+ for token in (b"c2pa", b"jumb", b"contentauth")
272
+ ),
273
+ "has_content_credentials_text": b"content credentials" in data.lower(),
274
+ },
275
+ }
276
+ if image.format == "JPEG":
277
+ result["jpeg"] = jpeg_markers(data)
278
+ result["recompression"] = recompression_analysis(image, args.out)
279
+ else:
280
+ result["recompression"] = {
281
+ "available": False,
282
+ "reason": "JPEG ELA is not applicable to this source format; inspect metadata and pixels directly.",
283
+ }
284
+
285
+ output = args.out / "image-forensics.json"
286
+ output.write_text(
287
+ json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
288
+ )
289
+ print(output)
290
+ return 0
291
+
292
+
293
+ if __name__ == "__main__":
294
+ raise SystemExit(main())
@@ -0,0 +1,401 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = ["pikepdf>=8", "pdfplumber>=0.11"]
4
+ # ///
5
+ """Extract forensic signals from a PDF for the doc-authenticity skill.
6
+
7
+ Usage:
8
+ uv run pdf_forensics.py input.pdf --out outdir/
9
+
10
+ Writes:
11
+ outdir/forensics.json structural + metadata signals (F1-F9 inputs)
12
+ outdir/pages/page-N.txt extracted text per page
13
+ outdir/pages/page-N.fonts.tsv word-level font map (word, font, size, x0, top)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import hashlib
20
+ import json
21
+ import re
22
+ import sys
23
+ from collections import Counter
24
+ from pathlib import Path
25
+
26
+ import pdfplumber
27
+ import pikepdf
28
+
29
+
30
+ def parse_pdf_date(raw: str | None) -> str | None:
31
+ """D:20260115093000+00'00' -> 2026-01-15T09:30:00+00:00 (best effort)."""
32
+ if not raw:
33
+ return None
34
+ s = str(raw)
35
+ m = re.match(r"D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?([Zz+\-].*)?", s)
36
+ if not m:
37
+ return s
38
+ y, mo, d, h, mi, sec, tz = m.groups()
39
+ iso = f"{y}-{mo or '01'}-{d or '01'}T{h or '00'}:{mi or '00'}:{sec or '00'}"
40
+ if tz and tz not in ("Z", "z"):
41
+ tz = tz.replace("'", ":").rstrip(":")
42
+ iso += tz if re.match(r"[+\-]\d{2}:\d{2}$", tz) else ""
43
+ elif tz:
44
+ iso += "+00:00"
45
+ return iso
46
+
47
+
48
+ def scan_raw_structure(data: bytes) -> dict:
49
+ """Byte-level scan: save generations and objects redefined across them."""
50
+ eof_positions = [m.start() for m in re.finditer(rb"%%EOF", data)]
51
+ startxrefs = len(re.findall(rb"startxref", data))
52
+ prev_pointers = len(re.findall(rb"/Prev\s+\d+", data))
53
+
54
+ # Objects defined more than once = redefined in a later generation
55
+ # (approximation: also matched inside object streams is not possible since
56
+ # those are compressed; raw matches are top-level definitions).
57
+ defs: dict[int, list[int]] = {}
58
+ for m in re.finditer(rb"(?m)^\s*(\d+)\s+\d+\s+obj\b", data):
59
+ defs.setdefault(int(m.group(1)), []).append(m.start())
60
+ updated = sorted(n for n, offs in defs.items() if len(offs) > 1)
61
+
62
+ trailing = data[eof_positions[-1] + 5 :] if eof_positions else b""
63
+ return {
64
+ "file_size": len(data),
65
+ "eof_count": len(eof_positions),
66
+ "startxref_count": startxrefs,
67
+ "prev_pointers": prev_pointers,
68
+ "incremental_updates": max(0, len(eof_positions) - 1),
69
+ "updated_objects": updated[:50],
70
+ "updated_object_count": len(updated),
71
+ "bytes_after_last_eof": len(trailing.strip()),
72
+ "header": data[:16].decode("latin-1", "replace").strip(),
73
+ }
74
+
75
+
76
+ def extract_metadata(pdf: pikepdf.Pdf) -> dict:
77
+ info = {}
78
+ try:
79
+ for k, v in (pdf.docinfo or {}).items():
80
+ info[str(k).lstrip("/")] = str(v)
81
+ except Exception as e: # noqa: BLE001
82
+ info["_error"] = str(e)
83
+ return {
84
+ "producer": info.get("Producer"),
85
+ "creator": info.get("Creator"),
86
+ "author": info.get("Author"),
87
+ "title": info.get("Title"),
88
+ "creation_date_raw": info.get("CreationDate"),
89
+ "mod_date_raw": info.get("ModDate"),
90
+ "creation_date": parse_pdf_date(info.get("CreationDate")),
91
+ "mod_date": parse_pdf_date(info.get("ModDate")),
92
+ "all_docinfo": info,
93
+ }
94
+
95
+
96
+ def extract_xmp(pdf: pikepdf.Pdf) -> dict:
97
+ out: dict = {"present": False}
98
+ try:
99
+ meta = pdf.open_metadata()
100
+ if not str(meta):
101
+ return out
102
+ out["present"] = True
103
+ keys = [
104
+ ("xmp:CreatorTool", "creator_tool"),
105
+ ("pdf:Producer", "producer"),
106
+ ("xmp:CreateDate", "create_date"),
107
+ ("xmp:ModifyDate", "modify_date"),
108
+ ("xmp:MetadataDate", "metadata_date"),
109
+ ("xmpMM:DocumentID", "document_id"),
110
+ ("xmpMM:InstanceID", "instance_id"),
111
+ ]
112
+ for xmp_key, name in keys:
113
+ try:
114
+ if xmp_key in meta:
115
+ out[name] = str(meta[xmp_key])
116
+ except Exception: # noqa: BLE001, S110
117
+ pass
118
+ raw = str(meta)
119
+ out["xmp_toolkit"] = None
120
+ tk = re.search(r'x:xmptk="([^"]+)"', raw)
121
+ if tk:
122
+ out["xmp_toolkit"] = tk.group(1)
123
+ out["has_history"] = "xmpMM:History" in raw
124
+ out["has_derived_from"] = "xmpMM:DerivedFrom" in raw
125
+ agents = sorted(set(re.findall(r"softwareAgent>([^<]+)<", raw)))
126
+ agents += sorted(set(re.findall(r'softwareAgent="([^"]+)"', raw)))
127
+ out["history_software_agents"] = sorted(set(agents))
128
+ except Exception as e: # noqa: BLE001
129
+ out["_error"] = str(e)
130
+ return out
131
+
132
+
133
+ def extract_signatures(pdf: pikepdf.Pdf, file_size: int) -> list[dict]:
134
+ sigs = []
135
+ try:
136
+ root = pdf.Root
137
+ acro = root.get("/AcroForm")
138
+ if acro is None:
139
+ return sigs
140
+ for field in acro.get("/Fields", []):
141
+ try:
142
+ if str(field.get("/FT", "")) != "/Sig":
143
+ continue
144
+ v = field.get("/V")
145
+ entry: dict = {"field_name": str(field.get("/T", ""))}
146
+ if v is None:
147
+ entry["signed"] = False
148
+ sigs.append(entry)
149
+ continue
150
+ entry["signed"] = True
151
+ entry["cryptographic_validation"] = "not_performed"
152
+ entry["sub_filter"] = str(v.get("/SubFilter", "")) or None
153
+ entry["signer_name"] = str(v.get("/Name", "")) or None
154
+ entry["sign_date"] = parse_pdf_date(
155
+ str(v.get("/M")) if v.get("/M") is not None else None
156
+ )
157
+ br = v.get("/ByteRange")
158
+ if br is not None:
159
+ br = [int(x) for x in br]
160
+ entry["byte_range"] = br
161
+ # This checks structural coverage only; it does not verify
162
+ # the signature value, certificate chain, or trust anchor.
163
+ entry["byte_range_covers_file"] = (
164
+ len(br) == 4
165
+ and br[0] == 0
166
+ and br[1] >= 0
167
+ and br[2] >= br[1]
168
+ and br[3] >= 0
169
+ and br[2] + br[3] >= file_size - 4
170
+ )
171
+ sigs.append(entry)
172
+ except Exception as e: # noqa: BLE001
173
+ sigs.append({"_error": str(e)})
174
+ except Exception as e: # noqa: BLE001
175
+ sigs.append({"_error": str(e)})
176
+ return sigs
177
+
178
+
179
+ def extract_fonts_and_annots(pdf: pikepdf.Pdf) -> tuple[list[dict], list[dict], dict]:
180
+ fonts: dict[tuple, dict] = {}
181
+ annots: list[dict] = []
182
+ doc_flags = {
183
+ "has_javascript": False,
184
+ "has_embedded_files": False,
185
+ "has_open_action": False,
186
+ }
187
+ try:
188
+ names = pdf.Root.get("/Names")
189
+ if names is not None:
190
+ if names.get("/JavaScript") is not None:
191
+ doc_flags["has_javascript"] = True
192
+ if names.get("/EmbeddedFiles") is not None:
193
+ doc_flags["has_embedded_files"] = True
194
+ if pdf.Root.get("/OpenAction") is not None:
195
+ doc_flags["has_open_action"] = True
196
+ except Exception: # noqa: BLE001, S110
197
+ pass
198
+
199
+ for pageno, page in enumerate(pdf.pages, 1):
200
+ try:
201
+ res = page.get("/Resources", {})
202
+ fdict = res.get("/Font", {}) if res is not None else {}
203
+ for _, fobj in fdict.items() if fdict is not None else []:
204
+ try:
205
+ base = str(fobj.get("/BaseFont", "unknown")).lstrip("/")
206
+ desc = fobj.get("/FontDescriptor")
207
+ if desc is None and fobj.get("/DescendantFonts") is not None:
208
+ desc = fobj.get("/DescendantFonts")[0].get("/FontDescriptor")
209
+ embedded = bool(
210
+ desc is not None
211
+ and any(
212
+ desc.get(k) is not None
213
+ for k in ("/FontFile", "/FontFile2", "/FontFile3")
214
+ )
215
+ )
216
+ m = re.match(r"^([A-Z]{6})\+(.+)$", base)
217
+ key = (base, embedded)
218
+ rec = fonts.setdefault(
219
+ key,
220
+ {
221
+ "base_font": base,
222
+ "family": m.group(2) if m else base,
223
+ "subset_prefix": m.group(1) if m else None,
224
+ "embedded": embedded,
225
+ "subtype": str(fobj.get("/Subtype", "")).lstrip("/"),
226
+ "pages": [],
227
+ },
228
+ )
229
+ if pageno not in rec["pages"]:
230
+ rec["pages"].append(pageno)
231
+ except Exception: # noqa: BLE001, S112
232
+ continue
233
+ except Exception: # noqa: BLE001, S110
234
+ pass
235
+ try:
236
+ for a in page.get("/Annots", []) or []:
237
+ sub = str(a.get("/Subtype", "")).lstrip("/")
238
+ annots.append(
239
+ {
240
+ "page": pageno,
241
+ "subtype": sub,
242
+ "contents": str(a.get("/Contents", ""))[:200] or None,
243
+ "rect": [float(x) for x in a.get("/Rect", [])] or None,
244
+ }
245
+ )
246
+ except Exception: # noqa: BLE001, S110
247
+ pass
248
+
249
+ font_list = sorted(fonts.values(), key=lambda f: f["base_font"])
250
+ # duplicate subset embeddings of the same family = edit trace (F5)
251
+ fam_prefixes: dict[str, set] = {}
252
+ for f in font_list:
253
+ if f["subset_prefix"]:
254
+ fam_prefixes.setdefault(f["family"], set()).add(f["subset_prefix"])
255
+ doc_flags["families_with_multiple_subsets"] = sorted(
256
+ fam for fam, p in fam_prefixes.items() if len(p) > 1
257
+ )
258
+ return font_list, annots, doc_flags
259
+
260
+
261
+ def analyze_text(pdf_path: Path, pages_dir: Path) -> tuple[list[dict], list[dict]]:
262
+ """Per-page text + font map; returns (page_stats, font_anomalies)."""
263
+ pages_dir.mkdir(parents=True, exist_ok=True)
264
+ page_stats: list[dict] = []
265
+ anomalies: list[dict] = []
266
+ with pdfplumber.open(pdf_path) as pdf:
267
+ for i, page in enumerate(pdf.pages, 1):
268
+ text = page.extract_text() or ""
269
+ (pages_dir / f"page-{i}.txt").write_text(text, encoding="utf-8")
270
+ words = page.extract_words(extra_attrs=["fontname", "size"]) or []
271
+ with (pages_dir / f"page-{i}.fonts.tsv").open("w", encoding="utf-8") as fh:
272
+ fh.write("word\tfont\tsize\tx0\ttop\n")
273
+ for w in words:
274
+ fh.write(
275
+ f"{w['text']}\t{w.get('fontname')}\t"
276
+ f"{round(float(w.get('size', 0)), 2)}\t"
277
+ f"{round(w['x0'], 1)}\t{round(w['top'], 1)}\n"
278
+ )
279
+ page_area = float(page.width) * float(page.height) or 1.0
280
+ img_area = sum(
281
+ max(0.0, (im["x1"] - im["x0"]) * (im["bottom"] - im["top"]))
282
+ for im in page.images
283
+ )
284
+ page_stats.append(
285
+ {
286
+ "page": i,
287
+ "char_count": len(text),
288
+ "word_count": len(words),
289
+ "image_count": len(page.images),
290
+ "image_coverage": round(min(1.0, img_area / page_area), 3),
291
+ }
292
+ )
293
+ # group words into lines by 'top', find words whose font deviates
294
+ # from the line's modal font (F6 splice candidates)
295
+ lines: dict[int, list[dict]] = {}
296
+ for w in words:
297
+ lines.setdefault(round(w["top"] / 3.0), []).append(w)
298
+ for line_words in lines.values():
299
+ if len(line_words) < 3:
300
+ continue
301
+ fnames = Counter(w.get("fontname") for w in line_words)
302
+ modal, modal_n = fnames.most_common(1)[0]
303
+ if len(fnames) == 1 or modal_n < max(3, len(line_words) - 2):
304
+ continue
305
+ for w in line_words:
306
+ if w.get("fontname") != modal:
307
+ anomalies.append(
308
+ {
309
+ "page": i,
310
+ "word": w["text"],
311
+ "font": w.get("fontname"),
312
+ "line_modal_font": modal,
313
+ "size": round(float(w.get("size", 0)), 2),
314
+ "x0": round(w["x0"], 1),
315
+ "top": round(w["top"], 1),
316
+ "line_text": " ".join(
317
+ x["text"]
318
+ for x in sorted(line_words, key=lambda x: x["x0"])
319
+ )[:200],
320
+ }
321
+ )
322
+ return page_stats, anomalies
323
+
324
+
325
+ def main() -> int:
326
+ ap = argparse.ArgumentParser(description=__doc__)
327
+ ap.add_argument("pdf", type=Path)
328
+ ap.add_argument(
329
+ "--out",
330
+ type=Path,
331
+ default=None,
332
+ help="output dir (default: ./doc-authenticity-out/<stem>)",
333
+ )
334
+ args = ap.parse_args()
335
+
336
+ if not args.pdf.is_file():
337
+ print(f"error: {args.pdf} not found", file=sys.stderr)
338
+ return 2
339
+ outdir = args.out or Path("doc-authenticity-out") / args.pdf.stem
340
+ outdir.mkdir(parents=True, exist_ok=True)
341
+
342
+ data = args.pdf.read_bytes()
343
+ result: dict = {
344
+ "input": str(args.pdf.resolve()),
345
+ "media_type": "application/pdf",
346
+ "size_bytes": len(data),
347
+ "sha256": hashlib.sha256(data).hexdigest(),
348
+ "extension_magic_match": args.pdf.suffix.lower() == ".pdf",
349
+ "structure": scan_raw_structure(data),
350
+ }
351
+
352
+ repairs: list[str] = []
353
+ try:
354
+ import warnings
355
+
356
+ with warnings.catch_warnings(record=True) as caught:
357
+ warnings.simplefilter("always")
358
+ pdf = pikepdf.open(args.pdf)
359
+ repairs = [str(w.message) for w in caught]
360
+ except pikepdf.PasswordError:
361
+ result["error"] = "encrypted: password required"
362
+ (outdir / "forensics.json").write_text(json.dumps(result, indent=2))
363
+ print(json.dumps(result, indent=2))
364
+ return 1
365
+ except Exception as exc: # noqa: BLE001
366
+ result["error"] = f"PDF parsing failed: {exc}"
367
+ (outdir / "forensics.json").write_text(json.dumps(result, indent=2))
368
+ print(json.dumps(result, indent=2))
369
+ return 1
370
+
371
+ with pdf:
372
+ result["pdf_version"] = str(pdf.pdf_version)
373
+ result["encrypted"] = pdf.is_encrypted
374
+ result["linearized"] = pdf.is_linearized
375
+ result["page_count"] = len(pdf.pages)
376
+ result["metadata"] = extract_metadata(pdf)
377
+ result["xmp"] = extract_xmp(pdf)
378
+ result["signatures"] = extract_signatures(pdf, result["structure"]["file_size"])
379
+ fonts, annots, doc_flags = extract_fonts_and_annots(pdf)
380
+ result["fonts"] = fonts
381
+ result["annotations"] = annots
382
+ result["structure"].update(doc_flags)
383
+ result["structure"]["repairs"] = repairs
384
+
385
+ try:
386
+ page_stats, anomalies = analyze_text(args.pdf, outdir / "pages")
387
+ result["pages"] = page_stats
388
+ result["text_font_anomalies"] = anomalies
389
+ except Exception as e: # noqa: BLE001
390
+ result["pages_error"] = str(e)
391
+
392
+ out_path = outdir / "forensics.json"
393
+ out_path.write_text(json.dumps(result, indent=2), encoding="utf-8")
394
+ print(json.dumps(result, indent=2))
395
+ print(f"\n[written] {out_path}", file=sys.stderr)
396
+ print(f"[written] {outdir}/pages/page-N.txt + .fonts.tsv", file=sys.stderr)
397
+ return 0
398
+
399
+
400
+ if __name__ == "__main__":
401
+ sys.exit(main())