@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,117 @@
|
|
|
1
|
+
// Unit tests for the pure Perceivability cores (issue #196). Run: `node --test`.
|
|
2
|
+
// The Playwright wrappers (assertContrast / assertHiddenFromWrongRole /
|
|
3
|
+
// assertRemovedAfterMutation) need a real browser + @axe-core/playwright and are
|
|
4
|
+
// exercised downstream; the contrast math, axe classifier, and predicates are
|
|
5
|
+
// proven here without one.
|
|
6
|
+
|
|
7
|
+
import { test } from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
import {
|
|
10
|
+
relativeLuminance, contrastRatio, meetsContrast, classifyAxeContrast,
|
|
11
|
+
isEffectivelyHidden, isExemptFromContrast, assessElementContrast, AbstainError,
|
|
12
|
+
} from './presentation-perceivability.mjs'
|
|
13
|
+
|
|
14
|
+
const WHITE = [255, 255, 255]
|
|
15
|
+
const BLACK = [0, 0, 0]
|
|
16
|
+
|
|
17
|
+
// ---------- WCAG math ----------
|
|
18
|
+
|
|
19
|
+
test('relative luminance: white ~1, black 0', () => {
|
|
20
|
+
assert.ok(Math.abs(relativeLuminance(WHITE) - 1) < 1e-9)
|
|
21
|
+
assert.equal(relativeLuminance(BLACK), 0)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('contrast ratio: black on white is 21:1, white on white is 1:1', () => {
|
|
25
|
+
assert.ok(Math.abs(contrastRatio(BLACK, WHITE) - 21) < 1e-6)
|
|
26
|
+
assert.ok(Math.abs(contrastRatio(WHITE, WHITE) - 1) < 1e-9)
|
|
27
|
+
assert.equal(contrastRatio(BLACK, WHITE), contrastRatio(WHITE, BLACK)) // symmetric
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('meetsContrast: black text on white passes AA', () => {
|
|
31
|
+
const r = meetsContrast(BLACK, WHITE)
|
|
32
|
+
assert.equal(r.threshold, 4.5)
|
|
33
|
+
assert.equal(r.pass, true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('meetsContrast: a faint gray fails both text and non-text', () => {
|
|
37
|
+
const faint = [170, 170, 170] // ~2.3:1 on white
|
|
38
|
+
assert.equal(meetsContrast(faint, WHITE).pass, false)
|
|
39
|
+
assert.equal(meetsContrast(faint, WHITE, { nonText: true }).pass, false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('meetsContrast: threshold flips between text (4.5) and non-text (3.0)', () => {
|
|
43
|
+
const mid = [130, 130, 130] // ~3.8:1 on white — between 3 and 4.5
|
|
44
|
+
const asText = meetsContrast(mid, WHITE)
|
|
45
|
+
const asUi = meetsContrast(mid, WHITE, { nonText: true })
|
|
46
|
+
assert.equal(asText.threshold, 4.5)
|
|
47
|
+
assert.equal(asText.pass, false, 'fails as normal text')
|
|
48
|
+
assert.equal(asUi.threshold, 3)
|
|
49
|
+
assert.equal(asUi.pass, true, 'passes as a UI component / large text')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// ---------- axe classifier ----------
|
|
53
|
+
|
|
54
|
+
test('classifyAxeContrast: no findings → pass', () => {
|
|
55
|
+
assert.equal(classifyAxeContrast({ violations: [], incomplete: [] }).status, 'pass')
|
|
56
|
+
assert.equal(classifyAxeContrast({}).status, 'pass')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('classifyAxeContrast: a color-contrast violation → fail', () => {
|
|
60
|
+
const res = { violations: [{ id: 'color-contrast', nodes: [{ target: ['.btn'] }] }], incomplete: [] }
|
|
61
|
+
const c = classifyAxeContrast(res)
|
|
62
|
+
assert.equal(c.status, 'fail')
|
|
63
|
+
assert.equal(c.count, 1)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('classifyAxeContrast: incomplete (gradient/image) → abstain, not pass', () => {
|
|
67
|
+
const res = { violations: [], incomplete: [{ id: 'color-contrast', nodes: [{ target: ['.hero'] }] }] }
|
|
68
|
+
assert.equal(classifyAxeContrast(res).status, 'abstain')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('classifyAxeContrast: violation outranks incomplete; other rule ids ignored', () => {
|
|
72
|
+
const res = {
|
|
73
|
+
violations: [{ id: 'color-contrast', nodes: [{}] }, { id: 'label', nodes: [{}] }],
|
|
74
|
+
incomplete: [{ id: 'color-contrast', nodes: [{}] }],
|
|
75
|
+
}
|
|
76
|
+
const c = classifyAxeContrast(res)
|
|
77
|
+
assert.equal(c.status, 'fail')
|
|
78
|
+
assert.equal(c.count, 1, 'only color-contrast nodes counted')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// ---------- visibility / exemption predicates ----------
|
|
82
|
+
|
|
83
|
+
test('isEffectivelyHidden: catches display/visibility/opacity/aria/zero-size', () => {
|
|
84
|
+
assert.equal(isEffectivelyHidden({ style: { display: 'none' } }), true)
|
|
85
|
+
assert.equal(isEffectivelyHidden({ style: { visibility: 'hidden' } }), true)
|
|
86
|
+
assert.equal(isEffectivelyHidden({ style: { opacity: '0' } }), true)
|
|
87
|
+
assert.equal(isEffectivelyHidden({ ariaHidden: true }), true)
|
|
88
|
+
assert.equal(isEffectivelyHidden({ rect: { width: 0, height: 10 } }), true)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('isEffectivelyHidden: a normal visible element is not hidden', () => {
|
|
92
|
+
assert.equal(isEffectivelyHidden({ style: { display: 'block', visibility: 'visible', opacity: '1' }, rect: { width: 100, height: 40 } }), false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('isExemptFromContrast: disabled controls are exempt (WCAG), normal are not', () => {
|
|
96
|
+
assert.equal(isExemptFromContrast({ disabled: true }), true)
|
|
97
|
+
assert.equal(isExemptFromContrast({ ariaDisabled: true }), true)
|
|
98
|
+
assert.equal(isExemptFromContrast({}), false)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('assessElementContrast (manual/escalation path) composes exemption + threshold', () => {
|
|
102
|
+
// disabled → exempt, regardless of (bad) colors
|
|
103
|
+
assert.equal(assessElementContrast(WHITE, WHITE, { disabled: true }).status, 'exempt')
|
|
104
|
+
// enabled, good contrast → pass
|
|
105
|
+
assert.equal(assessElementContrast(BLACK, WHITE).status, 'pass')
|
|
106
|
+
// enabled, faint gray → fail, with ratio reported
|
|
107
|
+
const fail = assessElementContrast([170, 170, 170], WHITE)
|
|
108
|
+
assert.equal(fail.status, 'fail')
|
|
109
|
+
assert.ok(fail.ratio < fail.threshold)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('AbstainError is distinguishable from a hard failure', () => {
|
|
113
|
+
const e = new AbstainError('x', { count: 2 })
|
|
114
|
+
assert.equal(e.abstain, true)
|
|
115
|
+
assert.equal(e.name, 'AbstainError')
|
|
116
|
+
assert.ok(e instanceof Error)
|
|
117
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Presentation-axis Reachability & Affordance instrument for e2e-test (issue #197).
|
|
2
|
+
//
|
|
3
|
+
// "Can a user GET to a control and tell it's actionable" — plus the negative form,
|
|
4
|
+
// "things that must be blocked are blocked" (modal focus-trap, no click-through,
|
|
5
|
+
// wrong-role route not reachable). Tag specs `@level presentation:reachability`.
|
|
6
|
+
//
|
|
7
|
+
// Distinct from ensure-coverage's existing Route Reachability Protocol, which is
|
|
8
|
+
// GUARD-CHAIN reachability ("can the URL load past middleware/auth"). This is
|
|
9
|
+
// NAVIGATIONAL reachability ("can a user click their way here from an entry point").
|
|
10
|
+
// A route can be guard-reachable yet navigation-orphaned (nothing links to it).
|
|
11
|
+
//
|
|
12
|
+
// Affordance↔impeccable boundary: the DETERMINISTIC affordance checks live here
|
|
13
|
+
// (focus-indicator presence, ≥44px target size, nav-orphan). Holistic a11y
|
|
14
|
+
// resilience and the *aesthetics* of focus styling stay in the `impeccable` skill
|
|
15
|
+
// (its `harden` reference). This instrument proves the cue EXISTS; impeccable judges
|
|
16
|
+
// whether it's good. See impeccable/reference/harden.md.
|
|
17
|
+
|
|
18
|
+
// ---------- pure cores (no browser) ----------
|
|
19
|
+
|
|
20
|
+
// Navigational orphans = routes the app DEFINES (manifest) minus routes REACHABLE
|
|
21
|
+
// by crawling nav from an entry point. `ignore` suppresses intentionally
|
|
22
|
+
// unlinked-but-valid routes (deep-link targets, auth callbacks) by exact id or
|
|
23
|
+
// prefix ending in '*'.
|
|
24
|
+
export function findNavOrphans(manifestRoutes, reachableRoutes, { ignore = [] } = {}) {
|
|
25
|
+
const reachable = new Set(reachableRoutes)
|
|
26
|
+
const matchesIgnore = (r) => ignore.some((ig) => (ig.endsWith('*') ? r.startsWith(ig.slice(0, -1)) : r === ig))
|
|
27
|
+
return manifestRoutes.filter((r) => !reachable.has(r) && !matchesIgnore(r))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Minimum interactive target size (WCAG 2.5.5 / 2.5.8). Returns pass + which
|
|
31
|
+
// dimension is short. Default 44px (24px is the relaxed AA floor — pass min).
|
|
32
|
+
export function meetsTargetSize({ width = 0, height = 0 } = {}, min = 44) {
|
|
33
|
+
const short = []
|
|
34
|
+
if (width < min) short.push('width')
|
|
35
|
+
if (height < min) short.push('height')
|
|
36
|
+
return { pass: short.length === 0, min, width, height, short }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A focusable control must show a VISIBLE focus indicator: focusing it must change
|
|
40
|
+
// the outline or box-shadow vs its blurred state. Pure comparison of the two
|
|
41
|
+
// computed-style snapshots; absence of any change == no visible focus ring.
|
|
42
|
+
export function hasVisibleFocusIndicator(blurred = {}, focused = {}) {
|
|
43
|
+
const noOutline = (s) =>
|
|
44
|
+
!s.outlineStyle || s.outlineStyle === 'none' || parseFloat(s.outlineWidth || '0') === 0
|
|
45
|
+
// An outline change only counts if the FOCUSED outline is actually visible — a
|
|
46
|
+
// 0px-width color-only change (e.g. `0px solid red` → `0px solid blue`) paints
|
|
47
|
+
// nothing and must not pass.
|
|
48
|
+
const focusedHasOutline = !noOutline(focused)
|
|
49
|
+
const outlineVisibleAndChanged = focusedHasOutline && (
|
|
50
|
+
noOutline(blurred) ||
|
|
51
|
+
(blurred.outlineColor || '') !== (focused.outlineColor || '') ||
|
|
52
|
+
(blurred.outlineWidth || '') !== (focused.outlineWidth || '') ||
|
|
53
|
+
(blurred.outlineStyle || '') !== (focused.outlineStyle || '')
|
|
54
|
+
)
|
|
55
|
+
const shadowChanged = (blurred.boxShadow || 'none') !== (focused.boxShadow || 'none')
|
|
56
|
+
return Boolean(outlineVisibleAndChanged || shadowChanged)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------- Playwright wrappers (need a real browser) ----------
|
|
60
|
+
|
|
61
|
+
// BFS-crawl the navigational graph from `start`, clicking same-origin links the
|
|
62
|
+
// rendered nav exposes, and return the set of reachable pathnames. Drives a real
|
|
63
|
+
// browser so JS-rendered / hover/scroll-revealed nav is exercised. Seed an
|
|
64
|
+
// authenticated context for guarded nav.
|
|
65
|
+
export async function crawlReachableRoutes(page, { start = '/', maxPages = 200 } = {}) {
|
|
66
|
+
const origin = new URL(start, page.url() || 'http://localhost').origin
|
|
67
|
+
const seen = new Set()
|
|
68
|
+
const queue = [new URL(start, origin).pathname]
|
|
69
|
+
while (queue.length && seen.size < maxPages) {
|
|
70
|
+
const path = queue.shift()
|
|
71
|
+
if (seen.has(path)) continue
|
|
72
|
+
seen.add(path)
|
|
73
|
+
await page.goto(new URL(path, origin).href).catch(() => {})
|
|
74
|
+
const hrefs = await page.$$eval('a[href]', (as) => as.map((a) => a.getAttribute('href') || ''))
|
|
75
|
+
for (const h of hrefs) {
|
|
76
|
+
let u
|
|
77
|
+
try { u = new URL(h, origin) } catch { continue }
|
|
78
|
+
if (u.origin !== origin) continue
|
|
79
|
+
if (!seen.has(u.pathname)) queue.push(u.pathname)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...seen]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Assert no navigational orphans: every manifest route is reachable by crawling.
|
|
86
|
+
export async function assertNoNavOrphans(page, manifestRoutes, { start = '/', ignore = [] } = {}) {
|
|
87
|
+
const reachable = await crawlReachableRoutes(page, { start })
|
|
88
|
+
const orphans = findNavOrphans(manifestRoutes, reachable, { ignore })
|
|
89
|
+
if (orphans.length) {
|
|
90
|
+
throw new Error(`presentation:reachability — ${orphans.length} route(s) reachable by URL but linked from no nav:\n${orphans.map((o) => ' ' + o).join('\n')}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Assert every matched interactive control meets the minimum target size.
|
|
95
|
+
export async function assertTargetSizes(page, selector = 'a, button, [role="button"], input, select', { min = 44 } = {}) {
|
|
96
|
+
const boxes = await page.$$eval(selector, (els) => els
|
|
97
|
+
.filter((el) => el.offsetParent !== null)
|
|
98
|
+
.map((el) => { const r = el.getBoundingClientRect(); return { tag: el.tagName.toLowerCase(), id: el.id, width: r.width, height: r.height } }))
|
|
99
|
+
const small = boxes.map((b) => ({ ...b, ...meetsTargetSize(b, min) })).filter((b) => !b.pass)
|
|
100
|
+
if (small.length) {
|
|
101
|
+
throw new Error(`presentation:reachability — ${small.length} control(s) below ${min}px target:\n${small.map((s) => ` ${s.tag}${s.id ? '#' + s.id : ''} ${Math.round(s.width)}x${Math.round(s.height)}`).join('\n')}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Assert a control shows a visible focus indicator when focused.
|
|
106
|
+
export async function assertFocusVisible(page, locator) {
|
|
107
|
+
const read = () => locator.evaluate((el) => {
|
|
108
|
+
const s = getComputedStyle(el)
|
|
109
|
+
return { outlineStyle: s.outlineStyle, outlineWidth: s.outlineWidth, outlineColor: s.outlineColor, boxShadow: s.boxShadow }
|
|
110
|
+
})
|
|
111
|
+
const blurred = await read()
|
|
112
|
+
await locator.focus()
|
|
113
|
+
const focused = await read()
|
|
114
|
+
if (!hasVisibleFocusIndicator(blurred, focused)) {
|
|
115
|
+
throw new Error(`presentation:reachability (affordance) — control shows no visible focus indicator on focus: ${locator}`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Negative form: a modal must trap focus — tabbing never moves focus to an element
|
|
120
|
+
// outside the modal (no escape to the background). Containment is checked on the
|
|
121
|
+
// modal element the locator resolves to (no fragile re-derived global selector).
|
|
122
|
+
export async function assertModalTrapsFocus(page, modalLocator, { tabs = 12 } = {}) {
|
|
123
|
+
for (let i = 0; i < tabs; i++) {
|
|
124
|
+
await page.keyboard.press('Tab')
|
|
125
|
+
const inside = await modalLocator.evaluate((modal) => modal.contains(document.activeElement))
|
|
126
|
+
if (!inside) {
|
|
127
|
+
throw new Error('presentation:reachability (negative) — focus escaped the modal; focus is not trapped')
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Negative form: with a modal open, a background element must not be interactable
|
|
133
|
+
// (no click-through). Asserts the background is `inert`, `aria-hidden`, or covered
|
|
134
|
+
// (pointer-events:none) — the standard ways an open dialog neutralizes the page.
|
|
135
|
+
export async function assertNoBackgroundClickThrough(page, backgroundLocator) {
|
|
136
|
+
const interactable = await backgroundLocator.evaluate((el) => {
|
|
137
|
+
if (el.inert) return false
|
|
138
|
+
if (el.getAttribute('aria-hidden') === 'true') return false
|
|
139
|
+
if (getComputedStyle(el).pointerEvents === 'none') return false
|
|
140
|
+
return true // still interactable behind the modal — a click-through leak
|
|
141
|
+
})
|
|
142
|
+
if (interactable) {
|
|
143
|
+
throw new Error('presentation:reachability (negative) — background is still interactable behind the modal (click-through); expected inert / aria-hidden / pointer-events:none')
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Negative form: a privilege-gated route must NOT be navigationally reachable for a
|
|
148
|
+
// user lacking the role (it must not appear in the crawl set).
|
|
149
|
+
export async function assertRouteNotReachable(page, route, { start = '/' } = {}) {
|
|
150
|
+
const reachable = await crawlReachableRoutes(page, { start })
|
|
151
|
+
if (reachable.includes(route)) {
|
|
152
|
+
throw new Error(`presentation:reachability (negative) — privilege-gated route is reachable via nav for the wrong role: ${route}`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Unit tests for the pure Reachability & Affordance cores (issue #197).
|
|
2
|
+
// Run: `node --test`. The Playwright wrappers (crawlReachableRoutes,
|
|
3
|
+
// assertNoNavOrphans, assertTargetSizes, assertFocusVisible, assertModalTrapsFocus,
|
|
4
|
+
// assertRouteNotReachable) need a real browser and are exercised downstream.
|
|
5
|
+
|
|
6
|
+
import { test } from 'node:test'
|
|
7
|
+
import assert from 'node:assert/strict'
|
|
8
|
+
import { findNavOrphans, meetsTargetSize, hasVisibleFocusIndicator } from './presentation-reachability.mjs'
|
|
9
|
+
|
|
10
|
+
// ---------- nav-orphan set diff ----------
|
|
11
|
+
|
|
12
|
+
test('findNavOrphans: routes in manifest but not reachable are orphans', () => {
|
|
13
|
+
const manifest = ['/', '/dashboard', '/settings', '/secret-report']
|
|
14
|
+
const reachable = ['/', '/dashboard', '/settings']
|
|
15
|
+
assert.deepEqual(findNavOrphans(manifest, reachable), ['/secret-report'])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('findNavOrphans: none when everything is reachable', () => {
|
|
19
|
+
assert.deepEqual(findNavOrphans(['/', '/a'], ['/', '/a', '/extra']), [])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('findNavOrphans: ignore by exact id and by prefix glob', () => {
|
|
23
|
+
const manifest = ['/', '/auth/callback', '/admin/secret', '/deep/x', '/deep/y']
|
|
24
|
+
const reachable = ['/']
|
|
25
|
+
// exact ignore for the callback, prefix ignore for everything under /deep
|
|
26
|
+
const orphans = findNavOrphans(manifest, reachable, { ignore: ['/auth/callback', '/deep/*'] })
|
|
27
|
+
assert.deepEqual(orphans, ['/admin/secret'])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// ---------- target size ----------
|
|
31
|
+
|
|
32
|
+
test('meetsTargetSize: 44x44 passes, smaller fails with the short dimension', () => {
|
|
33
|
+
assert.equal(meetsTargetSize({ width: 44, height: 44 }).pass, true)
|
|
34
|
+
const r = meetsTargetSize({ width: 30, height: 44 })
|
|
35
|
+
assert.equal(r.pass, false)
|
|
36
|
+
assert.deepEqual(r.short, ['width'])
|
|
37
|
+
assert.deepEqual(meetsTargetSize({ width: 10, height: 10 }).short, ['width', 'height'])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('meetsTargetSize: custom minimum', () => {
|
|
41
|
+
assert.equal(meetsTargetSize({ width: 24, height: 24 }, 24).pass, true)
|
|
42
|
+
assert.equal(meetsTargetSize({ width: 24, height: 24 }, 44).pass, false)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// ---------- focus indicator ----------
|
|
46
|
+
|
|
47
|
+
const NO_OUTLINE = { outlineStyle: 'none', outlineWidth: '0px', outlineColor: 'rgb(0,0,0)', boxShadow: 'none' }
|
|
48
|
+
|
|
49
|
+
test('hasVisibleFocusIndicator: outline appearing on focus → visible', () => {
|
|
50
|
+
const focused = { outlineStyle: 'solid', outlineWidth: '2px', outlineColor: 'rgb(0,90,255)', boxShadow: 'none' }
|
|
51
|
+
assert.equal(hasVisibleFocusIndicator(NO_OUTLINE, focused), true)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('hasVisibleFocusIndicator: box-shadow ring appearing on focus → visible', () => {
|
|
55
|
+
const focused = { ...NO_OUTLINE, boxShadow: '0 0 0 3px rgba(0,90,255,.5)' }
|
|
56
|
+
assert.equal(hasVisibleFocusIndicator(NO_OUTLINE, focused), true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('hasVisibleFocusIndicator: no change between states → NOT visible (the bug)', () => {
|
|
60
|
+
// outline:none in both states and identical shadow — the missing-focus-ring case
|
|
61
|
+
assert.equal(hasVisibleFocusIndicator(NO_OUTLINE, { ...NO_OUTLINE }), false)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('hasVisibleFocusIndicator: zero-width color-only outline change → NOT visible', () => {
|
|
65
|
+
// `0px solid red` → `0px solid blue` paints nothing; must not false-pass
|
|
66
|
+
const blurred = { outlineStyle: 'solid', outlineWidth: '0px', outlineColor: 'rgb(255,0,0)', boxShadow: 'none' }
|
|
67
|
+
const focused = { outlineStyle: 'solid', outlineWidth: '0px', outlineColor: 'rgb(0,0,255)', boxShadow: 'none' }
|
|
68
|
+
assert.equal(hasVisibleFocusIndicator(blurred, focused), false)
|
|
69
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Presentation-axis SPINE instrument for e2e-test: render-health.
|
|
2
|
+
//
|
|
3
|
+
// The cheapest, most foundational presentation check — does the page render
|
|
4
|
+
// without a `pageerror` (uncaught exception), a `console.error`, a rendered error
|
|
5
|
+
// boundary, or an HTTP >= 500? An uncaught TypeError on mount fails here before
|
|
6
|
+
// any layout question is asked. This is the check that catches the class of bug a
|
|
7
|
+
// green functional journey sails past when its only test of the route intercepts
|
|
8
|
+
// the route's own API (the journey never exercises the real render path).
|
|
9
|
+
//
|
|
10
|
+
// Import into a Playwright spec and tag it `@covers <route>` /
|
|
11
|
+
// `@level presentation:render-health` so the Coverage Ledger resolves it. Drives a
|
|
12
|
+
// REAL browser — a DOM-only / JSDOM equivalent cannot observe a pageerror on mount
|
|
13
|
+
// and is graded `Over-mocked` by the ledger. Mirrors the contract in ensure-coverage
|
|
14
|
+
// references/presentation-sweep.md (result shape).
|
|
15
|
+
|
|
16
|
+
// ---- pure, unit-testable core (no browser) ----
|
|
17
|
+
|
|
18
|
+
// Default substrings that mark a console message or error as a framework/error-
|
|
19
|
+
// boundary render failure regardless of source. Repos extend via `extraFatal`.
|
|
20
|
+
export const DEFAULT_FATAL_MARKERS = [
|
|
21
|
+
'Application error: a client-side exception',
|
|
22
|
+
'Application error: a server-side exception',
|
|
23
|
+
'Unhandled Runtime Error',
|
|
24
|
+
'Cannot read properties of undefined',
|
|
25
|
+
'Cannot read properties of null',
|
|
26
|
+
'is not a function',
|
|
27
|
+
'Hydration failed',
|
|
28
|
+
'There was an error while hydrating',
|
|
29
|
+
'Minified React error',
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
// Decide whether a single captured console message is fatal. `type` is the console
|
|
33
|
+
// level ('error' | 'warning' | …). console.error is fatal by default; warnings are
|
|
34
|
+
// not. `ignore` suppresses known-benign messages by substring (curate sparingly).
|
|
35
|
+
export function isFatalConsole(type, text, { ignore = [] } = {}) {
|
|
36
|
+
if (type !== 'error') return false
|
|
37
|
+
if (ignore.some((ig) => text.includes(ig))) return false
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A response is fatal if the document or any first-party request returns >= 500.
|
|
42
|
+
// Cross-origin (external) responses are ignored — externals are faked, not graded.
|
|
43
|
+
export function isFatalResponse(status, url, { origin } = {}) {
|
|
44
|
+
if (status < 500) return false
|
|
45
|
+
if (origin && !url.startsWith(origin) && !url.startsWith('/')) return false
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Does any captured text match an error-boundary / framework-error marker?
|
|
50
|
+
export function hasFatalMarker(text, { extraFatal = [] } = {}) {
|
|
51
|
+
const markers = [...DEFAULT_FATAL_MARKERS, ...extraFatal]
|
|
52
|
+
return markers.some((m) => text.includes(m))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Reduce a batch of captured signals into a verdict. Pure — the Playwright wrapper
|
|
56
|
+
// collects `signals` and delegates here so the decision logic is testable headless.
|
|
57
|
+
// signals: { pageerrors:[{message}], consoles:[{type,text}], responses:[{status,url}], bodyText? }
|
|
58
|
+
export function evaluateRenderHealth(signals = {}, opts = {}) {
|
|
59
|
+
const { ignore = [], extraFatal = [], origin } = opts
|
|
60
|
+
const offenders = []
|
|
61
|
+
for (const e of signals.pageerrors || []) {
|
|
62
|
+
offenders.push({ kind: 'pageerror', detail: e.message || String(e) })
|
|
63
|
+
}
|
|
64
|
+
for (const c of signals.consoles || []) {
|
|
65
|
+
if (isFatalConsole(c.type, c.text || '', { ignore })) {
|
|
66
|
+
offenders.push({ kind: 'console.error', detail: c.text })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const r of signals.responses || []) {
|
|
70
|
+
if (isFatalResponse(r.status, r.url || '', { origin })) {
|
|
71
|
+
offenders.push({ kind: 'http>=500', detail: `${r.status} ${r.url}` })
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (signals.bodyText && hasFatalMarker(signals.bodyText, { extraFatal })) {
|
|
75
|
+
offenders.push({ kind: 'error-boundary', detail: 'error overlay / boundary text present in DOM' })
|
|
76
|
+
}
|
|
77
|
+
return { healthy: offenders.length === 0, offenders }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- browser wrapper (Playwright) ----
|
|
81
|
+
|
|
82
|
+
// Attach listeners BEFORE navigation, then drive: returns a handle whose
|
|
83
|
+
// `.assert()` throws with the concrete offender(s) if the page did not render
|
|
84
|
+
// healthy. Usage:
|
|
85
|
+
// const health = watchRenderHealth(page, { origin: baseURL })
|
|
86
|
+
// await page.goto('/tasks')
|
|
87
|
+
// await page.waitForLoadState('networkidle').catch(() => {})
|
|
88
|
+
// await health.assert()
|
|
89
|
+
export function watchRenderHealth(page, opts = {}) {
|
|
90
|
+
const signals = { pageerrors: [], consoles: [], responses: [] }
|
|
91
|
+
page.on('pageerror', (err) => signals.pageerrors.push({ message: err?.message || String(err) }))
|
|
92
|
+
page.on('console', (msg) => signals.consoles.push({ type: msg.type(), text: msg.text() }))
|
|
93
|
+
page.on('response', (res) => signals.responses.push({ status: res.status(), url: res.url() }))
|
|
94
|
+
return {
|
|
95
|
+
signals,
|
|
96
|
+
async evaluate() {
|
|
97
|
+
// Pull rendered body text so an error boundary that swallowed the throw is caught too.
|
|
98
|
+
try { signals.bodyText = await page.evaluate(() => document.body?.innerText || '') } catch { /* page gone */ }
|
|
99
|
+
return evaluateRenderHealth(signals, opts)
|
|
100
|
+
},
|
|
101
|
+
async assert() {
|
|
102
|
+
const { healthy, offenders } = await this.evaluate()
|
|
103
|
+
if (!healthy) {
|
|
104
|
+
const lines = offenders.map((o) => ` - [${o.kind}] ${o.detail}`).join('\n')
|
|
105
|
+
throw new Error(`presentation:render-health — page did not render cleanly:\n${lines}`)
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Continuous-invariant form: render-health must hold not only at LOAD but THROUGH an
|
|
112
|
+
// interaction — a `presentation:temporal` trigger, a `presentation:perceivability`
|
|
113
|
+
// post-delete mutation, or a mutation-form journey's submit. Attach the watcher,
|
|
114
|
+
// run the interaction, then assert nothing threw during it. COMPOSE with the other
|
|
115
|
+
// presentation instruments rather than re-driving the form:
|
|
116
|
+
//
|
|
117
|
+
// await assertHealthyThrough(page, async () => {
|
|
118
|
+
// await assertRemovedAfterMutation(page, page.getByRole('row', { name: 'X' }))
|
|
119
|
+
// }, { origin: baseURL })
|
|
120
|
+
//
|
|
121
|
+
// This is "mutate-health": render-health × the trigger/mutation pattern temporal and
|
|
122
|
+
// perceivability already use. The durable-state assertion (depth) and the visible
|
|
123
|
+
// post-state assertion (perceivability) stay as-is; this adds "…and nothing threw
|
|
124
|
+
// while it happened." Reuse one handle across several interactions for full lifecycle
|
|
125
|
+
// coverage, or call this per interaction.
|
|
126
|
+
export async function assertHealthyThrough(page, action, opts = {}) {
|
|
127
|
+
const health = watchRenderHealth(page, opts)
|
|
128
|
+
await action()
|
|
129
|
+
await health.assert()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// One-shot convenience: navigate and assert render-health in a single call.
|
|
133
|
+
export async function assertRenderHealthy(page, url, opts = {}) {
|
|
134
|
+
const health = watchRenderHealth(page, opts)
|
|
135
|
+
const res = await page.goto(url, { waitUntil: 'domcontentloaded' })
|
|
136
|
+
await page.waitForLoadState('networkidle').catch(() => {})
|
|
137
|
+
if (res && isFatalResponse(res.status(), res.url(), { origin: opts.origin })) {
|
|
138
|
+
throw new Error(`presentation:render-health — document returned ${res.status()} for ${url}`)
|
|
139
|
+
}
|
|
140
|
+
await health.assert()
|
|
141
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Unit tests for the render-health spine — the pure core, no browser.
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import assert from 'node:assert/strict'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
isFatalConsole,
|
|
7
|
+
isFatalResponse,
|
|
8
|
+
hasFatalMarker,
|
|
9
|
+
evaluateRenderHealth,
|
|
10
|
+
assertHealthyThrough,
|
|
11
|
+
} from './presentation-render-health.mjs'
|
|
12
|
+
|
|
13
|
+
// Minimal fake Playwright page: records listeners, lets a test emit events, and
|
|
14
|
+
// returns body text from evaluate(). Enough to exercise the watcher/compose path
|
|
15
|
+
// without a browser.
|
|
16
|
+
function fakePage(bodyText = '') {
|
|
17
|
+
const handlers = {}
|
|
18
|
+
return {
|
|
19
|
+
on(ev, fn) { (handlers[ev] ||= []).push(fn) },
|
|
20
|
+
emit(ev, arg) { (handlers[ev] || []).forEach((f) => f(arg)) },
|
|
21
|
+
async evaluate() { return bodyText },
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('console.error is fatal; warning is not', () => {
|
|
26
|
+
assert.equal(isFatalConsole('error', 'boom'), true)
|
|
27
|
+
assert.equal(isFatalConsole('warning', 'meh'), false)
|
|
28
|
+
assert.equal(isFatalConsole('log', 'hi'), false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('ignored console substrings are not fatal', () => {
|
|
32
|
+
assert.equal(isFatalConsole('error', 'Download the React DevTools', { ignore: ['React DevTools'] }), false)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('http >= 500 is fatal; < 500 is not', () => {
|
|
36
|
+
assert.equal(isFatalResponse(500, '/api/x'), true)
|
|
37
|
+
assert.equal(isFatalResponse(503, '/api/x'), true)
|
|
38
|
+
assert.equal(isFatalResponse(404, '/api/x'), false)
|
|
39
|
+
assert.equal(isFatalResponse(200, '/api/x'), false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('cross-origin 5xx is ignored (externals are faked, not graded)', () => {
|
|
43
|
+
assert.equal(isFatalResponse(500, 'https://stripe.com/v1/x', { origin: 'http://localhost:3000' }), false)
|
|
44
|
+
assert.equal(isFatalResponse(500, 'http://localhost:3000/api/x', { origin: 'http://localhost:3000' }), true)
|
|
45
|
+
assert.equal(isFatalResponse(500, '/api/x', { origin: 'http://localhost:3000' }), true) // relative = first-party
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('error-boundary markers are detected in body text', () => {
|
|
49
|
+
assert.equal(hasFatalMarker('Application error: a client-side exception has occurred'), true)
|
|
50
|
+
assert.equal(hasFatalMarker("Cannot read properties of undefined (reading 'id')"), true)
|
|
51
|
+
assert.equal(hasFatalMarker('All good here'), false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('the /tasks bug shape fails render-health (pageerror on mount)', () => {
|
|
55
|
+
const v = evaluateRenderHealth({
|
|
56
|
+
pageerrors: [{ message: "Cannot read properties of undefined (reading 'id')" }],
|
|
57
|
+
})
|
|
58
|
+
assert.equal(v.healthy, false)
|
|
59
|
+
assert.equal(v.offenders[0].kind, 'pageerror')
|
|
60
|
+
assert.match(v.offenders[0].detail, /reading 'id'/)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a clean render is healthy', () => {
|
|
64
|
+
const v = evaluateRenderHealth({
|
|
65
|
+
consoles: [{ type: 'log', text: 'mounted' }, { type: 'warning', text: 'deprecated' }],
|
|
66
|
+
responses: [{ status: 200, url: '/api/tasks/list' }],
|
|
67
|
+
bodyText: 'Tasks\nNo tasks match these filters.',
|
|
68
|
+
})
|
|
69
|
+
assert.equal(v.healthy, true)
|
|
70
|
+
assert.deepEqual(v.offenders, [])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('assertHealthyThrough fails when an error fires DURING the interaction (mutate-health)', async () => {
|
|
74
|
+
const page = fakePage()
|
|
75
|
+
await assert.rejects(
|
|
76
|
+
assertHealthyThrough(page, async () => {
|
|
77
|
+
// simulate an uncaught error thrown while a delete/submit runs
|
|
78
|
+
page.emit('pageerror', { message: "Cannot read properties of undefined (reading 'id')" })
|
|
79
|
+
}),
|
|
80
|
+
/render-health.*reading 'id'/s,
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('assertHealthyThrough passes a clean interaction', async () => {
|
|
85
|
+
const page = fakePage('Tasks\nDeleted.')
|
|
86
|
+
await assertHealthyThrough(page, async () => { /* clean mutation */ })
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('aggregates multiple offenders (pageerror + console + 5xx + boundary)', () => {
|
|
90
|
+
const v = evaluateRenderHealth({
|
|
91
|
+
pageerrors: [{ message: 'x is not a function' }],
|
|
92
|
+
consoles: [{ type: 'error', text: 'render failed' }],
|
|
93
|
+
responses: [{ status: 500, url: '/api/tasks/list' }],
|
|
94
|
+
bodyText: 'Unhandled Runtime Error',
|
|
95
|
+
}, { origin: 'http://localhost:3000' })
|
|
96
|
+
assert.equal(v.healthy, false)
|
|
97
|
+
assert.deepEqual(v.offenders.map((o) => o.kind).sort(), ['console.error', 'error-boundary', 'http>=500', 'pageerror'])
|
|
98
|
+
})
|