voidmachine 0.0.0 → 4.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/LICENSE +21 -0
- package/README.md +123 -4
- package/bin/void-harness.mjs +12 -0
- package/bin/void-machine.mjs +12 -0
- package/core-assets/.claude-plugin/plugin.json +253 -0
- package/core-assets/.gitkeep +0 -0
- package/core-assets/PHILOSOPHY.md +218 -0
- package/core-assets/PROJECT-DOCTRINE.template.md +14 -0
- package/core-assets/adapters/security/manifest.yaml +96 -0
- package/core-assets/agents/.gitkeep +0 -0
- package/core-assets/agents/accessibility-specialist.md +59 -0
- package/core-assets/agents/api-integration-engineer.md +59 -0
- package/core-assets/agents/code-explorer.md +94 -0
- package/core-assets/agents/code-explorer.source +7 -0
- package/core-assets/agents/data-migration-engineer.md +59 -0
- package/core-assets/agents/devex-docs-engineer.md +57 -0
- package/core-assets/agents/doctrine-critic.md +91 -0
- package/core-assets/agents/doctrine-critic.source +11 -0
- package/core-assets/agents/domain-architect.md +56 -0
- package/core-assets/agents/experience-designer.md +59 -0
- package/core-assets/agents/frontend-engineer.md +60 -0
- package/core-assets/agents/independent-code-reviewer.md +61 -0
- package/core-assets/agents/migration-planner.md +97 -0
- package/core-assets/agents/migration-planner.source +7 -0
- package/core-assets/agents/observability-sre-engineer.md +59 -0
- package/core-assets/agents/pdf-specialist.md +58 -0
- package/core-assets/agents/performance-engineer.md +58 -0
- package/core-assets/agents/product-challenger.md +55 -0
- package/core-assets/agents/security-engineer.md +66 -0
- package/core-assets/agents/silent-failure-hunter.md +94 -0
- package/core-assets/agents/silent-failure-hunter.source +8 -0
- package/core-assets/agents/solution-architect.md +57 -0
- package/core-assets/agents/test-qa-engineer.md +60 -0
- package/core-assets/agents/type-design-analyzer.md +93 -0
- package/core-assets/agents/type-design-analyzer.source +8 -0
- package/core-assets/agents/visual-craft-director.md +62 -0
- package/core-assets/codex/hooks.json +157 -0
- package/core-assets/contract-history/visual-craft-director/v2.yaml +43 -0
- package/core-assets/data/catalog.v3.json +10479 -0
- package/core-assets/data/certification.json +1800 -0
- package/core-assets/data/identity.json +8 -0
- package/core-assets/data/model.json +4226 -0
- package/core-assets/enforce/ci-enforce.sh +183 -0
- package/core-assets/hooks/_checks.sh +145 -0
- package/core-assets/hooks/_hooklib.sh +163 -0
- package/core-assets/hooks/_syntax-worker.cjs +503 -0
- package/core-assets/hooks/_void-hook.mjs +5459 -0
- package/core-assets/hooks/activation-meter.sh +13 -0
- package/core-assets/hooks/anti-ai-slop-grep.sh +38 -0
- package/core-assets/hooks/auto-format.sh +9 -0
- package/core-assets/hooks/axe-precommit.sh +35 -0
- package/core-assets/hooks/block-dangerous-bash.sh +10 -0
- package/core-assets/hooks/block-protected-push.sh +81 -0
- package/core-assets/hooks/boundary-direction-check.sh +10 -0
- package/core-assets/hooks/control-character-grep.sh +10 -0
- package/core-assets/hooks/fowler-refactors.txt +71 -0
- package/core-assets/hooks/large-cl-grep.sh +9 -0
- package/core-assets/hooks/llm-cost-precommit.sh +55 -0
- package/core-assets/hooks/migration-lint.sh +58 -0
- package/core-assets/hooks/migration-pr-template.sh +55 -0
- package/core-assets/hooks/no-ai-design-slop.sh +10 -0
- package/core-assets/hooks/no-any-grep.sh +10 -0
- package/core-assets/hooks/no-as-cast-grep.sh +10 -0
- package/core-assets/hooks/no-console-log-grep.sh +10 -0
- package/core-assets/hooks/no-null-grep.sh +10 -0
- package/core-assets/hooks/no-only-no-skip.sh +10 -0
- package/core-assets/hooks/outcome-meter.sh +13 -0
- package/core-assets/hooks/pre-PR-review-evidence.sh +47 -0
- package/core-assets/hooks/protect-sensitive-files.sh +10 -0
- package/core-assets/hooks/refactor-named-grep.sh +49 -0
- package/core-assets/hooks/secret-in-content.sh +10 -0
- package/core-assets/hooks/sessionstart-context.sh +9 -0
- package/core-assets/hooks/stop-typecheck.sh +9 -0
- package/core-assets/hooks/tdd-guard.md +90 -0
- package/core-assets/hooks/tdd-guard.sh +10 -0
- package/core-assets/hooks/test-name-lint.sh +10 -0
- package/core-assets/hooks/tidying-commit-prefix.sh +37 -0
- package/core-assets/hooks/trim-large-output.sh +9 -0
- package/core-assets/hooks/tsc-noemit-precommit.sh +22 -0
- package/core-assets/hooks/ubiquitous-language-lint.sh +58 -0
- package/core-assets/hooks/viewport-screenshot-gate.sh +48 -0
- package/core-assets/modules/.gitkeep +0 -0
- package/core-assets/packs/pack-mobile/skills/void-eas-build-profile/.source +7 -0
- package/core-assets/packs/pack-mobile/skills/void-eas-build-profile/SKILL.md +164 -0
- package/core-assets/packs/pack-mobile/skills/void-eas-build-profile/harness.yaml +14 -0
- package/core-assets/packs/pack-mobile/skills/void-expo-config-plugins/.source +7 -0
- package/core-assets/packs/pack-mobile/skills/void-expo-config-plugins/SKILL.md +178 -0
- package/core-assets/packs/pack-mobile/skills/void-expo-config-plugins/harness.yaml +14 -0
- package/core-assets/packs/pack-mobile/skills/void-expo-router-pattern/.source +8 -0
- package/core-assets/packs/pack-mobile/skills/void-expo-router-pattern/SKILL.md +209 -0
- package/core-assets/packs/pack-mobile/skills/void-expo-router-pattern/harness.yaml +14 -0
- package/core-assets/packs/pack-mobile/skills/void-ota-update-strategy/.source +7 -0
- package/core-assets/packs/pack-mobile/skills/void-ota-update-strategy/SKILL.md +194 -0
- package/core-assets/packs/pack-mobile/skills/void-ota-update-strategy/harness.yaml +14 -0
- package/core-assets/packs/pack-monorepo/skills/void-dependency-direction/.source +5 -0
- package/core-assets/packs/pack-monorepo/skills/void-dependency-direction/SKILL.md +160 -0
- package/core-assets/packs/pack-monorepo/skills/void-dependency-direction/harness.yaml +14 -0
- package/core-assets/packs/pack-monorepo/skills/void-package-extraction/.source +5 -0
- package/core-assets/packs/pack-monorepo/skills/void-package-extraction/SKILL.md +70 -0
- package/core-assets/packs/pack-monorepo/skills/void-package-extraction/harness.yaml +14 -0
- package/core-assets/packs/pack-monorepo/skills/void-service-package/.source +5 -0
- package/core-assets/packs/pack-monorepo/skills/void-service-package/SKILL.md +122 -0
- package/core-assets/packs/pack-monorepo/skills/void-service-package/harness.yaml +14 -0
- package/core-assets/packs/pack-monorepo/skills/void-turbo-pipeline-tuning/.source +7 -0
- package/core-assets/packs/pack-monorepo/skills/void-turbo-pipeline-tuning/SKILL.md +116 -0
- package/core-assets/packs/pack-monorepo/skills/void-turbo-pipeline-tuning/harness.yaml +14 -0
- package/core-assets/packs/pack-nextjs/skills/void-cache-component-pattern/.source +5 -0
- package/core-assets/packs/pack-nextjs/skills/void-cache-component-pattern/SKILL.md +162 -0
- package/core-assets/packs/pack-nextjs/skills/void-cache-component-pattern/harness.yaml +14 -0
- package/core-assets/packs/pack-nextjs/skills/void-instrumentation-setup/.source +5 -0
- package/core-assets/packs/pack-nextjs/skills/void-instrumentation-setup/SKILL.md +161 -0
- package/core-assets/packs/pack-nextjs/skills/void-instrumentation-setup/harness.yaml +14 -0
- package/core-assets/packs/pack-nextjs/skills/void-loading-error-boundaries/.source +5 -0
- package/core-assets/packs/pack-nextjs/skills/void-loading-error-boundaries/SKILL.md +176 -0
- package/core-assets/packs/pack-nextjs/skills/void-loading-error-boundaries/harness.yaml +14 -0
- package/core-assets/packs/pack-nextjs/skills/void-parallel-routes-slots/.source +5 -0
- package/core-assets/packs/pack-nextjs/skills/void-parallel-routes-slots/SKILL.md +129 -0
- package/core-assets/packs/pack-nextjs/skills/void-parallel-routes-slots/harness.yaml +14 -0
- package/core-assets/packs/pack-nextjs/skills/void-route-group-decision/.source +5 -0
- package/core-assets/packs/pack-nextjs/skills/void-route-group-decision/SKILL.md +107 -0
- package/core-assets/packs/pack-nextjs/skills/void-route-group-decision/harness.yaml +14 -0
- package/core-assets/packs/pack-pwa/skills/void-install-prompt-ux/.source +5 -0
- package/core-assets/packs/pack-pwa/skills/void-install-prompt-ux/SKILL.md +153 -0
- package/core-assets/packs/pack-pwa/skills/void-install-prompt-ux/harness.yaml +14 -0
- package/core-assets/packs/pack-pwa/skills/void-manifest-checklist/.source +5 -0
- package/core-assets/packs/pack-pwa/skills/void-manifest-checklist/SKILL.md +204 -0
- package/core-assets/packs/pack-pwa/skills/void-manifest-checklist/harness.yaml +14 -0
- package/core-assets/packs/pack-pwa/skills/void-offline-first-mutation/.source +8 -0
- package/core-assets/packs/pack-pwa/skills/void-offline-first-mutation/SKILL.md +266 -0
- package/core-assets/packs/pack-pwa/skills/void-offline-first-mutation/harness.yaml +14 -0
- package/core-assets/packs/pack-pwa/skills/void-service-worker-strategy/.source +5 -0
- package/core-assets/packs/pack-pwa/skills/void-service-worker-strategy/SKILL.md +178 -0
- package/core-assets/packs/pack-pwa/skills/void-service-worker-strategy/harness.yaml +14 -0
- package/core-assets/packs/pack-react/skills/void-accessibility-check/.source +8 -0
- package/core-assets/packs/pack-react/skills/void-accessibility-check/SKILL.md +136 -0
- package/core-assets/packs/pack-react/skills/void-accessibility-check/harness.yaml +14 -0
- package/core-assets/packs/pack-react/skills/void-client-vs-server-component/.source +5 -0
- package/core-assets/packs/pack-react/skills/void-client-vs-server-component/SKILL.md +116 -0
- package/core-assets/packs/pack-react/skills/void-client-vs-server-component/harness.yaml +14 -0
- package/core-assets/packs/pack-react/skills/void-form-pattern/.source +7 -0
- package/core-assets/packs/pack-react/skills/void-form-pattern/SKILL.md +157 -0
- package/core-assets/packs/pack-react/skills/void-form-pattern/harness.yaml +14 -0
- package/core-assets/packs/pack-react/skills/void-state-architecture/.source +5 -0
- package/core-assets/packs/pack-react/skills/void-state-architecture/SKILL.md +160 -0
- package/core-assets/packs/pack-react/skills/void-state-architecture/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-background-job-pattern/.source +6 -0
- package/core-assets/packs/pack-server/skills/void-background-job-pattern/SKILL.md +214 -0
- package/core-assets/packs/pack-server/skills/void-background-job-pattern/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-drizzle-migration-safe/.source +6 -0
- package/core-assets/packs/pack-server/skills/void-drizzle-migration-safe/SKILL.md +168 -0
- package/core-assets/packs/pack-server/skills/void-drizzle-migration-safe/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-env-validation/.source +6 -0
- package/core-assets/packs/pack-server/skills/void-env-validation/SKILL.md +131 -0
- package/core-assets/packs/pack-server/skills/void-env-validation/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-rate-limit-strategy/.source +6 -0
- package/core-assets/packs/pack-server/skills/void-rate-limit-strategy/SKILL.md +149 -0
- package/core-assets/packs/pack-server/skills/void-rate-limit-strategy/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-server-action/.source +7 -0
- package/core-assets/packs/pack-server/skills/void-server-action/SKILL.md +181 -0
- package/core-assets/packs/pack-server/skills/void-server-action/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-testing-server-modules/.source +7 -0
- package/core-assets/packs/pack-server/skills/void-testing-server-modules/SKILL.md +73 -0
- package/core-assets/packs/pack-server/skills/void-testing-server-modules/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-webhook-handler-pattern/.source +7 -0
- package/core-assets/packs/pack-server/skills/void-webhook-handler-pattern/SKILL.md +205 -0
- package/core-assets/packs/pack-server/skills/void-webhook-handler-pattern/harness.yaml +14 -0
- package/core-assets/policies/core.yaml +95 -0
- package/core-assets/policies/ui.yaml +23 -0
- package/core-assets/profiles/base.yaml +29 -0
- package/core-assets/profiles/expo-config.yaml +36 -0
- package/core-assets/profiles/expo.yaml +35 -0
- package/core-assets/profiles/monorepo.yaml +33 -0
- package/core-assets/profiles/nextjs-config.yaml +31 -0
- package/core-assets/profiles/nextjs.yaml +31 -0
- package/core-assets/profiles/node-server.yaml +31 -0
- package/core-assets/profiles/pwa.yaml +33 -0
- package/core-assets/profiles/react.yaml +39 -0
- package/core-assets/profiles/sql.yaml +39 -0
- package/core-assets/profiles/typescript.yaml +39 -0
- package/core-assets/skills/.gitkeep +0 -0
- package/core-assets/skills/void-accessibility/.source +15 -0
- package/core-assets/skills/void-accessibility/SKILL.md +275 -0
- package/core-assets/skills/void-accessibility/harness.yaml +17 -0
- package/core-assets/skills/void-api-and-interface-design/.source +13 -0
- package/core-assets/skills/void-api-and-interface-design/SKILL.md +231 -0
- package/core-assets/skills/void-api-and-interface-design/harness.yaml +16 -0
- package/core-assets/skills/void-async-safety/.source +13 -0
- package/core-assets/skills/void-async-safety/SKILL.md +287 -0
- package/core-assets/skills/void-async-safety/harness.yaml +16 -0
- package/core-assets/skills/void-audit/.source +15 -0
- package/core-assets/skills/void-audit/SKILL.md +83 -0
- package/core-assets/skills/void-audit/harness.yaml +14 -0
- package/core-assets/skills/void-autopilot/.source +21 -0
- package/core-assets/skills/void-autopilot/SKILL.md +358 -0
- package/core-assets/skills/void-autopilot/harness.yaml +14 -0
- package/core-assets/skills/void-brainstorm/.source +14 -0
- package/core-assets/skills/void-brainstorm/SKILL.md +271 -0
- package/core-assets/skills/void-brainstorm/harness.yaml +14 -0
- package/core-assets/skills/void-checkpoint/.source +39 -0
- package/core-assets/skills/void-checkpoint/SKILL.md +227 -0
- package/core-assets/skills/void-checkpoint/harness.yaml +14 -0
- package/core-assets/skills/void-claude-md/.source +15 -0
- package/core-assets/skills/void-claude-md/SKILL.md +180 -0
- package/core-assets/skills/void-claude-md/harness.yaml +14 -0
- package/core-assets/skills/void-code-review/.source +14 -0
- package/core-assets/skills/void-code-review/SKILL.md +232 -0
- package/core-assets/skills/void-code-review/harness.yaml +15 -0
- package/core-assets/skills/void-commit-discipline/.source +9 -0
- package/core-assets/skills/void-commit-discipline/SKILL.md +211 -0
- package/core-assets/skills/void-commit-discipline/harness.yaml +15 -0
- package/core-assets/skills/void-context/.source +15 -0
- package/core-assets/skills/void-context/SKILL.md +167 -0
- package/core-assets/skills/void-context/harness.yaml +14 -0
- package/core-assets/skills/void-debug/.source +11 -0
- package/core-assets/skills/void-debug/SKILL.md +211 -0
- package/core-assets/skills/void-debug/harness.yaml +14 -0
- package/core-assets/skills/void-decide/.source +9 -0
- package/core-assets/skills/void-decide/SKILL.md +147 -0
- package/core-assets/skills/void-decide/harness.yaml +14 -0
- package/core-assets/skills/void-devex-audit/.source +8 -0
- package/core-assets/skills/void-devex-audit/SKILL.md +113 -0
- package/core-assets/skills/void-devex-audit/harness.yaml +15 -0
- package/core-assets/skills/void-doctor/.source +15 -0
- package/core-assets/skills/void-doctor/SKILL.md +83 -0
- package/core-assets/skills/void-doctor/harness.yaml +14 -0
- package/core-assets/skills/void-domain-driven-design/.source +13 -0
- package/core-assets/skills/void-domain-driven-design/SKILL.md +292 -0
- package/core-assets/skills/void-domain-driven-design/harness.yaml +15 -0
- package/core-assets/skills/void-frontend-design/.source +19 -0
- package/core-assets/skills/void-frontend-design/SKILL.md +274 -0
- package/core-assets/skills/void-frontend-design/harness.yaml +17 -0
- package/core-assets/skills/void-functional/.source +14 -0
- package/core-assets/skills/void-functional/SKILL.md +312 -0
- package/core-assets/skills/void-functional/harness.yaml +15 -0
- package/core-assets/skills/void-graph/.source +15 -0
- package/core-assets/skills/void-graph/SKILL.md +80 -0
- package/core-assets/skills/void-graph/harness.yaml +14 -0
- package/core-assets/skills/void-hexagonal-architecture/.source +12 -0
- package/core-assets/skills/void-hexagonal-architecture/SKILL.md +276 -0
- package/core-assets/skills/void-hexagonal-architecture/harness.yaml +15 -0
- package/core-assets/skills/void-implement/.source +39 -0
- package/core-assets/skills/void-implement/SKILL.md +183 -0
- package/core-assets/skills/void-implement/harness.yaml +14 -0
- package/core-assets/skills/void-learn/.source +13 -0
- package/core-assets/skills/void-learn/SKILL.md +226 -0
- package/core-assets/skills/void-learn/harness.yaml +14 -0
- package/core-assets/skills/void-llm-cost-discipline/.source +13 -0
- package/core-assets/skills/void-llm-cost-discipline/SKILL.md +299 -0
- package/core-assets/skills/void-llm-cost-discipline/harness.yaml +15 -0
- package/core-assets/skills/void-make-pdf/.source +9 -0
- package/core-assets/skills/void-make-pdf/SKILL.md +52 -0
- package/core-assets/skills/void-make-pdf/harness.yaml +15 -0
- package/core-assets/skills/void-merge/.source +8 -0
- package/core-assets/skills/void-merge/SKILL.md +111 -0
- package/core-assets/skills/void-merge/harness.yaml +14 -0
- package/core-assets/skills/void-migrations/.source +13 -0
- package/core-assets/skills/void-migrations/SKILL.md +264 -0
- package/core-assets/skills/void-migrations/harness.yaml +17 -0
- package/core-assets/skills/void-observability/.source +12 -0
- package/core-assets/skills/void-observability/SKILL.md +245 -0
- package/core-assets/skills/void-observability/harness.yaml +15 -0
- package/core-assets/skills/void-plan/.source +15 -0
- package/core-assets/skills/void-plan/SKILL.md +239 -0
- package/core-assets/skills/void-plan/harness.yaml +14 -0
- package/core-assets/skills/void-plan-review/.source +12 -0
- package/core-assets/skills/void-plan-review/SKILL.md +128 -0
- package/core-assets/skills/void-plan-review/harness.yaml +15 -0
- package/core-assets/skills/void-qa/.source +15 -0
- package/core-assets/skills/void-qa/SKILL.md +103 -0
- package/core-assets/skills/void-qa/harness.yaml +15 -0
- package/core-assets/skills/void-refactor/.source +9 -0
- package/core-assets/skills/void-refactor/SKILL.md +220 -0
- package/core-assets/skills/void-refactor/harness.yaml +15 -0
- package/core-assets/skills/void-retrospective/.source +9 -0
- package/core-assets/skills/void-retrospective/SKILL.md +71 -0
- package/core-assets/skills/void-retrospective/harness.yaml +15 -0
- package/core-assets/skills/void-security-audit/.source +12 -0
- package/core-assets/skills/void-security-audit/SKILL.md +138 -0
- package/core-assets/skills/void-security-audit/harness.yaml +15 -0
- package/core-assets/skills/void-security-guidance/.source +12 -0
- package/core-assets/skills/void-security-guidance/SKILL.md +255 -0
- package/core-assets/skills/void-security-guidance/harness.yaml +15 -0
- package/core-assets/skills/void-source-driven-development/.source +12 -0
- package/core-assets/skills/void-source-driven-development/SKILL.md +158 -0
- package/core-assets/skills/void-source-driven-development/harness.yaml +15 -0
- package/core-assets/skills/void-tdd/.source +15 -0
- package/core-assets/skills/void-tdd/SKILL.md +394 -0
- package/core-assets/skills/void-tdd/harness.yaml +15 -0
- package/core-assets/skills/void-testing/.source +19 -0
- package/core-assets/skills/void-testing/SKILL.md +301 -0
- package/core-assets/skills/void-testing/harness.yaml +17 -0
- package/core-assets/skills/void-ticket/.source +18 -0
- package/core-assets/skills/void-ticket/SKILL.md +239 -0
- package/core-assets/skills/void-ticket/harness.yaml +14 -0
- package/core-assets/skills/void-typescript-strict/.source +10 -0
- package/core-assets/skills/void-typescript-strict/SKILL.md +294 -0
- package/core-assets/skills/void-typescript-strict/harness.yaml +17 -0
- package/core-assets/skills/void-ui-review/.source +12 -0
- package/core-assets/skills/void-ui-review/SKILL.md +97 -0
- package/core-assets/skills/void-ui-review/harness.yaml +15 -0
- package/core-assets/skills/void-verify/.source +12 -0
- package/core-assets/skills/void-verify/SKILL.md +184 -0
- package/core-assets/skills/void-verify/harness.yaml +15 -0
- package/core-assets/specialists/accessibility-specialist.source +11 -0
- package/core-assets/specialists/accessibility-specialist.yaml +30 -0
- package/core-assets/specialists/api-integration-engineer.source +11 -0
- package/core-assets/specialists/api-integration-engineer.yaml +30 -0
- package/core-assets/specialists/data-migration-engineer.source +11 -0
- package/core-assets/specialists/data-migration-engineer.yaml +30 -0
- package/core-assets/specialists/devex-docs-engineer.source +11 -0
- package/core-assets/specialists/devex-docs-engineer.yaml +29 -0
- package/core-assets/specialists/domain-architect.source +11 -0
- package/core-assets/specialists/domain-architect.yaml +28 -0
- package/core-assets/specialists/experience-designer.source +15 -0
- package/core-assets/specialists/experience-designer.yaml +38 -0
- package/core-assets/specialists/frontend-engineer.source +11 -0
- package/core-assets/specialists/frontend-engineer.yaml +32 -0
- package/core-assets/specialists/independent-code-reviewer.source +10 -0
- package/core-assets/specialists/independent-code-reviewer.yaml +37 -0
- package/core-assets/specialists/migrations.json +15 -0
- package/core-assets/specialists/observability-sre-engineer.source +11 -0
- package/core-assets/specialists/observability-sre-engineer.yaml +30 -0
- package/core-assets/specialists/pdf-specialist.source +11 -0
- package/core-assets/specialists/pdf-specialist.yaml +29 -0
- package/core-assets/specialists/performance-engineer.source +11 -0
- package/core-assets/specialists/performance-engineer.yaml +29 -0
- package/core-assets/specialists/product-challenger.source +10 -0
- package/core-assets/specialists/product-challenger.yaml +27 -0
- package/core-assets/specialists/security-engineer.source +13 -0
- package/core-assets/specialists/security-engineer.yaml +41 -0
- package/core-assets/specialists/solution-architect.source +13 -0
- package/core-assets/specialists/solution-architect.yaml +32 -0
- package/core-assets/specialists/test-qa-engineer.source +14 -0
- package/core-assets/specialists/test-qa-engineer.yaml +35 -0
- package/core-assets/specialists/visual-craft-director.source +17 -0
- package/core-assets/specialists/visual-craft-director.yaml +60 -0
- package/core-assets/templates/github/dependency-audit.mjs +54 -0
- package/core-assets/templates/github/void-dependency-audit.yml +35 -0
- package/core-assets/templates/github/void-security.yml +85 -0
- package/dist/main.js +57001 -0
- package/package.json +66 -4
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Sources for `void-offline-first-mutation` skill (see `docs/plans/skill-audits/void-offline-first-mutation.md` for the full audit):
|
|
2
|
+
|
|
3
|
+
- Solaar `@solaar/ui/offline` implementation — primary source; pattern lifted with adaptation to be stack-neutral.
|
|
4
|
+
- Stripe API idempotency-key docs — confirms the client-generates-UUID approach for HTTP idempotency.
|
|
5
|
+
- RxDB / WatermelonDB conflict-resolution docs — inspiration for "server wins" baseline; richer reconciliation rejected for scope.
|
|
6
|
+
- Yjs / Automerge — mentioned as out-of-scope for explicit non-coverage.
|
|
7
|
+
|
|
8
|
+
Strategy: distill. Capture-queue + sync pattern distilled from Solaar and adapted to be stack-neutral.
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: void-offline-first-mutation
|
|
3
|
+
description: "Implement a UI mutation that works offline via the capture-queue + sync pattern: IndexedDB, optimistic UI, idempotency keys, retry, conflict resolution. Self-contained, no harness wrappers."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# offline-first-mutation
|
|
7
|
+
|
|
8
|
+
Use when implementing **any user-triggered write** in a PWA that must succeed when the device is offline (mobile in subway, mobile in field, flaky wifi) and reconcile when connectivity returns.
|
|
9
|
+
|
|
10
|
+
If the mutation is read-only or the user can tolerate "please try again when online", this skill does not apply.
|
|
11
|
+
|
|
12
|
+
## When this skill triggers
|
|
13
|
+
|
|
14
|
+
- "User taps the save button" / "user posts a comment" / "user updates a profile field"
|
|
15
|
+
- "Capture this action even if offline"
|
|
16
|
+
- Any flow where dropping the user's input is a product failure
|
|
17
|
+
|
|
18
|
+
## The model (capture-queue + sync)
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
┌────────────┐ enqueue ┌─────────────┐ sync() ┌──────────────┐
|
|
22
|
+
│ UI write │ ────────────► │ IndexedDB │ ───────────► │ Server Action │
|
|
23
|
+
│ (optimistic│ │ capture │ │ (or POST API) │
|
|
24
|
+
│ update) │ │ queue │ └──────────────┘
|
|
25
|
+
└────────────┘ └─────────────┘ │
|
|
26
|
+
▲ │ ▼
|
|
27
|
+
│ conflict ? │ commit | reject
|
|
28
|
+
└────────────────────────────┴───── server wins ────────────┘
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Three guarantees:
|
|
32
|
+
|
|
33
|
+
1. **Local first**: UI updates synchronously from the optimistic state — the user never waits for the network.
|
|
34
|
+
2. **Durable**: the intent is persisted to IndexedDB **before** the UI confirms; an immediate browser crash doesn't lose it.
|
|
35
|
+
3. **Idempotent**: each intent carries a client-generated `idempotencyKey` so retries are safe.
|
|
36
|
+
|
|
37
|
+
## Anatomy of an intent
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
type Intent<TPayload> = {
|
|
41
|
+
readonly id: string; // client UUID v7 — sortable, also serves as idempotencyKey
|
|
42
|
+
readonly kind: string; // 'note.create', 'contact.update', etc.
|
|
43
|
+
readonly payload: TPayload; // serializable
|
|
44
|
+
readonly createdAt: number; // epoch ms (client clock; used for ordering, not for truth)
|
|
45
|
+
readonly attempts: number; // bumped on each retry
|
|
46
|
+
readonly status: 'pending' | 'syncing' | 'committed' | 'failed';
|
|
47
|
+
readonly lastError?: { code: string; message: string };
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`status` transitions:
|
|
52
|
+
|
|
53
|
+
- `pending` → `syncing` (sync picks it up, network in-flight)
|
|
54
|
+
- `syncing` → `committed` (server returned 2xx)
|
|
55
|
+
- `syncing` → `pending` (retryable error: network, 5xx, 429 — schedule next attempt with backoff)
|
|
56
|
+
- `syncing` → `failed` (non-retryable: 4xx other than 429 — surface to user, do not retry automatically)
|
|
57
|
+
|
|
58
|
+
## Capture-queue primitives (write them once per project)
|
|
59
|
+
|
|
60
|
+
The capture-queue is small enough to own outright. Write these in your project (e.g., `packages/offline/src/`):
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// packages/offline/src/capture-queue.ts
|
|
64
|
+
import { openDB, IDBPDatabase } from 'idb';
|
|
65
|
+
|
|
66
|
+
const DB_NAME = 'offline-queue';
|
|
67
|
+
const STORE = 'intents';
|
|
68
|
+
|
|
69
|
+
let dbPromise: Promise<IDBPDatabase> | null = null;
|
|
70
|
+
function db() {
|
|
71
|
+
if (!dbPromise) {
|
|
72
|
+
dbPromise = openDB(DB_NAME, 1, {
|
|
73
|
+
upgrade(d) {
|
|
74
|
+
d.createObjectStore(STORE, { keyPath: 'id' });
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return dbPromise;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function enqueue<T>(intent: Omit<Intent<T>, 'attempts' | 'status' | 'createdAt'> & { createdAt?: number }): Promise<Intent<T>> {
|
|
82
|
+
const full: Intent<T> = {
|
|
83
|
+
...intent,
|
|
84
|
+
createdAt: intent.createdAt ?? Date.now(),
|
|
85
|
+
attempts: 0,
|
|
86
|
+
status: 'pending',
|
|
87
|
+
};
|
|
88
|
+
const d = await db();
|
|
89
|
+
await d.put(STORE, full);
|
|
90
|
+
return full;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function pending(): Promise<Intent<unknown>[]> {
|
|
94
|
+
const d = await db();
|
|
95
|
+
return (await d.getAll(STORE)).filter((i) => i.status === 'pending');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function markStatus(id: string, status: Intent<unknown>['status'], lastError?: Intent<unknown>['lastError']): Promise<void> {
|
|
99
|
+
const d = await db();
|
|
100
|
+
const intent = await d.get(STORE, id);
|
|
101
|
+
if (!intent) return;
|
|
102
|
+
await d.put(STORE, { ...intent, status, lastError, attempts: intent.attempts + (status === 'pending' ? 1 : 0) });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function failed(): Promise<Intent<unknown>[]> {
|
|
106
|
+
const d = await db();
|
|
107
|
+
return (await d.getAll(STORE)).filter((i) => i.status === 'failed');
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Sync engine
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// packages/offline/src/sync.ts
|
|
115
|
+
import { pending, markStatus } from './capture-queue';
|
|
116
|
+
|
|
117
|
+
const BACKOFF_MS = [2_000, 5_000, 15_000, 60_000, 300_000, 1_800_000, 14_400_000]; // 2s..4h
|
|
118
|
+
|
|
119
|
+
export type SyncHandler<T> = (intent: Intent<T>) => Promise<void>;
|
|
120
|
+
|
|
121
|
+
export async function sync(handlers: Record<string, SyncHandler<unknown>>): Promise<void> {
|
|
122
|
+
if (typeof navigator !== 'undefined' && navigator.onLine === false) return;
|
|
123
|
+
|
|
124
|
+
const items = await pending();
|
|
125
|
+
for (const intent of items) {
|
|
126
|
+
const handler = handlers[intent.kind];
|
|
127
|
+
if (!handler) continue;
|
|
128
|
+
|
|
129
|
+
await markStatus(intent.id, 'syncing');
|
|
130
|
+
try {
|
|
131
|
+
await handler(intent);
|
|
132
|
+
await markStatus(intent.id, 'committed');
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (isPermanentFailure(err)) {
|
|
135
|
+
await markStatus(intent.id, 'failed', { code: 'permanent', message: String(err) });
|
|
136
|
+
} else {
|
|
137
|
+
// Schedule retry — caller's responsibility to call sync() again after backoff
|
|
138
|
+
await markStatus(intent.id, 'pending', { code: 'transient', message: String(err) });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isPermanentFailure(err: unknown): boolean {
|
|
145
|
+
// Adapt to your error types — 4xx (not 429) is permanent
|
|
146
|
+
if (err && typeof err === 'object' && 'status' in err) {
|
|
147
|
+
const s = (err as { status: number }).status;
|
|
148
|
+
return s >= 400 && s < 500 && s !== 429;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
That's ~60 lines of code consumers OWN — not a wrapper from this pack. Easier to debug, easier to adapt to your stack (Dexie, idb-keyval, raw IndexedDB).
|
|
155
|
+
|
|
156
|
+
## Use it in a component
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
'use client';
|
|
160
|
+
|
|
161
|
+
import { useState } from 'react';
|
|
162
|
+
import { enqueue } from '@/offline/capture-queue';
|
|
163
|
+
import { sync } from '@/offline/sync';
|
|
164
|
+
import { createNoteAction } from '@/actions/notes';
|
|
165
|
+
|
|
166
|
+
export function NoteForm() {
|
|
167
|
+
const [submitting, setSubmitting] = useState(false);
|
|
168
|
+
|
|
169
|
+
async function onSubmit(formData: FormData) {
|
|
170
|
+
setSubmitting(true);
|
|
171
|
+
const intent = await enqueue({
|
|
172
|
+
id: crypto.randomUUID(),
|
|
173
|
+
kind: 'note.create',
|
|
174
|
+
payload: {
|
|
175
|
+
title: formData.get('title'),
|
|
176
|
+
body: formData.get('body'),
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Optimistic UI update — show the note in the list immediately
|
|
181
|
+
addNoteOptimistic({ id: intent.id, ...intent.payload, status: 'pending' });
|
|
182
|
+
|
|
183
|
+
// Kick off sync in background — do not await
|
|
184
|
+
sync({
|
|
185
|
+
'note.create': async (i) => {
|
|
186
|
+
await createNoteAction({
|
|
187
|
+
idempotencyKey: i.id,
|
|
188
|
+
...(i.payload as { title: string; body: string }),
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
setSubmitting(false);
|
|
194
|
+
// Form clears immediately — the optimistic note is in the list already.
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return <form action={onSubmit}>...</form>;
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Server-side idempotency
|
|
202
|
+
|
|
203
|
+
The Server Action MUST honor `idempotencyKey`:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
'use server';
|
|
207
|
+
|
|
208
|
+
export async function createNoteAction(input: { idempotencyKey: string; title: string; body: string }) {
|
|
209
|
+
// Inbox pattern: check if we've already committed this key
|
|
210
|
+
const existing = await db.query.notes.findFirst({
|
|
211
|
+
where: eq(notes.idempotencyKey, input.idempotencyKey),
|
|
212
|
+
});
|
|
213
|
+
if (existing) return { ok: true, data: existing }; // safe re-deliver
|
|
214
|
+
|
|
215
|
+
// Otherwise create + commit
|
|
216
|
+
const [created] = await db.insert(notes).values({ ... }).returning();
|
|
217
|
+
return { ok: true, data: created };
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The `idempotencyKey` column on the notes table needs a unique index. Migration follows the safe pattern (see `void-drizzle-migration-safe`).
|
|
222
|
+
|
|
223
|
+
## Conflict resolution
|
|
224
|
+
|
|
225
|
+
When the server response contradicts the local optimistic state (e.g., a concurrent edit from another device), the **server wins**. Two cases:
|
|
226
|
+
|
|
227
|
+
- **Field-level conflict** (last-write-wins): server returns its version, UI overwrites the optimistic state.
|
|
228
|
+
- **Validation rejection** (e.g., title became required): mark intent as `failed`, surface a toast to the user with the option to edit and retry.
|
|
229
|
+
|
|
230
|
+
For richer reconciliation (operational transforms, CRDTs), don't try to roll your own — use Yjs or Automerge. Outside this skill's scope.
|
|
231
|
+
|
|
232
|
+
## Backoff schedule
|
|
233
|
+
|
|
234
|
+
Default retry intervals: **2s, 5s, 15s, 60s, 5min, 30min, 4h**. After 4h with no success, mark `failed` and notify the user. Driven by the caller (re-call `sync()` from a `setTimeout` or `requestIdleCallback` chain).
|
|
235
|
+
|
|
236
|
+
Pause sync entirely when `navigator.onLine === false`. Resume on `online` event:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
window.addEventListener('online', () => sync(handlers));
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Dead-letter
|
|
243
|
+
|
|
244
|
+
Intents in `failed` status surface in a dead-letter UI (`Settings → Pending sync`) where the user can:
|
|
245
|
+
|
|
246
|
+
- Retry manually (`markStatus(id, 'pending')` + `sync()`)
|
|
247
|
+
- Edit and resubmit (creates a new intent, discards the old)
|
|
248
|
+
- Delete (drops the change permanently)
|
|
249
|
+
|
|
250
|
+
Never silently drop a failed intent — the user wrote it, it's their data.
|
|
251
|
+
|
|
252
|
+
## Anti-patterns
|
|
253
|
+
|
|
254
|
+
- ✗ **Optimistic update without persistence**: tab close = lost data.
|
|
255
|
+
- ✗ **Client-generated IDs that aren't UUID v7 / ULID**: ordering breaks across devices.
|
|
256
|
+
- ✗ **`idempotencyKey` reused across mutations**: server returns the wrong response.
|
|
257
|
+
- ✗ **Showing "Saved!" toast on enqueue**: lies to the user — sync hasn't happened. Use "Captured" or no toast.
|
|
258
|
+
- ✗ **Catching `failed` errors and swallowing them**: dead-letter MUST be visible.
|
|
259
|
+
- ✗ **Mixing capture-queue with React Query**: pick one for mutations. RQ for reads, capture-queue for offline-tolerant writes.
|
|
260
|
+
|
|
261
|
+
## Composition (informational)
|
|
262
|
+
|
|
263
|
+
- `void-server-action` — sync target; idempotency-key handling lives there.
|
|
264
|
+
- `void-async-safety` — backoff schedule, dead-letter, bounded retry semantics.
|
|
265
|
+
- `void-drizzle-migration-safe` — adding the `idempotencyKey` unique index.
|
|
266
|
+
- `void-observability` — log sync attempts with `idempotencyKey` so a failed intent can be traced end-to-end.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Harness metadata for this skill. Not part of the Agent Skills
|
|
2
|
+
# specification, and never copied into a consumer project: the SKILL.md
|
|
3
|
+
# beside it stays portable and validates against the official six fields.
|
|
4
|
+
# Read by this repository's graph, and by the installer for `runtimes`.
|
|
5
|
+
kind: standard
|
|
6
|
+
owner: folpe
|
|
7
|
+
runtimes: [claude, codex]
|
|
8
|
+
enforcement:
|
|
9
|
+
floor: ci
|
|
10
|
+
inline:
|
|
11
|
+
claude: active
|
|
12
|
+
codex: active
|
|
13
|
+
hermes: ci-only
|
|
14
|
+
eval_targets: [claude/anthropic/opus]
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Sources for `void-service-worker-strategy` skill (see `docs/plans/skill-audits/void-service-worker-strategy.md` for the full audit):
|
|
2
|
+
|
|
3
|
+
- No external source table in the audit — skill distilled into the canonical per-resource-class strategy table, cache versioning rule, update activation patterns, and a Serwist-based config + DevTools debug section.
|
|
4
|
+
|
|
5
|
+
Strategy: distill. The audit lists no external "Sources audited" table; recorded honestly here.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: void-service-worker-strategy
|
|
3
|
+
description: Pick caching strategy per route class (NetworkFirst, CacheFirst, StaleWhileRevalidate, NetworkOnly), version your caches, handle update activation. Use Serwist (Next) or vite-plugin-pwa.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# service-worker-strategy
|
|
7
|
+
|
|
8
|
+
Use when configuring or modifying a service worker in a PWA project. The strategy choice per route class is the difference between "instant repeat visits" and "stale content shown for days" or "no offline support".
|
|
9
|
+
|
|
10
|
+
If your app has no service worker yet, this skill says how to add one. If it has one but feels broken, this skill says how to debug.
|
|
11
|
+
|
|
12
|
+
## The library choice
|
|
13
|
+
|
|
14
|
+
| Stack | Library |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Next.js 16 | **Serwist** (modern Workbox successor, App Router compatible) |
|
|
17
|
+
| Vite | **vite-plugin-pwa** (wraps Workbox) |
|
|
18
|
+
| Astro | `@vite-pwa/astro` |
|
|
19
|
+
| Custom | Workbox directly (hard mode; only if you understand SW internals) |
|
|
20
|
+
|
|
21
|
+
Don't hand-write a service worker. Serwist/Workbox handles the lifecycle gotchas (skipWaiting, clientsClaim, cache versioning) you'd otherwise rediscover the hard way.
|
|
22
|
+
|
|
23
|
+
## Strategies per route class
|
|
24
|
+
|
|
25
|
+
Match the strategy to the **freshness vs availability** trade-off of each resource type.
|
|
26
|
+
|
|
27
|
+
| Resource | Strategy | Why |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| HTML pages (`/`, `/blog/*`) | `NetworkFirst({ networkTimeoutSeconds: 3 })` | Fresh content preferred; fallback to cache after 3s for offline |
|
|
30
|
+
| JS / CSS bundles (hashed names) | `CacheFirst` | Hash in name = immutable; cache forever |
|
|
31
|
+
| Fonts | `CacheFirst({ maxAge: 1 year })` | Rarely changes; large download |
|
|
32
|
+
| Images | `CacheFirst({ maxAge: 30 days, maxEntries: 60 })` | Expensive bandwidth |
|
|
33
|
+
| API responses | `NetworkOnly` (default) | Fresh data required; capture-queue handles offline writes |
|
|
34
|
+
| API responses (tolerable stale, e.g. /me) | `StaleWhileRevalidate` | Show cached, update in background |
|
|
35
|
+
| Manifest | `NetworkOnly` | Browser handles caching natively |
|
|
36
|
+
| `/api/health` | `NetworkOnly` | Should always reflect server reality |
|
|
37
|
+
|
|
38
|
+
## Canonical Serwist config sketch (Next 16)
|
|
39
|
+
|
|
40
|
+
> Serwist evolves quickly; the **strategy-per-resource-class principles** below are the substance. The exact config shape may differ per Serwist version — always cross-reference the current Serwist docs when scaffolding.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// apps/web/app/sw.ts (shape — confirm against current Serwist version)
|
|
44
|
+
import { Serwist, NetworkFirst, CacheFirst, NetworkOnly } from 'serwist';
|
|
45
|
+
|
|
46
|
+
const SW_VERSION = '__BUILD_HASH__'; // injected by build
|
|
47
|
+
|
|
48
|
+
declare const self: ServiceWorkerGlobalScope;
|
|
49
|
+
|
|
50
|
+
const serwist = new Serwist({
|
|
51
|
+
precacheEntries: self.__SW_MANIFEST, // injected: static assets to pre-cache
|
|
52
|
+
skipWaiting: true,
|
|
53
|
+
clientsClaim: true,
|
|
54
|
+
navigationPreload: true,
|
|
55
|
+
runtimeCaching: [
|
|
56
|
+
// HTML pages: prefer fresh, fall back to cache after 3s on slow networks
|
|
57
|
+
{
|
|
58
|
+
matcher: ({ request }) => request.destination === 'document',
|
|
59
|
+
handler: new NetworkFirst({
|
|
60
|
+
cacheName: `html-${SW_VERSION}`,
|
|
61
|
+
networkTimeoutSeconds: 3,
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
// JS/CSS: hashed filenames are immutable; cache forever
|
|
65
|
+
{
|
|
66
|
+
matcher: ({ request }) => request.destination === 'script' || request.destination === 'style',
|
|
67
|
+
handler: new CacheFirst({ cacheName: `static-${SW_VERSION}` }),
|
|
68
|
+
},
|
|
69
|
+
// Images: cache for 30 days, cap at 60 entries
|
|
70
|
+
{
|
|
71
|
+
matcher: ({ request }) => request.destination === 'image',
|
|
72
|
+
handler: new CacheFirst({ cacheName: `images-${SW_VERSION}` }),
|
|
73
|
+
},
|
|
74
|
+
// API: never cache; let the network speak
|
|
75
|
+
{
|
|
76
|
+
matcher: ({ url }) => url.pathname.startsWith('/api/'),
|
|
77
|
+
handler: new NetworkOnly(),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
serwist.addEventListeners();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`vite-plugin-pwa` exposes the same strategies under Workbox names (`NetworkFirst`, etc.) with slightly different option keys. The **what** (per-resource strategy + version cacheName + max age) transfers; the **how** (exact constructor call) is library-specific.
|
|
86
|
+
|
|
87
|
+
## Cache versioning — the critical detail
|
|
88
|
+
|
|
89
|
+
Without versioning, an updated worker reads old caches with old asset URLs → broken page after deploy.
|
|
90
|
+
|
|
91
|
+
The pattern: include `SW_VERSION` (build hash, app version, anything that changes per deploy) in EVERY `cacheName`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
cacheName: `html-${SW_VERSION}` // not just `html`
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
On worker activation, old caches (with old version names) are orphaned. Periodic cleanup:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
self.addEventListener('activate', (event) => {
|
|
101
|
+
event.waitUntil((async () => {
|
|
102
|
+
const keys = await caches.keys();
|
|
103
|
+
await Promise.all(
|
|
104
|
+
keys
|
|
105
|
+
.filter((k) => !k.endsWith(`-${SW_VERSION}`))
|
|
106
|
+
.map((k) => caches.delete(k))
|
|
107
|
+
);
|
|
108
|
+
})());
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Serwist handles this if `cleanupOutdatedCaches: true` (default in newer versions).
|
|
113
|
+
|
|
114
|
+
## Update activation UX
|
|
115
|
+
|
|
116
|
+
Three patterns; pick one.
|
|
117
|
+
|
|
118
|
+
### A. Silent — worker activates on next page load
|
|
119
|
+
|
|
120
|
+
Default. Simplest. New worker becomes active after the user closes/reopens the tab. Works for most apps.
|
|
121
|
+
|
|
122
|
+
### B. "Reload to update" prompt
|
|
123
|
+
|
|
124
|
+
When a new worker is waiting, show a banner. User clicks → `skipWaiting()` + reload.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// In a Client Component
|
|
128
|
+
import { Workbox } from 'workbox-window';
|
|
129
|
+
|
|
130
|
+
const wb = new Workbox('/sw.js');
|
|
131
|
+
wb.addEventListener('waiting', () => {
|
|
132
|
+
setShowUpdatePrompt(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
function applyUpdate() {
|
|
136
|
+
wb.addEventListener('controlling', () => window.location.reload());
|
|
137
|
+
wb.messageSkipWaiting();
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Use when shipping critical fixes that should not wait for tab close.
|
|
142
|
+
|
|
143
|
+
### C. Auto-apply (aggressive)
|
|
144
|
+
|
|
145
|
+
`skipWaiting: true` + `clientsClaim: true` (shown in canonical config above). New worker takes over immediately. Risk: in-flight requests served by old worker can race with new worker.
|
|
146
|
+
|
|
147
|
+
Default to C for greenfield, B for production-critical apps where users keep tabs open for days.
|
|
148
|
+
|
|
149
|
+
## Anti-patterns
|
|
150
|
+
|
|
151
|
+
- ✗ **No version in cacheName** — orphan caches grow forever, eventually quota-exceed
|
|
152
|
+
- ✗ **CacheFirst on HTML** — stuck on stale page after deploy
|
|
153
|
+
- ✗ **NetworkFirst without networkTimeoutSeconds** — slow networks see infinite spinner instead of cached fallback
|
|
154
|
+
- ✗ **Workbox plugin without HTTPS** — service workers only register on HTTPS or localhost (silent failure otherwise)
|
|
155
|
+
- ✗ **`skipWaiting` for sensitive data screens** — old worker mid-checkout, new worker takes over, weird state. Pattern B (prompt) is safer for checkout flows.
|
|
156
|
+
- ✗ **Caching POST/PUT/DELETE** — only GET is cacheable; service workers don't cache mutations
|
|
157
|
+
|
|
158
|
+
## Debugging
|
|
159
|
+
|
|
160
|
+
DevTools → Application → Service Workers:
|
|
161
|
+
|
|
162
|
+
- "Update on reload" checkbox helps during development
|
|
163
|
+
- "Unregister" + hard reload resets state
|
|
164
|
+
- "Bypass for network" disables SW temporarily
|
|
165
|
+
|
|
166
|
+
DevTools → Application → Cache Storage:
|
|
167
|
+
|
|
168
|
+
- Lists all named caches
|
|
169
|
+
- Click a cache → see entries, sizes, last accessed
|
|
170
|
+
- Should see one cache PER cacheName variant (versioned correctly)
|
|
171
|
+
|
|
172
|
+
## Composition
|
|
173
|
+
|
|
174
|
+
- `void-manifest-checklist` — manifest declares the SW scope; service worker honors it.
|
|
175
|
+
- `void-install-prompt-ux` — SW must be registered before install prompt fires.
|
|
176
|
+
- `void-offline-first-mutation` — capture-queue lives alongside SW; SW handles GET caching, capture-queue handles mutations.
|
|
177
|
+
- `void-cache-component-pattern` — Next's server-side cache is independent of SW; both layers compose.
|
|
178
|
+
- `void-async-safety` — SW activation race conditions are timing bugs; bounded retries on update apply.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Harness metadata for this skill. Not part of the Agent Skills
|
|
2
|
+
# specification, and never copied into a consumer project: the SKILL.md
|
|
3
|
+
# beside it stays portable and validates against the official six fields.
|
|
4
|
+
# Read by this repository's graph, and by the installer for `runtimes`.
|
|
5
|
+
kind: standard
|
|
6
|
+
owner: folpe
|
|
7
|
+
runtimes: [claude, codex]
|
|
8
|
+
enforcement:
|
|
9
|
+
floor: ci
|
|
10
|
+
inline:
|
|
11
|
+
claude: active
|
|
12
|
+
codex: active
|
|
13
|
+
hermes: ci-only
|
|
14
|
+
eval_targets: [claude/anthropic/opus]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Sources for `void-accessibility-check` skill (see `docs/plans/skill-audits/void-accessibility-check.md` for the full audit):
|
|
2
|
+
|
|
3
|
+
- WCAG 2.2 quick reference — authoritative for contrast and touch target sizes.
|
|
4
|
+
- WebAIM "Designing for Screen Reader Compatibility" — source of the "name accessible to AT" framing.
|
|
5
|
+
- Apple HIG / Material Design (touch targets) — 44×44 / 48×48 numbers.
|
|
6
|
+
- accessibility (parent skill) — doctrine source; this skill is its execution arm.
|
|
7
|
+
|
|
8
|
+
Strategy: distill. The doctrine lives in accessibility; this skill is the per-component operational gate.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: void-accessibility-check
|
|
3
|
+
description: "Per-component accessibility checklist: semantic HTML, ARIA, keyboard, focus, contrast, touch targets. Auto-applies on any React component edit. Composes with accessibility."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# accessibility-check
|
|
7
|
+
|
|
8
|
+
Use when creating or editing **any React component** in a `components/` directory. The 7-point checklist below is the gate — if a component ships violating any of these, it ships broken for somebody.
|
|
9
|
+
|
|
10
|
+
Composes with `void-accessibility` (the generic doctrine and Radix-as-primitive philosophy). This skill is the **execution checklist** for one component.
|
|
11
|
+
|
|
12
|
+
If the file is in `services/`, `adapters/`, or `domain/`, this skill does not apply (those are not UI).
|
|
13
|
+
|
|
14
|
+
## When this skill triggers
|
|
15
|
+
|
|
16
|
+
- New file under `apps/*/src/components/` or `packages/ui/src/`
|
|
17
|
+
- Editing JSX in a component file
|
|
18
|
+
- "Add a button that does X" / "make this a card" / any UI creation
|
|
19
|
+
|
|
20
|
+
## The 7-point gate
|
|
21
|
+
|
|
22
|
+
A component does not ship until all 7 are satisfied. Each one has an easy check + the lazy mistake to avoid.
|
|
23
|
+
|
|
24
|
+
### 1. Semantic HTML
|
|
25
|
+
|
|
26
|
+
Choose the tag that matches the role, before reaching for ARIA.
|
|
27
|
+
|
|
28
|
+
- Action that submits or navigates → `<button>` or `<a>`, never `<div onClick>`.
|
|
29
|
+
- Heading → `<h1>` to `<h6>` in document order, never `<div className="text-2xl font-bold">`.
|
|
30
|
+
- List → `<ul>` / `<ol>` / `<li>`.
|
|
31
|
+
- Form control → `<input>`, `<select>`, `<textarea>` with associated `<label>`.
|
|
32
|
+
|
|
33
|
+
Lazy mistake: shadcn primitives wrap divs but expose the right role (via Radix). Always check the underlying element — `<DropdownMenuItem>` is keyboard-navigable because Radix sets `role="menuitem"`. Custom replacements lose that.
|
|
34
|
+
|
|
35
|
+
### 2. Labels and names
|
|
36
|
+
|
|
37
|
+
Every interactive element has a programmatic name accessible to screen readers.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// ✓
|
|
41
|
+
<button aria-label="Close dialog"><XIcon /></button>
|
|
42
|
+
<label htmlFor="email">Email</label>
|
|
43
|
+
<input id="email" type="email" />
|
|
44
|
+
|
|
45
|
+
// ✗
|
|
46
|
+
<button><XIcon /></button> // unnamed
|
|
47
|
+
<input type="email" placeholder="Email" /> // placeholder is NOT a label
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Lazy mistake: relying on placeholder text. Placeholders disappear on focus and don't announce as labels. Use a real `<label>` or `aria-label`.
|
|
51
|
+
|
|
52
|
+
### 3. Keyboard navigation
|
|
53
|
+
|
|
54
|
+
Every interactive element reachable by Tab, operable by Enter / Space, dismissible by Esc (for modals/menus).
|
|
55
|
+
|
|
56
|
+
- Tab order follows visual order (avoid `tabIndex` ≥ 1).
|
|
57
|
+
- `outline` is **never** stripped without a replacement focus ring (use a `:focus-visible` style).
|
|
58
|
+
- Modals trap focus (Radix Dialog does this automatically; if you build a modal from scratch you must trap).
|
|
59
|
+
|
|
60
|
+
Lazy mistake: `outline: none` in global CSS. The focus ring is the only signal a keyboard user has for where they are.
|
|
61
|
+
|
|
62
|
+
### 4. Focus management on action
|
|
63
|
+
|
|
64
|
+
When an action changes context (open modal, route to new page, reveal an error):
|
|
65
|
+
|
|
66
|
+
- Modal opens → focus moves to the modal (Radix handles).
|
|
67
|
+
- Modal closes → focus returns to the trigger (Radix handles).
|
|
68
|
+
- Form submit with error → focus moves to the first invalid field, or to an error summary.
|
|
69
|
+
- Route change → focus moves to the new page's `<h1>` (or main content) on Next.js soft navigation (manual via `next/navigation` `useRouter` + `useEffect`).
|
|
70
|
+
|
|
71
|
+
Lazy mistake: forgetting to restore focus when a dialog closes — keyboard user lands at the top of the page.
|
|
72
|
+
|
|
73
|
+
### 5. Contrast
|
|
74
|
+
|
|
75
|
+
WCAG AA minimum:
|
|
76
|
+
|
|
77
|
+
- Normal text: **4.5:1** against background.
|
|
78
|
+
- Large text (≥ 18px bold or ≥ 24px): **3:1**.
|
|
79
|
+
- Interactive elements (button borders, focus rings, form field borders): **3:1**.
|
|
80
|
+
|
|
81
|
+
Quick checks:
|
|
82
|
+
|
|
83
|
+
- The design tokens in `@repo/ui` should already meet AA. If you reach for a non-token color, justify it in a comment.
|
|
84
|
+
- Disabled buttons are often the lazy violator (`opacity: 0.5` halves contrast).
|
|
85
|
+
|
|
86
|
+
Lazy mistake: gray-on-gray placeholder text. Most placeholders are 2.5:1 — fail.
|
|
87
|
+
|
|
88
|
+
### 6. Touch targets
|
|
89
|
+
|
|
90
|
+
Minimum **44×44 CSS px** (iOS HIG) / **48×48 dp** (Material). Applies to anything tappable.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// ✓ Tappable from @repo/ui enforces it
|
|
94
|
+
<Tappable onPress={handlePress}><Icon /></Tappable>
|
|
95
|
+
|
|
96
|
+
// ✗ Bare icon with 24px hitbox
|
|
97
|
+
<button onClick={handlePress}><Icon size={24} /></button>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Lazy mistake: dense icon-only toolbars with 24-32px buttons. Fail on every phone.
|
|
101
|
+
|
|
102
|
+
### 7. Motion and reduced motion
|
|
103
|
+
|
|
104
|
+
Animations respect `prefers-reduced-motion`:
|
|
105
|
+
|
|
106
|
+
```css
|
|
107
|
+
@media (prefers-reduced-motion: reduce) {
|
|
108
|
+
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Or per-component via `useReducedMotion()` from Framer Motion.
|
|
113
|
+
|
|
114
|
+
Lazy mistake: shipping a beautiful parallax that triggers vestibular nausea for ~3% of users.
|
|
115
|
+
|
|
116
|
+
## Workflow
|
|
117
|
+
|
|
118
|
+
For each new or edited component:
|
|
119
|
+
|
|
120
|
+
1. **Run the 7-point gate mentally** before opening the JSX.
|
|
121
|
+
2. If you're not sure what role the component plays, write one sentence first: "this is a button that submits a form" / "this is a heading announcing the section". The sentence dictates the tag.
|
|
122
|
+
3. **Prefer Radix primitives** from `@repo/ui` over building from scratch — Radix gets roles, keyboard, focus right.
|
|
123
|
+
4. **Run a Lighthouse a11y audit** on the route the component lands in (`pnpm dlx @lhci/cli` or DevTools). Target score: 100. Anything under 95 = block.
|
|
124
|
+
5. **Manual keyboard test**: Tab through the component, Enter/Space/Esc as appropriate. If you have to use the mouse to operate it, it's broken.
|
|
125
|
+
6. **Screen reader test on at least one new component per PR**: macOS VoiceOver (`Cmd+F5`), Windows NVDA, Android TalkBack, or iOS VoiceOver. You'll hear gaps immediately.
|
|
126
|
+
|
|
127
|
+
## When AA is not enough
|
|
128
|
+
|
|
129
|
+
`void-accessibility` floor is AA. AAA is sometimes the goal for **specific surfaces** (form errors, primary CTAs). It's not the project-wide default — declare it explicitly in `PROJECT-DOCTRINE.md` if so.
|
|
130
|
+
|
|
131
|
+
## Composition
|
|
132
|
+
|
|
133
|
+
- `void-accessibility` — the doctrine; this skill is the gate.
|
|
134
|
+
- `void-frontend-design` — anti AI-slop, sober density, real visual hierarchy.
|
|
135
|
+
- the `pack-react` doctrine module on components — components are pure UI (no DB, no fetch) which is itself an a11y win (less surface to break).
|
|
136
|
+
- `void-tdd` — interactive components get jsdom tests asserting roles and keyboard behavior (`@testing-library/user-event`).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Harness metadata for this skill. Not part of the Agent Skills
|
|
2
|
+
# specification, and never copied into a consumer project: the SKILL.md
|
|
3
|
+
# beside it stays portable and validates against the official six fields.
|
|
4
|
+
# Read by this repository's graph, and by the installer for `runtimes`.
|
|
5
|
+
kind: standard
|
|
6
|
+
owner: folpe
|
|
7
|
+
runtimes: [claude, codex]
|
|
8
|
+
enforcement:
|
|
9
|
+
floor: ci
|
|
10
|
+
inline:
|
|
11
|
+
claude: active
|
|
12
|
+
codex: active
|
|
13
|
+
hermes: ci-only
|
|
14
|
+
eval_targets: [claude/anthropic/opus]
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Sources for `void-client-vs-server-component` skill (see `docs/plans/skill-audits/void-client-vs-server-component.md` for the full audit):
|
|
2
|
+
|
|
3
|
+
- No external source audited — native skill authored for void-machine.
|
|
4
|
+
|
|
5
|
+
Strategy: native. Codifies "server by default, client at the leaf" for React 19 / Next 14+ Server Components, a construct that does not exist in non-RSC stacks.
|