@1aboveio/skills 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -2
- package/bin/cli.mjs +14 -0
- package/bin/select-core.mjs +172 -0
- package/package.json +146 -4
- package/skills/app-dev/airflow/LICENSE +3 -0
- package/skills/app-dev/airflow/SKILL.md +199 -0
- package/skills/app-dev/airflow/release-check.mjs +93 -0
- package/skills/app-dev/android-foundation/LICENSE +3 -0
- package/skills/app-dev/android-foundation/SKILL.md +192 -0
- package/skills/app-dev/android-foundation/assets/android-ci-job.yml +217 -0
- package/skills/app-dev/android-foundation/references/ci.md +87 -0
- package/skills/app-dev/android-foundation/references/gotchas.md +221 -0
- package/skills/app-dev/android-foundation/references/setup.md +164 -0
- package/skills/app-dev/android-foundation/release-check.mjs +93 -0
- package/skills/app-dev/android-foundation/scripts/device-run.sh +176 -0
- package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +172 -0
- package/skills/app-dev/app-debug/LICENSE +3 -0
- package/skills/app-dev/app-debug/SKILL.md +110 -0
- package/skills/app-dev/app-debug/references/common-errors.md +128 -0
- package/skills/app-dev/app-debug/release-check.mjs +93 -0
- package/skills/app-dev/better-auth/LICENSE +3 -0
- package/skills/app-dev/better-auth/SKILL.md +598 -0
- package/skills/app-dev/better-auth/references/feishu-api.md +270 -0
- package/skills/app-dev/better-auth/release-check.mjs +93 -0
- package/skills/app-dev/data-catalog/LICENSE +3 -0
- package/skills/app-dev/data-catalog/SKILL.md +204 -0
- package/skills/app-dev/data-catalog/agents/openai.yaml +3 -0
- package/skills/app-dev/data-catalog/references/data-catalog-conventions.md +75 -0
- package/skills/app-dev/data-catalog/references/field-definitions.md +116 -0
- package/skills/app-dev/data-catalog/references/format.md +121 -0
- package/skills/app-dev/data-catalog/release-check.mjs +93 -0
- package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +36 -0
- package/skills/app-dev/data-catalog/scripts/data_catalog.py +1392 -0
- package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +117 -0
- package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +17 -0
- package/skills/app-dev/data-catalog/scripts/test_data_catalog.py +571 -0
- package/skills/app-dev/ios-foundation/LICENSE +3 -0
- package/skills/app-dev/ios-foundation/SKILL.md +184 -0
- package/skills/app-dev/ios-foundation/references/setup.md +243 -0
- package/skills/app-dev/ios-foundation/release-check.mjs +93 -0
- package/skills/app-dev/monorepo/LICENSE +3 -0
- package/skills/app-dev/monorepo/SKILL.md +462 -0
- package/skills/app-dev/monorepo/release-check.mjs +93 -0
- package/skills/app-dev/nextjs-fullstack/LICENSE +3 -0
- package/skills/app-dev/nextjs-fullstack/SKILL.md +211 -0
- package/skills/app-dev/nextjs-fullstack/conventions.md +318 -0
- package/skills/app-dev/nextjs-fullstack/frontend-conventions.md +61 -0
- package/skills/app-dev/nextjs-fullstack/nextjs16.md +287 -0
- package/skills/app-dev/nextjs-fullstack/release-check.mjs +93 -0
- package/skills/app-dev/nextjs-fullstack/server-actions.md +409 -0
- package/skills/app-dev/prisma-setup/LICENSE +3 -0
- package/skills/app-dev/prisma-setup/SKILL.md +181 -0
- package/skills/app-dev/prisma-setup/nextjs.md +258 -0
- package/skills/app-dev/prisma-setup/release-check.mjs +93 -0
- package/skills/app-dev/prisma-setup/turborepo.md +301 -0
- package/skills/app-dev/pyspark/LICENSE +3 -0
- package/skills/app-dev/pyspark/SKILL.md +127 -0
- package/skills/app-dev/pyspark/assets/templates/etl.py +748 -0
- package/skills/app-dev/pyspark/assets/templates/utils/__init__.py +1 -0
- package/skills/app-dev/pyspark/assets/templates/utils/hudi_metadata.py +14 -0
- package/skills/app-dev/pyspark/references/diagnosis-and-profiling.md +169 -0
- package/skills/app-dev/pyspark/references/etl-contract.md +129 -0
- package/skills/app-dev/pyspark/references/production-validation.md +131 -0
- package/skills/app-dev/pyspark/references/reconciliation.md +38 -0
- package/skills/app-dev/pyspark/references/transformation-design.md +190 -0
- package/skills/app-dev/pyspark/references/velocity-feature-calculation.md +193 -0
- package/skills/app-dev/pyspark/release-check.mjs +93 -0
- package/skills/app-dev/pyspark/scripts/spark_eventlog_summary.py +223 -0
- package/skills/app-dev/python-backend/LICENSE +3 -0
- package/skills/app-dev/python-backend/SKILL.md +325 -0
- package/skills/app-dev/python-backend/assets/uv-install.sh +48 -0
- package/skills/app-dev/python-backend/references/uv-installs.md +83 -0
- package/skills/app-dev/python-backend/release-check.mjs +93 -0
- package/skills/app-dev/shadcn/LICENSE +3 -0
- package/skills/app-dev/shadcn/SKILL.md +120 -0
- package/skills/app-dev/shadcn/assets/shadcn-small.png +0 -0
- package/skills/app-dev/shadcn/assets/shadcn.png +0 -0
- package/skills/app-dev/shadcn/cli.md +411 -0
- package/skills/app-dev/shadcn/customization.md +224 -0
- package/skills/app-dev/shadcn/mcp.md +101 -0
- package/skills/app-dev/shadcn/release-check.mjs +93 -0
- package/skills/app-dev/shadcn/rules/base-vs-radix.md +323 -0
- package/skills/app-dev/shadcn/rules/component-selection.md +67 -0
- package/skills/app-dev/shadcn/rules/composition.md +195 -0
- package/skills/app-dev/shadcn/rules/data-table.md +201 -0
- package/skills/app-dev/shadcn/rules/forms.md +255 -0
- package/skills/app-dev/shadcn/rules/icons.md +103 -0
- package/skills/app-dev/shadcn/rules/styling.md +167 -0
- package/skills/app-dev/zod-v4/LICENSE +3 -0
- package/skills/app-dev/zod-v4/SKILL.md +286 -0
- package/skills/app-dev/zod-v4/release-check.mjs +93 -0
- package/skills/cicd/cloud-build/LICENSE +3 -0
- package/skills/cicd/cloud-build/SKILL.md +707 -0
- package/skills/cicd/cloud-build/release-check.mjs +93 -0
- package/skills/cicd/cloud-debug/LICENSE +3 -0
- package/skills/cicd/cloud-debug/SKILL.md +315 -0
- package/skills/cicd/cloud-debug/references/build-failures.md +238 -0
- package/skills/cicd/cloud-debug/references/deploy-failures.md +376 -0
- package/skills/cicd/cloud-debug/references/pipeline-failures.md +378 -0
- package/skills/cicd/cloud-debug/release-check.mjs +93 -0
- package/skills/cicd/cloud-deploy/LICENSE +3 -0
- package/skills/cicd/cloud-deploy/SKILL.md +230 -0
- package/skills/cicd/cloud-deploy/references/config-templates.md +257 -0
- package/skills/cicd/cloud-deploy/release-check.mjs +93 -0
- package/skills/cicd/docker/LICENSE +3 -0
- package/skills/cicd/docker/SKILL.md +151 -0
- package/skills/cicd/docker/release-check.mjs +93 -0
- package/skills/cicd/github-actions/LICENSE +3 -0
- package/skills/cicd/github-actions/SKILL.md +111 -0
- package/skills/cicd/github-actions/references/audit-profile-optimize.md +150 -0
- package/skills/cicd/github-actions/references/ci-case-studies.md +141 -0
- package/skills/cicd/github-actions/references/conditional-ci.md +214 -0
- package/skills/cicd/github-actions/references/debugging.md +13 -0
- package/skills/cicd/github-actions/references/pipeline.md +87 -0
- package/skills/cicd/github-actions/references/post-deploy-and-publishing.md +33 -0
- package/skills/cicd/github-actions/references/runners.md +253 -0
- package/skills/cicd/github-actions/references/security.md +29 -0
- package/skills/cicd/github-actions/references/speed-and-audit.md +115 -0
- package/skills/cicd/github-actions/references/two-pass-reading.md +92 -0
- package/skills/cicd/github-actions/release-check.mjs +93 -0
- package/skills/cicd/github-actions/scripts/ci-run-audit-core.mjs +612 -0
- package/skills/cicd/github-actions/scripts/ci-run-audit.mjs +4 -0
- package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +637 -0
- package/skills/cicd/github-flow/LICENSE +3 -0
- package/skills/cicd/github-flow/SKILL.md +123 -0
- package/skills/cicd/github-flow/references/ci-conventions.md +14 -0
- package/skills/cicd/github-flow/release-check.mjs +93 -0
- package/skills/cicd/github-flow/scripts/flow-core.mjs +105 -0
- package/skills/cicd/github-flow/scripts/flow.mjs +81 -0
- package/skills/cicd/github-flow/scripts/flow.test.mjs +114 -0
- package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +117 -0
- package/skills/cicd/github-flow/scripts/persist-conventions.mjs +49 -0
- package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +108 -0
- package/skills/cicd/google-cloud/LICENSE +3 -0
- package/skills/cicd/google-cloud/SKILL.md +119 -0
- package/skills/cicd/google-cloud/references/gcs.md +469 -0
- package/skills/cicd/google-cloud/references/iam.md +451 -0
- package/skills/cicd/google-cloud/references/project.md +349 -0
- package/skills/cicd/google-cloud/references/secrets.md +336 -0
- package/skills/cicd/google-cloud/references/vpc.md +312 -0
- package/skills/cicd/google-cloud/release-check.mjs +93 -0
- package/skills/cicd/google-cloud/scripts/create-sa.sh +36 -0
- package/skills/cicd/google-cloud/scripts/gcp-config.sh +31 -0
- package/skills/cicd/google-cloud/scripts/grant-iap.sh +41 -0
- package/skills/cicd/google-cloud/scripts/setup-secrets.sh +48 -0
- package/skills/cicd/mergify/LICENSE +3 -0
- package/skills/cicd/mergify/SKILL.md +102 -0
- package/skills/cicd/mergify/assets/templates/mergify.yml +215 -0
- package/skills/cicd/mergify/assets/templates/ruleset.json +46 -0
- package/skills/cicd/mergify/references/branch-protection.md +282 -0
- package/skills/cicd/mergify/references/configuration.md +274 -0
- package/skills/cicd/mergify/references/diagnosis.md +109 -0
- package/skills/cicd/mergify/references/stacks.md +84 -0
- package/skills/cicd/mergify/references/traps.md +119 -0
- package/skills/cicd/mergify/references/watch-contract.md +274 -0
- package/skills/cicd/mergify/release-check.mjs +93 -0
- package/skills/cicd/mergify/scripts/audit-core.mjs +208 -0
- package/skills/cicd/mergify/scripts/audit.mjs +4 -0
- package/skills/cicd/mergify/scripts/audit.test.mjs +201 -0
- package/skills/cicd/mergify/scripts/entrypoints.test.mjs +106 -0
- package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +30 -0
- package/skills/cicd/mergify/scripts/stack-push-core.mjs +258 -0
- package/skills/cicd/mergify/scripts/stack-push.mjs +4 -0
- package/skills/cicd/mergify/scripts/stack-push.test.mjs +233 -0
- package/skills/cicd/mergify/scripts/watch-pr-delivery-core.mjs +1065 -0
- package/skills/cicd/mergify/scripts/watch-pr-delivery.mjs +4 -0
- package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +1826 -0
- package/skills/cicd/podman/LICENSE +3 -0
- package/skills/cicd/podman/SKILL.md +71 -0
- package/skills/cicd/podman/agents/openai.yaml +4 -0
- package/skills/cicd/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
- package/skills/cicd/podman/assets/templates/podman-service-override.conf +3 -0
- package/skills/cicd/podman/references/compose-compatibility.md +70 -0
- package/skills/cicd/podman/references/networking-and-ports.md +74 -0
- package/skills/cicd/podman/references/rootless-services-and-sockets.md +156 -0
- package/skills/cicd/podman/references/troubleshooting.md +98 -0
- package/skills/cicd/podman/release-check.mjs +93 -0
- package/skills/content/article-review/LICENSE +3 -0
- package/skills/content/article-review/SKILL.md +66 -0
- package/skills/content/article-review/references/report-format.md +68 -0
- package/skills/content/article-review/references/template-guide.md +79 -0
- package/skills/content/article-review/references/template-wechat-payments.md +125 -0
- package/skills/content/article-review/release-check.mjs +93 -0
- package/skills/content/article-to-image/LICENSE +3 -0
- package/skills/content/article-to-image/SKILL.md +91 -0
- package/skills/content/article-to-image/references/formats.md +53 -0
- package/skills/content/article-to-image/release-check.mjs +93 -0
- package/skills/content/flowchart-svg/LICENSE +3 -0
- package/skills/content/flowchart-svg/SKILL.md +43 -0
- package/skills/content/flowchart-svg/release-check.mjs +93 -0
- package/skills/content/flowchart-svg/scripts/export_png.sh +8 -0
- package/skills/content/image-gen/LICENSE +3 -0
- package/skills/content/image-gen/SKILL.md +192 -0
- package/skills/content/image-gen/release-check.mjs +93 -0
- package/skills/content/image-gen/scripts/credentials.mjs +217 -0
- package/skills/content/image-gen/scripts/credentials.test.mjs +212 -0
- package/skills/content/image-gen/scripts/generate.mjs +240 -0
- package/skills/content/image-gen/scripts/options.mjs +293 -0
- package/skills/content/image-gen/scripts/options.test.mjs +245 -0
- package/skills/content/mermaid/LICENSE +3 -0
- package/skills/content/mermaid/SKILL.md +635 -0
- package/skills/content/mermaid/release-check.mjs +93 -0
- package/skills/content/wechat-article-writer/LICENSE +3 -0
- package/skills/content/wechat-article-writer/SKILL.md +92 -0
- package/skills/content/wechat-article-writer/references/style-selector.md +133 -0
- package/skills/content/wechat-article-writer/references/writing-rules.md +152 -0
- package/skills/content/wechat-article-writer/release-check.mjs +93 -0
- package/skills/content/wechat-article-writer/templates/wechat-comparison.md +52 -0
- package/skills/content/wechat-article-writer/templates/wechat-howto.md +51 -0
- package/skills/content/wechat-article-writer/templates/wechat-insight.md +50 -0
- package/skills/content/wechat-article-writer/templates/wechat-pitfall.md +52 -0
- package/skills/content/wechat-publisher/LICENSE +3 -0
- package/skills/content/wechat-publisher/SKILL.md +171 -0
- package/skills/content/wechat-publisher/references/api.md +105 -0
- package/skills/content/wechat-publisher/references/layout-rules.md +57 -0
- package/skills/content/wechat-publisher/references/wechat-safe-template.html +86 -0
- package/skills/content/wechat-publisher/release-check.mjs +93 -0
- package/skills/content/wechat-publisher/scripts/md_to_wechat_html.py +194 -0
- package/skills/content/wechat-publisher/scripts/publish.py +239 -0
- package/skills/content/wechat-search/LICENSE +3 -0
- package/skills/content/wechat-search/SKILL.md +211 -0
- package/skills/content/wechat-search/release-check.mjs +93 -0
- package/skills/content/wechat-search/scripts/start-chrome.sh +83 -0
- package/skills/content/xai-search/LICENSE +3 -0
- package/skills/content/xai-search/SKILL.md +79 -0
- package/skills/content/xai-search/release-check.mjs +93 -0
- package/skills/content/xai-search/scripts/xai_search.sh +187 -0
- package/skills/content/xai-search/tests/smoke_test.sh +36 -0
- package/skills/core/distill-writing/LICENSE +3 -0
- package/skills/core/distill-writing/SKILL.md +74 -0
- package/skills/core/distill-writing/references/extraction.md +95 -0
- package/skills/core/distill-writing/references/updating.md +46 -0
- package/skills/core/distill-writing/release-check.mjs +93 -0
- package/skills/core/doc-to-md/LICENSE +3 -0
- package/skills/core/doc-to-md/SKILL.md +78 -0
- package/skills/core/doc-to-md/release-check.mjs +93 -0
- package/skills/core/doc-to-md/scripts/convert.py +157 -0
- package/skills/core/editorial-review/LICENSE +3 -0
- package/skills/core/editorial-review/SKILL.md +79 -0
- package/skills/core/editorial-review/references/checklist.md +94 -0
- package/skills/core/editorial-review/release-check.mjs +93 -0
- package/skills/core/house-style/LICENSE +3 -0
- package/skills/core/house-style/SKILL.md +33 -0
- package/skills/core/house-style/references/defaults.md +66 -0
- package/skills/core/house-style/references/rules.md +267 -0
- package/skills/core/house-style/references/sources.md +27 -0
- package/skills/core/house-style/references/style-en.md +131 -0
- package/skills/core/house-style/references/style-zh.md +141 -0
- package/skills/core/house-style/references/terminology-zh.md +223 -0
- package/skills/core/house-style/release-check.mjs +93 -0
- package/skills/core/house-style/templates/brd.md +103 -0
- package/skills/core/house-style/templates/commercial-proposal.md +93 -0
- package/skills/core/house-style/templates/compliance-policy.md +109 -0
- package/skills/core/house-style/templates/gap-analysis.md +105 -0
- package/skills/core/house-style/templates/general.md +63 -0
- package/skills/core/house-style/templates/integration-guide.md +98 -0
- package/skills/core/house-style/templates/prd.md +81 -0
- package/skills/core/house-style/templates/risk-operations-procedure.md +94 -0
- package/skills/core/house-style/templates/technical-spec.md +96 -0
- package/skills/core/polish-cn/LICENSE +3 -0
- package/skills/core/polish-cn/SKILL.md +102 -0
- package/skills/core/polish-cn/references/brief.md +93 -0
- package/skills/core/polish-cn/references/models.md +76 -0
- package/skills/core/polish-cn/release-check.mjs +93 -0
- package/skills/core/polish-cn/scripts/polish-cn.test.mjs +244 -0
- package/skills/core/polish-cn/scripts/polish_blocks.py +489 -0
- package/skills/core/reg-parser/LICENSE +3 -0
- package/skills/core/reg-parser/SKILL.md +212 -0
- package/skills/core/reg-parser/references/checklist-schema.md +223 -0
- package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.json +1306 -0
- package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.md +469 -0
- package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.json +634 -0
- package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.md +233 -0
- package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.json +2194 -0
- package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.md +771 -0
- package/skills/core/reg-parser/references/checklists/kyc-aml-checklist.md +258 -0
- package/skills/core/reg-parser/references/checklists/visa-vars-checklist.md +1058 -0
- package/skills/core/reg-parser/references/mandates/hk-amlo-cap615.md +389 -0
- package/skills/core/reg-parser/references/mandates/visa-vars.md +205 -0
- package/skills/core/reg-parser/release-check.mjs +93 -0
- package/skills/core/reg-parser/scripts/compare_checklist.py +230 -0
- package/skills/core/reg-parser/scripts/dump_checklist.py +361 -0
- package/skills/core/reg-parser/scripts/preprocess-mandate.sh +187 -0
- package/skills/core/reg-parser/scripts/reg-parser.test.mjs +165 -0
- package/skills/core/reg-parser/scripts/test_quarantine.sh +105 -0
- package/skills/core/reg-parser/scripts/validate_checklist.py +563 -0
- package/skills/core/reg-parser/scripts/validate_mandate.py +273 -0
- package/skills/core/terminology/LICENSE +3 -0
- package/skills/core/terminology/SKILL.md +62 -0
- package/skills/core/terminology/references/format.md +50 -0
- package/skills/core/terminology/references/init.md +32 -0
- package/skills/core/terminology/release-check.mjs +93 -0
- package/skills/core/write-doc/LICENSE +3 -0
- package/skills/core/write-doc/SKILL.md +141 -0
- package/skills/core/write-doc/references/bilingual.md +60 -0
- package/skills/core/write-doc/release-check.mjs +93 -0
- package/skills/engineering/better-review/LICENSE +3 -0
- package/skills/engineering/better-review/SKILL.md +249 -0
- package/skills/engineering/better-review/agents/openai.yaml +9 -0
- package/skills/engineering/better-review/references/domain-hazards.md +53 -0
- package/skills/engineering/better-review/references/passes.md +147 -0
- package/skills/engineering/better-review/references/result.md +151 -0
- package/skills/engineering/better-review/release-check.mjs +93 -0
- package/skills/engineering/better-review/scripts/pr-context.mjs +212 -0
- package/skills/engineering/better-review/scripts/pr-context.test.mjs +168 -0
- package/skills/engineering/better-review/scripts/rereview-scope.mjs +503 -0
- package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +587 -0
- package/skills/engineering/better-review/scripts/scan-diff.mjs +365 -0
- package/skills/engineering/better-review/scripts/scan-diff.test.mjs +414 -0
- package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +96 -0
- package/skills/engineering/e2e-test/LICENSE +3 -0
- package/skills/engineering/e2e-test/SKILL.md +98 -0
- package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +24 -0
- package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-config.md +73 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +144 -0
- package/skills/engineering/e2e-test/references/ci-integration.md +25 -0
- package/skills/engineering/e2e-test/references/ci-playwright-container.md +290 -0
- package/skills/engineering/e2e-test/references/presentation-sweep.md +104 -0
- package/skills/engineering/e2e-test/references/reviewing.md +41 -0
- package/skills/engineering/e2e-test/references/route-manifest.md +51 -0
- package/skills/engineering/e2e-test/release-check.mjs +93 -0
- package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
- package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
- package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +106 -0
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +117 -0
- package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
- package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +69 -0
- package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
- package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +98 -0
- package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +147 -0
- package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +71 -0
- package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
- package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +59 -0
- package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +82 -0
- package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +76 -0
- package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
- package/skills/engineering/engineering-runtime/LICENSE +3 -0
- package/skills/engineering/engineering-runtime/SKILL.md +11 -0
- package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/engineering-runtime/coherence/workflow.json +597 -0
- package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +68 -0
- package/skills/engineering/engineering-runtime/release-check.mjs +93 -0
- package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
- package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.mjs +187 -0
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +95 -0
- package/skills/engineering/engineering-runtime/scripts/main-module.mjs +79 -0
- package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +288 -0
- package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
- package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
- package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +96 -0
- package/skills/engineering/engineering-runtime/scripts/review-completion.mjs +56 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +595 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +189 -0
- package/skills/engineering/engineering-runtime/scripts/yaml-mapping.mjs +18 -0
- package/skills/engineering/ensure-coverage/LICENSE +3 -0
- package/skills/engineering/ensure-coverage/SKILL.md +129 -0
- package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +92 -0
- package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +85 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
- package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
- package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +19 -0
- package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
- package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +57 -0
- package/skills/engineering/ensure-coverage/references/depth/test-smells.md +39 -0
- package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +44 -0
- package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +110 -0
- package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +35 -0
- package/skills/engineering/ensure-coverage/references/presentation/axis.md +64 -0
- package/skills/engineering/ensure-coverage/references/process/audit-mode.md +27 -0
- package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
- package/skills/engineering/ensure-coverage/references/process/test-strategy.md +56 -0
- package/skills/engineering/ensure-coverage/release-check.mjs +93 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +67 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +140 -0
- package/skills/engineering/ensure-coverage/scripts/README.md +99 -0
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +182 -0
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +96 -0
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +105 -0
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +47 -0
- package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +137 -0
- package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +86 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +674 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +447 -0
- package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
- package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +552 -0
- package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +282 -0
- package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +289 -0
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +119 -0
- package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
- package/skills/engineering/golden-path/LICENSE +3 -0
- package/skills/engineering/golden-path/SKILL.md +230 -0
- package/skills/engineering/golden-path/agents/openai.yaml +3 -0
- package/skills/engineering/golden-path/assets/golden-paths.example.json +57 -0
- package/skills/engineering/golden-path/references/contract.md +239 -0
- package/skills/engineering/golden-path/references/migration.md +111 -0
- package/skills/engineering/golden-path/release-check.mjs +93 -0
- package/skills/engineering/golden-path/scripts/golden-path-core.mjs +1779 -0
- package/skills/engineering/golden-path/scripts/golden-path.mjs +6 -0
- package/skills/engineering/golden-path/scripts/golden-path.test.mjs +1457 -0
- package/skills/engineering/harness-runtime/LICENSE +3 -0
- package/skills/engineering/harness-runtime/SKILL.md +19 -0
- package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
- package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
- package/skills/engineering/harness-runtime/contracts.md +15 -0
- package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
- package/skills/engineering/harness-runtime/discover-models.test.mjs +363 -0
- package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
- package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
- package/skills/engineering/harness-runtime/harness-runtime.test.mjs +967 -0
- package/skills/engineering/harness-runtime/index.mjs +767 -0
- package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +829 -0
- package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
- package/skills/engineering/harness-runtime/model-catalog.test.mjs +366 -0
- package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
- package/skills/engineering/harness-runtime/references/model-catalog-seed.json +408 -0
- package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
- package/skills/engineering/harness-runtime/release-check.mjs +93 -0
- package/skills/engineering/resolve-issues/LICENSE +3 -0
- package/skills/engineering/resolve-issues/SKILL.md +250 -0
- package/skills/engineering/resolve-issues/agents/openai.yaml +9 -0
- package/skills/engineering/resolve-issues/plugin-hooks.json +17 -0
- package/skills/engineering/resolve-issues/references/canary.md +28 -0
- package/skills/engineering/resolve-issues/references/ci.md +105 -0
- package/skills/engineering/resolve-issues/references/done.md +25 -0
- package/skills/engineering/resolve-issues/references/dropbox.md +90 -0
- package/skills/engineering/resolve-issues/references/expire.md +318 -0
- package/skills/engineering/resolve-issues/references/exploration.md +202 -0
- package/skills/engineering/resolve-issues/references/implementation.md +303 -0
- package/skills/engineering/resolve-issues/references/preflight.md +445 -0
- package/skills/engineering/resolve-issues/references/review.md +228 -0
- package/skills/engineering/resolve-issues/references/shared-modules.md +44 -0
- package/skills/engineering/resolve-issues/references/task-plan.md +264 -0
- package/skills/engineering/resolve-issues/release-check.mjs +93 -0
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.mjs +364 -0
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +280 -0
- package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/dropbox.mjs +473 -0
- package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +346 -0
- package/skills/engineering/resolve-issues/scripts/findings-reuse.mjs +98 -0
- package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +88 -0
- package/skills/engineering/resolve-issues/scripts/github-reviews.mjs +384 -0
- package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +264 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +135 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +79 -0
- package/skills/engineering/resolve-issues/scripts/issue-tree.mjs +162 -0
- package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +107 -0
- package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +14 -0
- package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +149 -0
- package/skills/engineering/resolve-issues/scripts/next-actions.mjs +856 -0
- package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +915 -0
- package/skills/engineering/resolve-issues/scripts/plan-sync.mjs +269 -0
- package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +156 -0
- package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +736 -0
- package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +448 -0
- package/skills/engineering/resolve-issues/scripts/ready-check.mjs +185 -0
- package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +217 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +98 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +91 -0
- package/skills/engineering/resolve-issues/scripts/review-verdict.mjs +509 -0
- package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +1060 -0
- package/skills/engineering/resolve-issues/scripts/size-check.mjs +492 -0
- package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +458 -0
- package/skills/engineering/resolve-release/LICENSE +3 -0
- package/skills/engineering/resolve-release/SKILL.md +215 -0
- package/skills/engineering/resolve-release/agents/openai.yaml +8 -0
- package/skills/engineering/resolve-release/flows/cloud-run-canary.md +13 -0
- package/skills/engineering/resolve-release/flows/cloud-run.md +7 -0
- package/skills/engineering/resolve-release/flows/root-tag-npm.md +13 -0
- package/skills/engineering/resolve-release/flows/workspace-npm.md +22 -0
- package/skills/engineering/resolve-release/references/canary.md +51 -0
- package/skills/engineering/resolve-release/references/candidate.md +41 -0
- package/skills/engineering/resolve-release/references/cut.md +131 -0
- package/skills/engineering/resolve-release/references/dark-launch.md +66 -0
- package/skills/engineering/resolve-release/references/flows.md +150 -0
- package/skills/engineering/resolve-release/references/preflight.md +154 -0
- package/skills/engineering/resolve-release/references/prepared-metadata.md +260 -0
- package/skills/engineering/resolve-release/references/promotion.md +82 -0
- package/skills/engineering/resolve-release/references/publish.md +232 -0
- package/skills/engineering/resolve-release/references/workspace-npm.md +103 -0
- package/skills/engineering/resolve-release/release-check.mjs +93 -0
- package/skills/engineering/resolve-release/scripts/apply.mjs +212 -0
- package/skills/engineering/resolve-release/scripts/apply.test.mjs +99 -0
- package/skills/engineering/resolve-release/scripts/dark-flags.mjs +134 -0
- package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +100 -0
- package/skills/engineering/resolve-release/scripts/flow.mjs +500 -0
- package/skills/engineering/resolve-release/scripts/flow.test.mjs +340 -0
- package/skills/engineering/resolve-release/scripts/green-head.mjs +231 -0
- package/skills/engineering/resolve-release/scripts/green-head.test.mjs +101 -0
- package/skills/engineering/resolve-release/scripts/npm-version-absence.mjs +138 -0
- package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +426 -0
- package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +659 -0
- package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +317 -0
- package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +291 -0
- package/skills/engineering/resolve-release/scripts/plan.mjs +806 -0
- package/skills/engineering/resolve-release/scripts/plan.test.mjs +428 -0
- package/skills/engineering/rush-issues/LICENSE +3 -0
- package/skills/engineering/rush-issues/SKILL.md +58 -0
- package/skills/engineering/rush-issues/agents/openai.yaml +8 -0
- package/skills/engineering/rush-issues/rehydrate.mjs +10 -0
- package/skills/engineering/rush-issues/release-check.mjs +93 -0
- package/skills/engineering/rush-release/LICENSE +3 -0
- package/skills/engineering/rush-release/SKILL.md +35 -0
- package/skills/engineering/rush-release/agents/openai.yaml +8 -0
- package/skills/engineering/rush-release/release-check.mjs +93 -0
- package/skills/engineering/test-quality/LICENSE +3 -0
- package/skills/engineering/test-quality/SKILL.md +79 -0
- package/skills/engineering/test-quality/references/audit.md +80 -0
- package/skills/engineering/test-quality/references/lint.md +24 -0
- package/skills/engineering/test-quality/references/principles.md +143 -0
- package/skills/engineering/test-quality/references/testing-conventions.md +9 -0
- package/skills/engineering/test-quality/release-check.mjs +93 -0
- package/skills/engineering/test-quality/scripts/lint-tests.mjs +574 -0
- package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +591 -0
- package/skills/engineering/test-quality/scripts/mock-policy-config.mjs +176 -0
- package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +119 -0
- package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +117 -0
- package/skills/engineering/test-quality/scripts/persist-conventions.mjs +17 -0
- package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +104 -0
- package/skills/risk-compliance/compliance-audit/LICENSE +3 -0
- package/skills/risk-compliance/compliance-audit/SKILL.md +439 -0
- package/skills/risk-compliance/compliance-audit/references/findings-schema.md +293 -0
- package/skills/risk-compliance/compliance-audit/references/normalization-rules.md +45 -0
- package/skills/risk-compliance/compliance-audit/references/vars-checklist.md +1068 -0
- package/skills/risk-compliance/compliance-audit/references/vars-findings-schema.md +398 -0
- package/skills/risk-compliance/compliance-audit/release-check.mjs +93 -0
- package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +14 -0
- package/skills/risk-compliance/compliance-audit/scripts/export_tracker_xlsx.py +221 -0
- package/skills/risk-compliance/compliance-audit/scripts/reconcile_tracker.py +458 -0
- package/skills/risk-compliance/compliance-audit/scripts/test_export_tracker_xlsx.py +97 -0
- package/skills/risk-compliance/compliance-audit/scripts/test_validate_tracker.py +157 -0
- package/skills/risk-compliance/compliance-audit/scripts/validate_tracker.py +829 -0
- package/skills/risk-compliance/contract-compare/LICENSE +3 -0
- package/skills/risk-compliance/contract-compare/SKILL.md +199 -0
- package/skills/risk-compliance/contract-compare/references/comparison-dimensions.md +209 -0
- package/skills/risk-compliance/contract-compare/release-check.mjs +93 -0
- package/skills/risk-compliance/contract-review/LICENSE +3 -0
- package/skills/risk-compliance/contract-review/SKILL.md +240 -0
- package/skills/risk-compliance/contract-review/references/review-criteria.md +223 -0
- package/skills/risk-compliance/contract-review/release-check.mjs +93 -0
- package/skills/risk-compliance/doc-authenticity/LICENSE +3 -0
- package/skills/risk-compliance/doc-authenticity/SKILL.md +160 -0
- package/skills/risk-compliance/doc-authenticity/agents/openai.yaml +8 -0
- package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +12 -0
- package/skills/risk-compliance/doc-authenticity/references/agreement.md +66 -0
- package/skills/risk-compliance/doc-authenticity/references/bank-statement.md +150 -0
- package/skills/risk-compliance/doc-authenticity/references/forensic-signals.md +157 -0
- package/skills/risk-compliance/doc-authenticity/references/image-signals.md +114 -0
- package/skills/risk-compliance/doc-authenticity/references/invoice.md +66 -0
- package/skills/risk-compliance/doc-authenticity/references/issuer-profiles.md +86 -0
- package/skills/risk-compliance/doc-authenticity/references/verdict-and-report.md +95 -0
- package/skills/risk-compliance/doc-authenticity/references/verdict.schema.json +81 -0
- package/skills/risk-compliance/doc-authenticity/release-check.mjs +93 -0
- package/skills/risk-compliance/doc-authenticity/scripts/image_forensics.py +294 -0
- package/skills/risk-compliance/doc-authenticity/scripts/pdf_forensics.py +401 -0
- package/skills/risk-compliance/doc-authenticity/scripts/validate_codes.py +224 -0
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.py +308 -0
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +212 -0
- package/skills/risk-compliance/fraud-analysis/LICENSE +3 -0
- package/skills/risk-compliance/fraud-analysis/SKILL.md +114 -0
- package/skills/risk-compliance/fraud-analysis/references/archetypes/authorized-payment-scam.md +41 -0
- package/skills/risk-compliance/fraud-analysis/references/archetypes/first-party-fraud.md +44 -0
- package/skills/risk-compliance/fraud-analysis/references/archetypes/third-party-fraud.md +27 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/bank-transfer.md +24 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/card-payment.md +30 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/payment-collection.md +20 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/payout.md +20 -0
- package/skills/risk-compliance/fraud-analysis/references/feature-engineering.md +158 -0
- package/skills/risk-compliance/fraud-analysis/references/mechanisms/account-takeover.md +36 -0
- package/skills/risk-compliance/fraud-analysis/references/report-rationale.md +45 -0
- package/skills/risk-compliance/fraud-analysis/references/report-template.md +190 -0
- package/skills/risk-compliance/fraud-analysis/references/review-checklist.md +175 -0
- package/skills/risk-compliance/fraud-analysis/references/taxonomy.md +79 -0
- package/skills/risk-compliance/fraud-analysis/references/terminology.md +108 -0
- package/skills/risk-compliance/fraud-analysis/references/workflow.md +175 -0
- package/skills/risk-compliance/fraud-analysis/release-check.mjs +93 -0
- package/skills/risk-compliance/payment-analysis/LICENSE +3 -0
- package/skills/risk-compliance/payment-analysis/SKILL.md +180 -0
- package/skills/risk-compliance/payment-analysis/references/auth-rate-actions.md +30 -0
- package/skills/risk-compliance/payment-analysis/references/chargebacks.md +208 -0
- package/skills/risk-compliance/payment-analysis/references/chinese-polish.md +51 -0
- package/skills/risk-compliance/payment-analysis/references/event-layers.md +127 -0
- package/skills/risk-compliance/payment-analysis/references/fx.md +59 -0
- package/skills/risk-compliance/payment-analysis/references/journey.md +79 -0
- package/skills/risk-compliance/payment-analysis/references/metrics.md +67 -0
- package/skills/risk-compliance/payment-analysis/references/report-template.md +119 -0
- package/skills/risk-compliance/payment-analysis/references/terminology.md +95 -0
- package/skills/risk-compliance/payment-analysis/references/visualization.md +56 -0
- package/skills/risk-compliance/payment-analysis/release-check.mjs +93 -0
- package/skills/risk-compliance/report-writing/LICENSE +3 -0
- package/skills/risk-compliance/report-writing/SKILL.md +220 -0
- package/skills/risk-compliance/report-writing/references/audit-report-template.md +121 -0
- package/skills/risk-compliance/report-writing/references/external-assessment-template.md +167 -0
- package/skills/risk-compliance/report-writing/references/management-report-template.md +145 -0
- package/skills/risk-compliance/report-writing/references/vars-report-template.md +224 -0
- package/skills/risk-compliance/report-writing/release-check.mjs +93 -0
- package/skills/risk-compliance/similar-domain/LICENSE +3 -0
- package/skills/risk-compliance/similar-domain/SKILL.md +203 -0
- package/skills/risk-compliance/similar-domain/references/entities.md +67 -0
- package/skills/risk-compliance/similar-domain/references/fingerprints.md +134 -0
- package/skills/risk-compliance/similar-domain/references/pivots.md +82 -0
- package/skills/risk-compliance/similar-domain/references/report-template.md +93 -0
- package/skills/risk-compliance/similar-domain/references/scoring.md +130 -0
- package/skills/risk-compliance/similar-domain/references/terminology.md +41 -0
- package/skills/risk-compliance/similar-domain/references/workflow.md +85 -0
- package/skills/risk-compliance/similar-domain/release-check.mjs +93 -0
- package/skills/risk-compliance/tld-detection/LICENSE +3 -0
- package/skills/risk-compliance/tld-detection/SKILL.md +245 -0
- package/skills/risk-compliance/tld-detection/agents/openai.yaml +9 -0
- package/skills/risk-compliance/tld-detection/references/chinese-polish.md +55 -0
- package/skills/risk-compliance/tld-detection/references/clean.md +51 -0
- package/skills/risk-compliance/tld-detection/references/clustering.md +235 -0
- package/skills/risk-compliance/tld-detection/references/decision.md +189 -0
- package/skills/risk-compliance/tld-detection/references/fx-conversion.md +232 -0
- package/skills/risk-compliance/tld-detection/references/report-template.md +272 -0
- package/skills/risk-compliance/tld-detection/references/streams.md +141 -0
- package/skills/risk-compliance/tld-detection/references/terminology.md +95 -0
- package/skills/risk-compliance/tld-detection/references/workflow.md +95 -0
- package/skills/risk-compliance/tld-detection/release-check.mjs +93 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.py +390 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +14 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.py +650 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +14 -0
- package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +134 -0
- package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +520 -0
|
@@ -0,0 +1,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())
|