@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,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
detect-routes-nextjs.mjs
|
|
5
|
+
|
|
6
|
+
Detect all Next.js App Router routes and compare against route manifest.
|
|
7
|
+
Fails CI if any route exists in the app but not in the manifest.
|
|
8
|
+
|
|
9
|
+
Scans for page routes (page.tsx) and API routes (route.ts).
|
|
10
|
+
|
|
11
|
+
Handles Next.js conventions:
|
|
12
|
+
Route groups (auth)/ -- stripped from path
|
|
13
|
+
Dynamic segments [id]/ -- preserved
|
|
14
|
+
Catch-all [...slug]/ -- preserved
|
|
15
|
+
Parallel routes @panel/ -- skipped
|
|
16
|
+
Intercepting routes (.)/ -- skipped
|
|
17
|
+
Private folders _components/ -- skipped
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
node detect-routes-nextjs.mjs [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--app-dir <path> Path to app/ directory (default: src/app or app)
|
|
24
|
+
--manifest <path> Path to route-manifest.json (default: e2e/route-manifest.json)
|
|
25
|
+
--strict Exit 1 if any route is missing (default: true)
|
|
26
|
+
--no-strict Only warn, don't fail
|
|
27
|
+
|
|
28
|
+
Exit codes:
|
|
29
|
+
0 -- all routes accounted for
|
|
30
|
+
1 -- routes missing from manifest (strict mode)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { readdirSync, existsSync, readFileSync, statSync } from 'fs';
|
|
34
|
+
import { join, resolve, relative, sep } from 'path';
|
|
35
|
+
|
|
36
|
+
// ─── CLI args ───────────────────────────────────────────────
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
function getArg(name, defaultVal) {
|
|
39
|
+
const idx = args.indexOf(`--${name}`);
|
|
40
|
+
return idx >= 0 && args[idx + 1] ? args[idx + 1] : defaultVal;
|
|
41
|
+
}
|
|
42
|
+
const hasFlag = (name) => args.includes(`--${name}`);
|
|
43
|
+
const strict = !hasFlag('no-strict');
|
|
44
|
+
|
|
45
|
+
// Auto-detect app directory
|
|
46
|
+
function findAppDir() {
|
|
47
|
+
const explicit = getArg('app-dir', '');
|
|
48
|
+
if (explicit) return resolve(explicit);
|
|
49
|
+
if (existsSync('src/app')) return resolve('src/app');
|
|
50
|
+
if (existsSync('app')) return resolve('app');
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const appDir = findAppDir();
|
|
55
|
+
const manifestPath = resolve(getArg('manifest', 'e2e/route-manifest.json'));
|
|
56
|
+
|
|
57
|
+
if (!appDir || !existsSync(appDir)) {
|
|
58
|
+
console.error(`❌ App directory not found. Use --app-dir <path>`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Route scanning ─────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Recursively scan the app directory for page.tsx/ts and route.ts/tsx files.
|
|
66
|
+
* Returns an array of { path: string, type: 'page' | 'api', file: string }
|
|
67
|
+
*/
|
|
68
|
+
function scanRoutes(dir, segments = []) {
|
|
69
|
+
const routes = [];
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
73
|
+
} catch {
|
|
74
|
+
return routes;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for route files in current directory
|
|
78
|
+
const pageFile = entries.find(
|
|
79
|
+
(e) => e.isFile() && /^page\.(tsx?|jsx?)$/.test(e.name)
|
|
80
|
+
);
|
|
81
|
+
const routeFile = entries.find(
|
|
82
|
+
(e) => e.isFile() && /^route\.(tsx?|jsx?)$/.test(e.name)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (pageFile) {
|
|
86
|
+
const routePath = '/' + segments.join('/') || '/';
|
|
87
|
+
routes.push({
|
|
88
|
+
path: routePath === '/' ? '/' : routePath,
|
|
89
|
+
type: 'page',
|
|
90
|
+
file: relative(process.cwd(), join(dir, pageFile.name)),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (routeFile) {
|
|
95
|
+
const routePath = '/' + segments.join('/') || '/';
|
|
96
|
+
routes.push({
|
|
97
|
+
path: routePath,
|
|
98
|
+
type: 'api',
|
|
99
|
+
file: relative(process.cwd(), join(dir, routeFile.name)),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Recurse into subdirectories
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (!entry.isDirectory()) continue;
|
|
106
|
+
const name = entry.name;
|
|
107
|
+
|
|
108
|
+
// Skip private folders (Next.js convention: _name)
|
|
109
|
+
if (name.startsWith('_')) continue;
|
|
110
|
+
|
|
111
|
+
// Skip parallel routes (@panel, @modal — not independent routes)
|
|
112
|
+
if (name.startsWith('@')) continue;
|
|
113
|
+
|
|
114
|
+
// Skip intercepting routes ((.), (..), (..)(..))
|
|
115
|
+
if (/^\(\.+\)/.test(name)) continue;
|
|
116
|
+
|
|
117
|
+
// Route groups: (groupName) => strip from path
|
|
118
|
+
if (/^\([^)]+\)$/.test(name) && !name.startsWith('(.)')) {
|
|
119
|
+
routes.push(...scanRoutes(join(dir, name), segments));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Normal segment (including [dynamic], [...catchAll], [[...optional]])
|
|
124
|
+
routes.push(...scanRoutes(join(dir, name), [...segments, name]));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return routes;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Manifest comparison ────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
// Load manifest
|
|
133
|
+
let manifest = { routes: [] };
|
|
134
|
+
if (existsSync(manifestPath)) {
|
|
135
|
+
try {
|
|
136
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error(`❌ Invalid JSON in manifest: ${e.message}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
console.error(`❌ Manifest not found: ${manifestPath}`);
|
|
143
|
+
console.error(` Create it with at minimum: { "version": 1, "routes": [] }`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const manifestRoutes = new Set(
|
|
148
|
+
(manifest.routes || []).map((r) => r.path)
|
|
149
|
+
);
|
|
150
|
+
// Also check endpoints (for API routes in combined manifests)
|
|
151
|
+
const manifestEndpoints = new Set(
|
|
152
|
+
(manifest.endpoints || []).map((r) => r.path)
|
|
153
|
+
);
|
|
154
|
+
const allManifestPaths = new Set([...manifestRoutes, ...manifestEndpoints]);
|
|
155
|
+
|
|
156
|
+
// Scan
|
|
157
|
+
console.log(`🔍 Scanning: ${relative(process.cwd(), appDir)}/`);
|
|
158
|
+
console.log(`📋 Manifest: ${relative(process.cwd(), manifestPath)}\n`);
|
|
159
|
+
|
|
160
|
+
const appRoutes = scanRoutes(appDir);
|
|
161
|
+
const missing = [];
|
|
162
|
+
const accounted = [];
|
|
163
|
+
|
|
164
|
+
for (const route of appRoutes) {
|
|
165
|
+
if (allManifestPaths.has(route.path)) {
|
|
166
|
+
accounted.push(route);
|
|
167
|
+
} else {
|
|
168
|
+
missing.push(route);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── Report ─────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
console.log(`Found ${appRoutes.length} routes in app (${appRoutes.filter(r => r.type === 'page').length} pages, ${appRoutes.filter(r => r.type === 'api').length} API routes)`);
|
|
175
|
+
console.log(`Manifest has ${allManifestPaths.size} entries`);
|
|
176
|
+
console.log(`Accounted: ${accounted.length} | Missing: ${missing.length}\n`);
|
|
177
|
+
|
|
178
|
+
if (missing.length > 0) {
|
|
179
|
+
console.log(`${'─'.repeat(60)}`);
|
|
180
|
+
console.log(`❌ Routes in app but NOT in manifest:\n`);
|
|
181
|
+
for (const route of missing) {
|
|
182
|
+
const tag = route.type === 'api' ? ' [API]' : '';
|
|
183
|
+
console.log(` ${route.path}${tag}`);
|
|
184
|
+
console.log(` └─ ${route.file}`);
|
|
185
|
+
}
|
|
186
|
+
console.log(`\n${'─'.repeat(60)}`);
|
|
187
|
+
console.log(`\nAdd these to ${relative(process.cwd(), manifestPath)}:`);
|
|
188
|
+
console.log(` { "path": "<route>", "tier": 1, "specs": [] }\n`);
|
|
189
|
+
console.log(`Routes with specs: [] will pass CI but show as uncovered`);
|
|
190
|
+
console.log(`in the E2E PR comment.\n`);
|
|
191
|
+
|
|
192
|
+
if (strict) {
|
|
193
|
+
console.log('🚫 Failing CI (strict mode). Use --no-strict to warn only.');
|
|
194
|
+
process.exit(1);
|
|
195
|
+
} else {
|
|
196
|
+
console.log('⚠️ Warning only (non-strict mode).');
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
console.log('✅ All app routes are accounted for in the manifest.');
|
|
200
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Presentation-axis instruments for e2e-test (issue #195).
|
|
2
|
+
//
|
|
3
|
+
// Real-rendered-page checks — import into a Playwright spec and tag it
|
|
4
|
+
// `@covers <route>` / `@level presentation:spatial` so the Coverage Ledger
|
|
5
|
+
// resolves it. The spine ships the Spatial/overflow invariant; later slices add
|
|
6
|
+
// perceivability (contrast), reachability, and temporal (layout shift).
|
|
7
|
+
//
|
|
8
|
+
// These drive a REAL browser. A DOM-only / JSDOM equivalent cannot see layout and
|
|
9
|
+
// is graded `Over-mocked` by the ledger — that is intentional.
|
|
10
|
+
|
|
11
|
+
// The condition-matrix floor: every browser route is checked at these viewports.
|
|
12
|
+
// Mirrors PRESENTATION_VIEWPORT_FLOOR in ensure-coverage's coverage-ledger.mjs.
|
|
13
|
+
export const PRESENTATION_VIEWPORT_FLOOR = [
|
|
14
|
+
{ name: 'mobile', width: 375, height: 812 },
|
|
15
|
+
{ name: 'desktop', width: 1280, height: 800 },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
// Pure predicate (no browser) — given element geometry and the viewport width,
|
|
19
|
+
// return the elements that overflow horizontally. Extracted so the detection
|
|
20
|
+
// logic is unit-testable without a browser; the Playwright wrapper collects the
|
|
21
|
+
// geometry and delegates here.
|
|
22
|
+
//
|
|
23
|
+
// elements: [{ selector, left, right, width, height }]
|
|
24
|
+
export function findHorizontalOverflow(elements, viewportWidth, { tolerancePx = 1, ignore = [] } = {}) {
|
|
25
|
+
const offenders = []
|
|
26
|
+
for (const el of elements) {
|
|
27
|
+
if (!el.width || !el.height) continue // not rendered
|
|
28
|
+
const overflowsRight = el.right - tolerancePx > viewportWidth
|
|
29
|
+
const overflowsLeft = el.left < -tolerancePx
|
|
30
|
+
if (!overflowsRight && !overflowsLeft) continue
|
|
31
|
+
if (ignore.some((ig) => (el.selector || '').includes(ig))) continue
|
|
32
|
+
offenders.push({
|
|
33
|
+
selector: el.selector,
|
|
34
|
+
overflowPx: Math.round(overflowsRight ? el.right - viewportWidth : -el.left),
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
return offenders
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// In-browser collector, serialized into page.evaluate. Returns the viewport width
|
|
41
|
+
// and one record per body descendant.
|
|
42
|
+
function collectGeometry() {
|
|
43
|
+
const vw = document.documentElement.clientWidth
|
|
44
|
+
const sel = (el) => {
|
|
45
|
+
if (el.id) return '#' + el.id
|
|
46
|
+
let s = el.tagName.toLowerCase()
|
|
47
|
+
if (typeof el.className === 'string' && el.className.trim()) {
|
|
48
|
+
s += '.' + el.className.trim().split(/\s+/).join('.')
|
|
49
|
+
}
|
|
50
|
+
return s
|
|
51
|
+
}
|
|
52
|
+
const elements = [...document.querySelectorAll('body *')].map((el) => {
|
|
53
|
+
const r = el.getBoundingClientRect()
|
|
54
|
+
return { selector: sel(el), left: r.left, right: r.right, width: r.width, height: r.height }
|
|
55
|
+
})
|
|
56
|
+
return { vw, elements }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Assert no unintended horizontal overflow at each floor viewport. Throws with the
|
|
60
|
+
// offending selectors + overflow amounts. `ignore` suppresses intentional cases by
|
|
61
|
+
// substring match on the selector (curate sparingly).
|
|
62
|
+
export async function assertNoHorizontalOverflow(page, { viewports = PRESENTATION_VIEWPORT_FLOOR, tolerancePx = 1, ignore = [] } = {}) {
|
|
63
|
+
const offenders = []
|
|
64
|
+
for (const vp of viewports) {
|
|
65
|
+
await page.setViewportSize({ width: vp.width, height: vp.height })
|
|
66
|
+
// Let layout settle (fonts/images) — real condition, not a fixed sleep.
|
|
67
|
+
await page.waitForLoadState('networkidle').catch(() => {})
|
|
68
|
+
const { vw, elements } = await page.evaluate(collectGeometry)
|
|
69
|
+
for (const o of findHorizontalOverflow(elements, vw, { tolerancePx, ignore })) {
|
|
70
|
+
offenders.push({ viewport: vp.name, width: vp.width, ...o })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (offenders.length) {
|
|
74
|
+
const lines = offenders.map((o) => ` [${o.viewport} ${o.width}px] ${o.selector} overflows by ${o.overflowPx}px`)
|
|
75
|
+
throw new Error(`presentation:spatial — horizontal overflow detected:\n${lines.join('\n')}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------- overlap detection (part of presentation:spatial) ----------
|
|
80
|
+
|
|
81
|
+
// Pure AABB intersection test with a tolerance (sub-pixel touching is not overlap).
|
|
82
|
+
export function rectsIntersect(a, b, tolerancePx = 1) {
|
|
83
|
+
return (
|
|
84
|
+
a.left < b.right - tolerancePx &&
|
|
85
|
+
a.right > b.left + tolerancePx &&
|
|
86
|
+
a.top < b.bottom - tolerancePx &&
|
|
87
|
+
a.bottom > b.top + tolerancePx
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Pure: find intersecting element pairs. `elements` is a flat list of rendered
|
|
92
|
+
// elements with geometry + an `interactive` flag; only sibling-ish *content*
|
|
93
|
+
// overlaps matter, so a pair where one element CONTAINS the other (nesting) is
|
|
94
|
+
// skipped — nesting is normal, collision is not.
|
|
95
|
+
function contains(outer, inner, tol = 1) {
|
|
96
|
+
return outer.left - tol <= inner.left && outer.right + tol >= inner.right &&
|
|
97
|
+
outer.top - tol <= inner.top && outer.bottom + tol >= inner.bottom
|
|
98
|
+
}
|
|
99
|
+
export function findOverlaps(elements, { tolerancePx = 1 } = {}) {
|
|
100
|
+
const els = elements.filter((e) => (e.rect?.width || 0) > 0 && (e.rect?.height || 0) > 0)
|
|
101
|
+
const pairs = []
|
|
102
|
+
for (let i = 0; i < els.length; i++) {
|
|
103
|
+
for (let j = i + 1; j < els.length; j++) {
|
|
104
|
+
const a = els[i], b = els[j]
|
|
105
|
+
if (!rectsIntersect(a.rect, b.rect, tolerancePx)) continue
|
|
106
|
+
if (contains(a.rect, b.rect, tolerancePx) || contains(b.rect, a.rect, tolerancePx)) continue // nesting
|
|
107
|
+
pairs.push({ a: a.selector, b: b.selector, aInteractive: !!a.interactive, bInteractive: !!b.interactive })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return pairs
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Pure: apply the GATED allowlist to overlap pairs. An allowlist entry needs a
|
|
114
|
+
// `match` (substring tested against either selector), a `category`, and a `reason`.
|
|
115
|
+
// GATE (mirrors the Surface Baseline Contract's exclusion gate): an entry may NOT
|
|
116
|
+
// suppress a pair where an **interactive** element is occluded — you cannot
|
|
117
|
+
// allowlist a reachable control into invisibility. Returns reported (still failing),
|
|
118
|
+
// suppressed (census), and rejected (bad allowlist usage — also failing).
|
|
119
|
+
export function applyOverlapAllowlist(pairs, allowlist = []) {
|
|
120
|
+
const reported = [], suppressed = [], rejected = []
|
|
121
|
+
for (const p of pairs) {
|
|
122
|
+
const entry = allowlist.find((e) => e.match && (p.a.includes(e.match) || p.b.includes(e.match)))
|
|
123
|
+
if (!entry) { reported.push(p); continue }
|
|
124
|
+
if (!entry.category || !entry.reason) { rejected.push({ ...p, why: 'allowlist entry needs category + reason' }); continue }
|
|
125
|
+
if (p.aInteractive || p.bInteractive) { rejected.push({ ...p, why: `cannot allowlist an overlap that occludes an interactive element (${entry.category})` }); continue }
|
|
126
|
+
suppressed.push({ ...p, category: entry.category, reason: entry.reason })
|
|
127
|
+
}
|
|
128
|
+
return { reported, suppressed, rejected }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// In-browser collector for overlap: geometry + interactivity per element.
|
|
132
|
+
function collectOverlapGeometry() {
|
|
133
|
+
const sel = (el) => {
|
|
134
|
+
if (el.id) return '#' + el.id
|
|
135
|
+
let s = el.tagName.toLowerCase()
|
|
136
|
+
if (typeof el.className === 'string' && el.className.trim()) s += '.' + el.className.trim().split(/\s+/).join('.')
|
|
137
|
+
return s
|
|
138
|
+
}
|
|
139
|
+
const INTERACTIVE = 'a,button,input,select,textarea,[role="button"],[role="link"],[tabindex]'
|
|
140
|
+
return [...document.querySelectorAll('body *')].map((el) => {
|
|
141
|
+
const r = el.getBoundingClientRect()
|
|
142
|
+
return {
|
|
143
|
+
selector: sel(el),
|
|
144
|
+
rect: { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height },
|
|
145
|
+
interactive: el.matches(INTERACTIVE),
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Assert no unintended element overlap. Intentional z-stacks (carousels, badges,
|
|
151
|
+
// sticky headers) are suppressed only via the gated `allowlist`
|
|
152
|
+
// (`[{ match, category, reason }]`); the suppressions are returned as a visible
|
|
153
|
+
// census. Throws on real overlaps and on rejected (ungated/interactive) allowlist use.
|
|
154
|
+
export async function assertNoOverlap(page, { selector, tolerancePx = 1, allowlist = [], viewports = PRESENTATION_VIEWPORT_FLOOR } = {}) {
|
|
155
|
+
const failures = [], census = []
|
|
156
|
+
for (const vp of viewports) {
|
|
157
|
+
await page.setViewportSize({ width: vp.width, height: vp.height })
|
|
158
|
+
await page.waitForLoadState('networkidle').catch(() => {})
|
|
159
|
+
let elements = await page.evaluate(collectOverlapGeometry)
|
|
160
|
+
if (selector) elements = elements.filter((e) => e.selector.includes(selector))
|
|
161
|
+
const pairs = findOverlaps(elements, { tolerancePx })
|
|
162
|
+
const { reported, suppressed, rejected } = applyOverlapAllowlist(pairs, allowlist)
|
|
163
|
+
for (const r of [...reported, ...rejected]) failures.push({ viewport: vp.name, ...r })
|
|
164
|
+
for (const s of suppressed) census.push({ viewport: vp.name, ...s })
|
|
165
|
+
}
|
|
166
|
+
if (failures.length) {
|
|
167
|
+
const lines = failures.map((f) => ` [${f.viewport}] ${f.a} ⨯ ${f.b}${f.why ? ` — ${f.why}` : ''}`)
|
|
168
|
+
throw new Error(`presentation:spatial — element overlap detected:\n${lines.join('\n')}`)
|
|
169
|
+
}
|
|
170
|
+
return { census } // visible record of intentional, gated suppressions
|
|
171
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Unit tests for the pure overflow predicate (issue #195). Run: `node --test`.
|
|
2
|
+
// The Playwright wrapper (assertNoHorizontalOverflow) needs a real browser and is
|
|
3
|
+
// exercised downstream; this proves the detection logic without one.
|
|
4
|
+
|
|
5
|
+
import { test } from 'node:test'
|
|
6
|
+
import assert from 'node:assert/strict'
|
|
7
|
+
import {
|
|
8
|
+
findHorizontalOverflow, PRESENTATION_VIEWPORT_FLOOR,
|
|
9
|
+
rectsIntersect, findOverlaps, applyOverlapAllowlist,
|
|
10
|
+
} from './presentation-checks.mjs'
|
|
11
|
+
|
|
12
|
+
// helper: build an element with a rect + interactive flag
|
|
13
|
+
const el = (selector, [left, top, right, bottom], interactive = false) =>
|
|
14
|
+
({ selector, rect: { left, top, right, bottom, width: right - left, height: bottom - top }, interactive })
|
|
15
|
+
|
|
16
|
+
const VW = 375
|
|
17
|
+
|
|
18
|
+
test('no overflow when everything fits', () => {
|
|
19
|
+
const els = [
|
|
20
|
+
{ selector: 'header', left: 0, right: 375, width: 375, height: 60 },
|
|
21
|
+
{ selector: '.card', left: 16, right: 359, width: 343, height: 120 },
|
|
22
|
+
]
|
|
23
|
+
assert.deepEqual(findHorizontalOverflow(els, VW), [])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('flags an element wider than the viewport (right overflow)', () => {
|
|
27
|
+
const els = [{ selector: '.wide-table', left: 0, right: 520, width: 520, height: 200 }]
|
|
28
|
+
const out = findHorizontalOverflow(els, VW)
|
|
29
|
+
assert.equal(out.length, 1)
|
|
30
|
+
assert.equal(out[0].selector, '.wide-table')
|
|
31
|
+
assert.equal(out[0].overflowPx, 145) // 520 - 375
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('flags a negatively-positioned element (left overflow)', () => {
|
|
35
|
+
const els = [{ selector: '#drawer', left: -120, right: 200, width: 320, height: 400 }]
|
|
36
|
+
const out = findHorizontalOverflow(els, VW)
|
|
37
|
+
assert.equal(out.length, 1)
|
|
38
|
+
assert.equal(out[0].overflowPx, 120)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('respects the tolerance band (sub-pixel rounding does not flag)', () => {
|
|
42
|
+
const els = [{ selector: '.x', left: 0, right: 375.5, width: 375.5, height: 10 }]
|
|
43
|
+
assert.deepEqual(findHorizontalOverflow(els, VW, { tolerancePx: 1 }), [])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('ignore list suppresses intentional overflow by selector substring', () => {
|
|
47
|
+
const els = [{ selector: '.carousel-track', left: 0, right: 900, width: 900, height: 150 }]
|
|
48
|
+
assert.deepEqual(findHorizontalOverflow(els, VW, { ignore: ['carousel'] }), [])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('zero-size (unrendered) elements are skipped', () => {
|
|
52
|
+
const els = [{ selector: '.hidden', left: 0, right: 999, width: 0, height: 0 }]
|
|
53
|
+
assert.deepEqual(findHorizontalOverflow(els, VW), [])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('viewport floor is the documented 375 + 1280 pair', () => {
|
|
57
|
+
assert.deepEqual(PRESENTATION_VIEWPORT_FLOOR.map((v) => v.width), [375, 1280])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// ---------- overlap detection (presentation:spatial) ----------
|
|
61
|
+
|
|
62
|
+
test('rectsIntersect: overlapping yes, separated no, touching (within tol) no', () => {
|
|
63
|
+
const a = { left: 0, top: 0, right: 100, bottom: 50 }
|
|
64
|
+
assert.equal(rectsIntersect(a, { left: 50, top: 25, right: 150, bottom: 75 }), true)
|
|
65
|
+
assert.equal(rectsIntersect(a, { left: 200, top: 0, right: 300, bottom: 50 }), false)
|
|
66
|
+
assert.equal(rectsIntersect(a, { left: 100, top: 0, right: 200, bottom: 50 }), false) // edge-touch
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('findOverlaps: flags a collision but skips pure nesting', () => {
|
|
70
|
+
const parent = el('.card', [0, 0, 200, 200])
|
|
71
|
+
const child = el('.card .title', [10, 10, 190, 40]) // nested inside parent
|
|
72
|
+
const stray = el('.badge', [180, 180, 260, 260]) // overlaps parent corner
|
|
73
|
+
const pairs = findOverlaps([parent, child, stray])
|
|
74
|
+
// parent⨯child is nesting (skipped); parent⨯stray is a real overlap
|
|
75
|
+
assert.equal(pairs.length, 1)
|
|
76
|
+
assert.equal(pairs[0].b, '.badge')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('applyOverlapAllowlist: gated suppression yields a census entry', () => {
|
|
80
|
+
const pairs = [{ a: '.carousel-track', b: '.carousel-slide', aInteractive: false, bInteractive: false }]
|
|
81
|
+
const r = applyOverlapAllowlist(pairs, [{ match: 'carousel', category: 'decorative', reason: 'z-stacked slides' }])
|
|
82
|
+
assert.equal(r.reported.length, 0)
|
|
83
|
+
assert.equal(r.suppressed.length, 1)
|
|
84
|
+
assert.equal(r.suppressed[0].reason, 'z-stacked slides')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('applyOverlapAllowlist: entry without category/reason is rejected (still fails)', () => {
|
|
88
|
+
const pairs = [{ a: '.x', b: '.y', aInteractive: false, bInteractive: false }]
|
|
89
|
+
const r = applyOverlapAllowlist(pairs, [{ match: '.x' }])
|
|
90
|
+
assert.equal(r.suppressed.length, 0)
|
|
91
|
+
assert.equal(r.rejected.length, 1)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('applyOverlapAllowlist: cannot allowlist an overlap that occludes an interactive element', () => {
|
|
95
|
+
const pairs = [{ a: '.overlay', b: 'button#save', aInteractive: false, bInteractive: true }]
|
|
96
|
+
const r = applyOverlapAllowlist(pairs, [{ match: '.overlay', category: 'decorative', reason: 'hero flourish' }])
|
|
97
|
+
assert.equal(r.suppressed.length, 0)
|
|
98
|
+
assert.equal(r.rejected.length, 1)
|
|
99
|
+
assert.match(r.rejected[0].why, /interactive/)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('applyOverlapAllowlist: unmatched overlap is reported', () => {
|
|
103
|
+
const pairs = [{ a: '.a', b: '.b', aInteractive: false, bInteractive: false }]
|
|
104
|
+
const r = applyOverlapAllowlist(pairs, [{ match: '.zzz', category: 'decorative', reason: 'n/a' }])
|
|
105
|
+
assert.equal(r.reported.length, 1)
|
|
106
|
+
})
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Presentation-axis Perceivability instrument for e2e-test (issue #196).
|
|
2
|
+
//
|
|
3
|
+
// "Can a human actually see/distinguish this control" — and its negative form,
|
|
4
|
+
// "is something that must be hidden actually gone" (privilege leak, stale item).
|
|
5
|
+
//
|
|
6
|
+
// Positive form uses axe-core's `color-contrast` rule against the REAL rendered
|
|
7
|
+
// page. Tag the spec `@covers <route>` / `@level presentation:perceivability`.
|
|
8
|
+
// A DOM-only / JSDOM equivalent is graded Over-mocked by the ledger.
|
|
9
|
+
//
|
|
10
|
+
// Abstain doctrine: axe abstains ("incomplete") on text over a background-image /
|
|
11
|
+
// gradient / partially-obscured element — exactly the case that hides an invisible
|
|
12
|
+
// control. An abstain is NOT a silent pass: it surfaces as a blocking AbstainError.
|
|
13
|
+
// Resolve it by a manual check (verified: true) OR by
|
|
14
|
+
// escalating to a stronger instrument (screenshot / AI-vision triage). AI-vision is
|
|
15
|
+
// triage-only — it may flag for human attestation but can never by itself promote
|
|
16
|
+
// an obligation to Covered (run-to-run non-determinism).
|
|
17
|
+
|
|
18
|
+
// Distinguishable error so callers/CI can tell an abstain from a hard violation.
|
|
19
|
+
export class AbstainError extends Error {
|
|
20
|
+
constructor(message, details) {
|
|
21
|
+
super(message)
|
|
22
|
+
this.name = 'AbstainError'
|
|
23
|
+
this.abstain = true
|
|
24
|
+
this.details = details
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------- pure WCAG contrast math (testable, no browser) ----------
|
|
29
|
+
|
|
30
|
+
// sRGB channel [0..255] → linearized component for relative luminance.
|
|
31
|
+
function linearize(c) {
|
|
32
|
+
const s = c / 255
|
|
33
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// WCAG relative luminance of an [r,g,b] triple.
|
|
37
|
+
export function relativeLuminance([r, g, b]) {
|
|
38
|
+
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// WCAG contrast ratio between two [r,g,b] colors → 1..21.
|
|
42
|
+
export function contrastRatio(rgb1, rgb2) {
|
|
43
|
+
const l1 = relativeLuminance(rgb1)
|
|
44
|
+
const l2 = relativeLuminance(rgb2)
|
|
45
|
+
const [hi, lo] = l1 >= l2 ? [l1, l2] : [l2, l1]
|
|
46
|
+
return (hi + 0.05) / (lo + 0.05)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Does fg-on-bg meet the WCAG threshold? large text & non-text UI components use
|
|
50
|
+
// 3:1; normal text uses 4.5:1 (AA). Returns the ratio, threshold, and pass.
|
|
51
|
+
export function meetsContrast(fg, bg, { large = false, nonText = false } = {}) {
|
|
52
|
+
const ratio = contrastRatio(fg, bg)
|
|
53
|
+
const threshold = nonText || large ? 3 : 4.5
|
|
54
|
+
return { ratio: Math.round(ratio * 100) / 100, threshold, pass: ratio >= threshold }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------- pure axe-result classifier (testable, no browser) ----------
|
|
58
|
+
|
|
59
|
+
// Map an axe-core run to one of three Presentation outcomes for color-contrast.
|
|
60
|
+
// `pass` → satisfied; `fail` → hard violation; `abstain` → axe could not decide
|
|
61
|
+
// (`Unverified` until read and marked verified, or escalated).
|
|
62
|
+
export function classifyAxeContrast(axeResults = {}) {
|
|
63
|
+
const pick = (arr) => (arr || []).filter((r) => r.id === 'color-contrast')
|
|
64
|
+
const violations = pick(axeResults.violations)
|
|
65
|
+
const incomplete = pick(axeResults.incomplete)
|
|
66
|
+
if (violations.length) {
|
|
67
|
+
const nodes = violations.flatMap((v) => v.nodes || [])
|
|
68
|
+
return { status: 'fail', count: nodes.length, nodes }
|
|
69
|
+
}
|
|
70
|
+
if (incomplete.length) {
|
|
71
|
+
const nodes = incomplete.flatMap((v) => v.nodes || [])
|
|
72
|
+
return { status: 'abstain', count: nodes.length, nodes }
|
|
73
|
+
}
|
|
74
|
+
return { status: 'pass', count: 0, nodes: [] }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------- pure visibility predicate (testable, no browser) ----------
|
|
78
|
+
|
|
79
|
+
// Is an element effectively hidden from a sighted user? Used by the negative form
|
|
80
|
+
// (a privilege-leaked control must be truly gone, not just visually small) and to
|
|
81
|
+
// AVOID false-flagging intentionally non-rendered state. `style` is the element's
|
|
82
|
+
// computed style snapshot; `rect` its bounding box.
|
|
83
|
+
export function isEffectivelyHidden({ style = {}, rect = {}, ariaHidden = false } = {}) {
|
|
84
|
+
if (style.display === 'none') return true
|
|
85
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse') return true
|
|
86
|
+
if (Number(style.opacity) === 0) return true
|
|
87
|
+
if (ariaHidden) return true
|
|
88
|
+
if ((rect.width || 0) === 0 || (rect.height || 0) === 0) return true
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// State-aware: a disabled control is *intended* to be low-contrast (WCAG exempts
|
|
93
|
+
// it), so a contrast check must skip it rather than false-flag.
|
|
94
|
+
//
|
|
95
|
+
// NOTE on where this runs: `assertContrast` delegates to axe-core, which already
|
|
96
|
+
// skips disabled controls natively — so the primary path does NOT call this. This
|
|
97
|
+
// predicate (and `assessElementContrast` below) is the **manual / escalation path**:
|
|
98
|
+
// when axe abstains and a reviewer escalates to per-element checking, they compute
|
|
99
|
+
// contrast for the specific element here, and this is where the disabled-exemption
|
|
100
|
+
// is enforced.
|
|
101
|
+
export function isExemptFromContrast({ disabled = false, ariaDisabled = false } = {}) {
|
|
102
|
+
return disabled || ariaDisabled
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Manual / escalation per-element contrast assessment. Composes the disabled
|
|
106
|
+
// exemption with the WCAG threshold check → 'exempt' | 'pass' | 'fail'. Pure and
|
|
107
|
+
// testable; used when axe abstains and a reviewer resolves a specific element by
|
|
108
|
+
// reading its computed fg/bg colors.
|
|
109
|
+
export function assessElementContrast(fg, bg, { large = false, nonText = false, disabled = false, ariaDisabled = false } = {}) {
|
|
110
|
+
if (isExemptFromContrast({ disabled, ariaDisabled })) return { status: 'exempt' }
|
|
111
|
+
const m = meetsContrast(fg, bg, { large, nonText })
|
|
112
|
+
return { status: m.pass ? 'pass' : 'fail', ratio: m.ratio, threshold: m.threshold }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------- Playwright wrappers (need a real browser) ----------
|
|
116
|
+
|
|
117
|
+
// Assert the rendered page has no color-contrast violations. Requires
|
|
118
|
+
// @axe-core/playwright (peer dep). On a hard violation → throws Error; on an axe
|
|
119
|
+
// abstain ("incomplete") → throws AbstainError (blocking, resolve by attest/escalate).
|
|
120
|
+
export async function assertContrast(page, { axeBuilder, include, exclude } = {}) {
|
|
121
|
+
if (!axeBuilder) {
|
|
122
|
+
// Lazy import so the pure exports above are usable without the peer dep.
|
|
123
|
+
const mod = await import('@axe-core/playwright')
|
|
124
|
+
const AxeBuilder = mod.default || mod.AxeBuilder
|
|
125
|
+
axeBuilder = new AxeBuilder({ page })
|
|
126
|
+
}
|
|
127
|
+
let builder = axeBuilder.withRules(['color-contrast'])
|
|
128
|
+
if (include) builder = builder.include(include)
|
|
129
|
+
if (exclude) builder = builder.exclude(exclude)
|
|
130
|
+
const results = await builder.analyze()
|
|
131
|
+
const c = classifyAxeContrast(results)
|
|
132
|
+
if (c.status === 'fail') {
|
|
133
|
+
const where = c.nodes.map((n) => ' ' + (n.target || []).join(' ')).join('\n')
|
|
134
|
+
throw new Error(`presentation:perceivability — ${c.count} contrast violation(s):\n${where}`)
|
|
135
|
+
}
|
|
136
|
+
if (c.status === 'abstain') {
|
|
137
|
+
const where = c.nodes.map((n) => ' ' + (n.target || []).join(' ')).join('\n')
|
|
138
|
+
throw new AbstainError(
|
|
139
|
+
`presentation:perceivability — axe could not decide contrast for ${c.count} element(s) ` +
|
|
140
|
+
`(text over image/gradient/obscured). Unverified until checked by hand and marked verified, or escalated ` +
|
|
141
|
+
`to a screenshot/AI-vision triage:\n${where}`,
|
|
142
|
+
c,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
return c // { status: 'pass' }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Negative form: a control gated to a role must NOT be present/visible for a user
|
|
149
|
+
// lacking that role (a visual privilege leak). `locator` should resolve to the
|
|
150
|
+
// gated control; asserts it is absent or effectively hidden. Uses the pure
|
|
151
|
+
// `isEffectivelyHidden` predicate against the element's computed style + box, which
|
|
152
|
+
// catches cases Playwright's `isVisible()` misses (e.g. opacity:0 with non-zero
|
|
153
|
+
// size — visually invisible but still a leak if it were the positive case).
|
|
154
|
+
export async function assertHiddenFromWrongRole(page, locator) {
|
|
155
|
+
const count = await locator.count()
|
|
156
|
+
if (count === 0) return // not rendered — good
|
|
157
|
+
const snap = await locator.first().evaluate((el) => {
|
|
158
|
+
const cs = getComputedStyle(el)
|
|
159
|
+
const r = el.getBoundingClientRect()
|
|
160
|
+
return {
|
|
161
|
+
style: { display: cs.display, visibility: cs.visibility, opacity: cs.opacity },
|
|
162
|
+
rect: { width: r.width, height: r.height },
|
|
163
|
+
ariaHidden: el.getAttribute('aria-hidden') === 'true',
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
if (!isEffectivelyHidden(snap)) {
|
|
167
|
+
throw new Error(`presentation:perceivability (negative) — privilege-gated control is visible to a user without the role: ${locator}`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Negative form: after a delete/optimistic mutation, the item must be gone from the
|
|
172
|
+
// rendered page (no ghost/stale row).
|
|
173
|
+
export async function assertRemovedAfterMutation(page, locator, { timeout = 5000 } = {}) {
|
|
174
|
+
await locator.waitFor({ state: 'detached', timeout }).catch(() => {})
|
|
175
|
+
const count = await locator.count()
|
|
176
|
+
if (count !== 0) {
|
|
177
|
+
throw new Error(`presentation:perceivability (negative) — item still rendered after its mutation (stale/ghost): ${locator}`)
|
|
178
|
+
}
|
|
179
|
+
}
|