lazycodex-ai 5.1.44 β 5.1.45
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/dist/cli/index.js +948 -416
- package/dist/cli/service-auth/keystore.d.ts +4 -2
- package/dist/cli-node/index.js +948 -416
- package/docs/reference/web-terminal-visual-qa.md +8 -0
- package/package.json +1 -1
- package/packages/lsp-daemon/dist/cli.js +4 -4
- package/packages/lsp-daemon/dist/client.js +4 -4
- package/packages/lsp-daemon/dist/index.js +4 -4
- package/packages/lsp-daemon/package.json +3 -3
- package/packages/lsp-tools-mcp/dist/cli.js +4 -4
- package/packages/lsp-tools-mcp/dist/mcp.js +4 -4
- package/packages/lsp-tools-mcp/dist/tools.js +4 -4
- package/packages/lsp-tools-mcp/package.json +3 -3
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +62 -21
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/biome.json +1 -21
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/biome.json +1 -21
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +4 -4
- package/packages/omo-codex/plugin/components/lsp/biome.json +1 -21
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +4 -4
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +0 -1
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-not-installed.test.ts +1 -1
- package/packages/omo-codex/plugin/components/rules/biome.json +1 -21
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +4 -4
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/biome.json +1 -21
- package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +90 -1
- package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +91 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -21
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/biome.json +1 -21
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/biome.json +1 -21
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +7 -7
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +2 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +7 -7
- package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +2 -0
- package/packages/omo-codex/plugin/skills/browser/SKILL.md +21 -22
- package/packages/omo-codex/plugin/skills/browser/references/commands.md +2 -9
- package/packages/omo-codex/plugin/skills/browser/references/install.md +1 -2
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +2 -2
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-engine-guard.mjs +63 -487
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +1 -1
- package/packages/omo-codex/plugin/skills/browser/scripts/omowright.mjs +1 -10
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +79 -118
- package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +75 -224
- package/packages/omo-codex/plugin/skills/frontend/references/design/stylegallery.md +32 -27
- package/packages/omo-codex/plugin/skills/frontend/references/perfection/README.md +2 -2
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/collect-files.ts +53 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +10 -7
- package/packages/omo-codex/plugin/skills/programming/references/typescript/README.md +10 -7
- package/packages/omo-codex/plugin/skills/programming/references/typescript/bootstrap.md +9 -23
- package/packages/omo-codex/plugin/skills/programming/references/typescript/oxc-lint.md +130 -0
- package/packages/omo-codex/plugin/skills/programming/references/typescript/tsconfig-strict.md +6 -62
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/new-project.ts +20 -81
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +14 -14
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +9 -3
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +7 -6
- package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +116 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +51 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/claims-index.mjs +125 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +68 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +136 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +204 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +103 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-brief.mjs +145 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-state.mjs +63 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +1 -27
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/lexicon.mjs +47 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/numerals.mjs +226 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +2 -3
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/particles-ko.mjs +108 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/rational.mjs +139 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/sentence-split.mjs +77 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-normalize.mjs +82 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-similarity.mjs +28 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +105 -0
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +71 -64
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +4 -1
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +6 -6
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +152 -23
- package/packages/shared-skills/skills/browser/SKILL.md +21 -22
- package/packages/shared-skills/skills/browser/references/commands.md +2 -9
- package/packages/shared-skills/skills/browser/references/install.md +1 -2
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +2 -2
- package/packages/shared-skills/skills/browser/scripts/browser-engine-guard.mjs +63 -487
- package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +1 -1
- package/packages/shared-skills/skills/browser/scripts/omowright.mjs +1 -10
- package/packages/shared-skills/skills/frontend/SKILL.md +79 -118
- package/packages/shared-skills/skills/frontend/references/design/README.md +75 -224
- package/packages/shared-skills/skills/frontend/references/design/stylegallery.md +32 -27
- package/packages/shared-skills/skills/frontend/references/perfection/README.md +2 -2
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.test.ts +101 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.test.ts +50 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.ts +53 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
- package/packages/shared-skills/skills/programming/SKILL.md +10 -7
- package/packages/shared-skills/skills/programming/references/typescript/README.md +10 -7
- package/packages/shared-skills/skills/programming/references/typescript/bootstrap.md +9 -23
- package/packages/shared-skills/skills/programming/references/typescript/oxc-lint.md +130 -0
- package/packages/shared-skills/skills/programming/references/typescript/tsconfig-strict.md +6 -62
- package/packages/shared-skills/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
- package/packages/shared-skills/skills/programming/scripts/typescript/new-project.test.ts +162 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/new-project.ts +20 -81
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
- package/packages/shared-skills/skills/refactor/SKILL.md +13 -13
- package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
- package/packages/shared-skills/skills/ulw-research/AGENTS.md +9 -3
- package/packages/shared-skills/skills/ulw-research/SKILL.md +2 -2
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +7 -6
- package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +116 -0
- package/packages/shared-skills/skills/ulw-research/references/report-gates.md +51 -3
- package/packages/shared-skills/skills/ulw-research/scripts/claims-index.mjs +125 -0
- package/packages/shared-skills/skills/ulw-research/scripts/claims-index.test.ts +149 -0
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +68 -3
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +97 -12
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +190 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +136 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +141 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.mjs +204 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.test.ts +43 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.mjs +103 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +252 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.mjs +145 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.test.ts +179 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.mjs +63 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.test.ts +46 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-context.test.ts +64 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +1 -1
- package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +1 -1
- package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +1 -27
- package/packages/shared-skills/skills/ulw-research/scripts/lexicon.mjs +47 -0
- package/packages/shared-skills/skills/ulw-research/scripts/lexicon.test.ts +57 -0
- package/packages/shared-skills/skills/ulw-research/scripts/numerals.mjs +226 -0
- package/packages/shared-skills/skills/ulw-research/scripts/numerals.test.ts +120 -0
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +2 -3
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +29 -0
- package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.mjs +108 -0
- package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.test.ts +86 -0
- package/packages/shared-skills/skills/ulw-research/scripts/rational.mjs +139 -0
- package/packages/shared-skills/skills/ulw-research/scripts/rational.test.ts +46 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +2 -2
- package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.mjs +77 -0
- package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.test.ts +109 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/claim-graph.md +13 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-locked.md +17 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-pending.md +17 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.mjs +82 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.test.ts +73 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.mjs +28 -0
- package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.test.ts +38 -0
- package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +105 -0
- package/packages/shared-skills/skills/ulw-research/scripts/units.test.ts +83 -0
- package/packages/shared-skills/skills/visual-qa/SKILL.md +70 -63
- package/packages/shared-skills/skills/visual-qa/scripts/skill-doc-fields.test.ts +34 -0
- package/script/qa/web-terminal-visual-qa.mjs +6 -2
- package/script/qa/xterm-live-terminal.mjs +26 -3
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { spawnSync } from "node:child_process"
|
|
3
|
+
import { readdirSync, readFileSync } from "node:fs"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import type { DraftRuleId } from "./draft-guard/findings"
|
|
6
|
+
import { lintDraft } from "./draft-guard/lint"
|
|
7
|
+
|
|
8
|
+
const FIXTURES = join(import.meta.dir, "fixtures", "draft-guard")
|
|
9
|
+
const ACCENT_FIXTURES = join(import.meta.dir, "fixtures", "accent-stripes")
|
|
10
|
+
const SCRIPT = join(import.meta.dir, "check-draft-guard.ts")
|
|
11
|
+
|
|
12
|
+
const EXPECTED: Readonly<Record<string, ReadonlyArray<[number, DraftRuleId]>>> = {
|
|
13
|
+
"promo-copy.html": [[2, "promo-copy"], [3, "promo-copy"], [5, "promo-copy"]],
|
|
14
|
+
"promo-copy.tsx": [[4, "promo-copy"], [5, "promo-copy"]],
|
|
15
|
+
"emoji-icons.tsx": [[4, "emoji-icon"], [6, "emoji-icon"]],
|
|
16
|
+
"vanity-stats.html": [[2, "vanity-stat"], [3, "vanity-stat"], [4, "vanity-stat"]],
|
|
17
|
+
"decorative-badge.vue": [[3, "decorative-badge"], [4, "decorative-badge"]],
|
|
18
|
+
"components.tsx": [
|
|
19
|
+
[5, "decorative-badge"],
|
|
20
|
+
[6, "decorative-badge"],
|
|
21
|
+
[7, "emoji-icon"],
|
|
22
|
+
[7, "decorative-badge"],
|
|
23
|
+
[8, "emoji-icon"],
|
|
24
|
+
[9, "emoji-icon"],
|
|
25
|
+
[10, "vanity-stat"],
|
|
26
|
+
[11, "vanity-stat"],
|
|
27
|
+
[12, "vanity-stat"],
|
|
28
|
+
[13, "promo-copy"],
|
|
29
|
+
],
|
|
30
|
+
"attribute-edges.html": [[2, "promo-copy"], [3, "promo-copy"], [4, "vanity-stat"], [5, "decorative-badge"], [6, "promo-copy"]],
|
|
31
|
+
"jsx-edges.tsx": [[1, "promo-copy"]],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function lintFixture(dir: string, name: string) {
|
|
35
|
+
const path = join(FIXTURES, dir, name)
|
|
36
|
+
return lintDraft([{ path, text: readFileSync(path, "utf8") }])
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("#given a positive fixture with a generated-look tell in its markup", () => {
|
|
40
|
+
test("#then every positive fixture has an expectation", () => {
|
|
41
|
+
expect(readdirSync(join(FIXTURES, "positive")).sort()).toEqual(Object.keys(EXPECTED).sort())
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
for (const [name, expected] of Object.entries(EXPECTED)) {
|
|
45
|
+
test(`#when ${name} is linted #then it reports exactly ${expected.length} finding(s) at the expected lines`, () => {
|
|
46
|
+
const findings = lintFixture("positive", name).map((f): [number, DraftRuleId] => [f.line, f.rule])
|
|
47
|
+
expect(findings).toEqual([...expected])
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe("#given a negative fixture (plain copy, code, scripts, styles, comments, bound values, status badges)", () => {
|
|
53
|
+
for (const name of readdirSync(join(FIXTURES, "negative"))) {
|
|
54
|
+
test(`#when ${name} is linted #then it reports nothing`, () => {
|
|
55
|
+
expect(lintFixture("negative", name)).toEqual([])
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("#given a generated page written on one line", () => {
|
|
61
|
+
test("#when it holds two promo phrases #then both are reported, each at its own column", () => {
|
|
62
|
+
const text = "<p>Seamless sync and effortless setup</p>\n"
|
|
63
|
+
const findings = lintDraft([{ path: "page.html", text }])
|
|
64
|
+
expect(findings.map((f) => [f.line, f.column, f.rule])).toEqual([
|
|
65
|
+
[1, text.indexOf("Seamless") + 1, "promo-copy"],
|
|
66
|
+
[1, text.indexOf("effortless") + 1, "promo-copy"],
|
|
67
|
+
])
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe("#given a stylesheet", () => {
|
|
72
|
+
test("#when it is linted #then text rules are not applied to CSS", () => {
|
|
73
|
+
expect(lintDraft([{ path: "a.css", text: '.hero::before { content: "Unlock π"; }\n' }])).toEqual([])
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe("#given the CLI", () => {
|
|
78
|
+
const run = (...args: string[]) => spawnSync(process.execPath, [SCRIPT, ...args], { encoding: "utf8", timeout: 30_000 })
|
|
79
|
+
|
|
80
|
+
test("#when it scans the draft-guard positives #then it exits 1 and prints file:line findings", () => {
|
|
81
|
+
const result = run(join(FIXTURES, "positive"))
|
|
82
|
+
expect(result.status).toBe(1)
|
|
83
|
+
expect(result.stdout).toMatch(/vanity-stats\.html:2:\d+ {2}vanity-stat/)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("#when it scans an accent-stripe positive #then the accent-stripe rules still report through it", () => {
|
|
87
|
+
const result = run("--json", join(ACCENT_FIXTURES, "positive", "border-top-card.css"))
|
|
88
|
+
expect(JSON.parse(result.stdout)).toEqual([expect.objectContaining({ line: 4, rule: "edge-stripe" })])
|
|
89
|
+
expect(result.status).toBe(1)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test("#when it scans both negative fixture sets #then it exits 0", () => {
|
|
93
|
+
expect(run(join(FIXTURES, "negative"), join(ACCENT_FIXTURES, "negative")).status).toBe(0)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("#when no path is given #then it exits 2 with usage", () => {
|
|
97
|
+
const result = run()
|
|
98
|
+
expect(result.status).toBe(2)
|
|
99
|
+
expect(result.stderr).toContain("usage:")
|
|
100
|
+
})
|
|
101
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// The draft guard in one command: accent stripes plus generated-look copy and markup (promo copy, emoji icons, hype badges, vanity stats).
|
|
3
|
+
// Usage: bun check-draft-guard.ts [--json] <file-or-dir>... Exit 0 clean, 1 findings, 2 usage or unreadable target.
|
|
4
|
+
import { readFileSync } from "node:fs"
|
|
5
|
+
import { RULE_MESSAGES } from "./accent-stripes/findings"
|
|
6
|
+
import { lintFiles, type SourceFile } from "./accent-stripes/lint"
|
|
7
|
+
import { collectFiles } from "./collect-files"
|
|
8
|
+
import { DRAFT_RULE_MESSAGES } from "./draft-guard/findings"
|
|
9
|
+
import { lintDraft } from "./draft-guard/lint"
|
|
10
|
+
|
|
11
|
+
const MESSAGES: Readonly<Record<string, string>> = { ...RULE_MESSAGES, ...DRAFT_RULE_MESSAGES }
|
|
12
|
+
|
|
13
|
+
function main(argv: readonly string[]): number {
|
|
14
|
+
const json = argv.includes("--json")
|
|
15
|
+
const targets = argv.filter((a) => a !== "--json")
|
|
16
|
+
if (targets.length === 0) {
|
|
17
|
+
process.stderr.write("usage: check-draft-guard.ts [--json] <file-or-dir>...\n")
|
|
18
|
+
return 2
|
|
19
|
+
}
|
|
20
|
+
const { paths, missing } = collectFiles(targets)
|
|
21
|
+
if (missing.length > 0) {
|
|
22
|
+
process.stderr.write(`check-draft-guard: cannot read ${missing.join(", ")}\n`)
|
|
23
|
+
return 2
|
|
24
|
+
}
|
|
25
|
+
const files: SourceFile[] = paths.map((path) => ({ path, text: readFileSync(path, "utf8") }))
|
|
26
|
+
const findings = [...lintFiles(files), ...lintDraft(files)].sort(
|
|
27
|
+
(a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column,
|
|
28
|
+
)
|
|
29
|
+
if (json) process.stdout.write(`${JSON.stringify(findings, null, 2)}\n`)
|
|
30
|
+
else {
|
|
31
|
+
for (const f of findings) {
|
|
32
|
+
process.stdout.write(`${f.file}:${f.line}:${f.column} ${f.rule} ${MESSAGES[f.rule]}\n > ${f.excerpt}\n`)
|
|
33
|
+
}
|
|
34
|
+
const summary = findings.length === 0 ? "clean" : `${findings.length} finding(s); fix each before visual QA`
|
|
35
|
+
process.stdout.write(`check-draft-guard: ${paths.length} file(s), ${summary}\n`)
|
|
36
|
+
}
|
|
37
|
+
return findings.length === 0 ? 0 : 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.exitCode = main(process.argv.slice(2))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { collectFiles } from "./collect-files"
|
|
6
|
+
|
|
7
|
+
// Owner test for the file-walk contract both perfection CLIs share (check-accent-stripes.ts, check-draft-guard.ts).
|
|
8
|
+
function withDir(run: (dir: string) => void): void {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), "collect-files-"))
|
|
10
|
+
try {
|
|
11
|
+
run(dir)
|
|
12
|
+
} finally {
|
|
13
|
+
chmodSync(dir, 0o700)
|
|
14
|
+
rmSync(dir, { recursive: true, force: true })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Permission bits do not stop root, and Windows ignores chmod, so the unreadable-directory case cannot be built there.
|
|
19
|
+
const cannotBuildUnreadableDir = process.platform === "win32" || process.getuid?.() === 0
|
|
20
|
+
|
|
21
|
+
describe("#given a scanned directory", () => {
|
|
22
|
+
test("#when it holds a dangling symlink #then the link is skipped and the rest is collected", () => {
|
|
23
|
+
withDir((dir) => {
|
|
24
|
+
symlinkSync(join(dir, "missing-target"), join(dir, "broken-link.tsx"))
|
|
25
|
+
writeFileSync(join(dir, "page.html"), "<p>ok</p>\n")
|
|
26
|
+
expect(collectFiles([dir])).toEqual({ paths: [join(dir, "page.html")], missing: [] })
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test.skipIf(cannotBuildUnreadableDir)("#when a nested directory cannot be listed #then it is skipped and the rest is collected", () => {
|
|
31
|
+
withDir((dir) => {
|
|
32
|
+
const locked = join(dir, "locked")
|
|
33
|
+
mkdirSync(locked)
|
|
34
|
+
writeFileSync(join(locked, "hidden.css"), ".a {}\n")
|
|
35
|
+
writeFileSync(join(dir, "page.html"), "<p>ok</p>\n")
|
|
36
|
+
chmodSync(locked, 0o000)
|
|
37
|
+
try {
|
|
38
|
+
expect(collectFiles([dir])).toEqual({ paths: [join(dir, "page.html")], missing: [] })
|
|
39
|
+
} finally {
|
|
40
|
+
chmodSync(locked, 0o700)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("#when an explicit target does not exist #then it is reported as missing", () => {
|
|
46
|
+
withDir((dir) => {
|
|
47
|
+
expect(collectFiles([join(dir, "nope")])).toEqual({ paths: [], missing: [join(dir, "nope")] })
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readdirSync, statSync, type Stats } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
|
|
4
|
+
const SCANNED = /\.(?:css|scss|less|pcss|postcss|html?|[cm]?[jt]sx?|vue|svelte|astro)$/i
|
|
5
|
+
const SKIPPED_DIRS = new Set([
|
|
6
|
+
"node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", ".svelte-kit", ".astro",
|
|
7
|
+
".turbo", ".cache", "coverage", "vendor", ".vercel", ".output",
|
|
8
|
+
])
|
|
9
|
+
const MAX_BYTES = 1_000_000
|
|
10
|
+
|
|
11
|
+
function statOrNull(path: string): Stats | null {
|
|
12
|
+
try {
|
|
13
|
+
return statSync(path)
|
|
14
|
+
} catch {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function entriesOrNull(path: string): string[] | null {
|
|
20
|
+
try {
|
|
21
|
+
return readdirSync(path)
|
|
22
|
+
} catch {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Lintable source files under the targets; dangling links and unreadable entries are skipped, an unreadable explicit target is reported. */
|
|
28
|
+
export function collectFiles(targets: readonly string[]): { readonly paths: string[]; readonly missing: string[] } {
|
|
29
|
+
const paths: string[] = []
|
|
30
|
+
const missing: string[] = []
|
|
31
|
+
const walk = (path: string, explicit: boolean): void => {
|
|
32
|
+
const stat = statOrNull(path)
|
|
33
|
+
if (!stat) {
|
|
34
|
+
if (explicit) missing.push(path)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
if (stat.isDirectory()) {
|
|
38
|
+
const entries = entriesOrNull(path)
|
|
39
|
+
if (entries === null) {
|
|
40
|
+
if (explicit) missing.push(path)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (!SKIPPED_DIRS.has(entry)) walk(join(path, entry), false)
|
|
45
|
+
}
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
const minified = /\.min\.[a-z]+$/i.test(path) || path.endsWith(".d.ts")
|
|
49
|
+
if (SCANNED.test(path) && !minified && stat.size <= MAX_BYTES) paths.push(path)
|
|
50
|
+
}
|
|
51
|
+
for (const target of targets) walk(target, true)
|
|
52
|
+
return { paths, missing }
|
|
53
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type DraftRuleId = "promo-copy" | "emoji-icon" | "decorative-badge" | "vanity-stat"
|
|
2
|
+
|
|
3
|
+
export type DraftFinding = {
|
|
4
|
+
readonly file: string
|
|
5
|
+
readonly line: number
|
|
6
|
+
readonly column: number
|
|
7
|
+
readonly rule: DraftRuleId
|
|
8
|
+
readonly excerpt: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const DRAFT_RULE_MESSAGES: Readonly<Record<DraftRuleId, string>> = {
|
|
12
|
+
"promo-copy": "promotional filler in visible copy; say what the screen does or shows instead",
|
|
13
|
+
"emoji-icon": "emoji used as an icon or bullet; use an SVG icon from the project's icon set, or nothing",
|
|
14
|
+
"decorative-badge": "badge or chip carrying a hype label instead of a state; remove it or show a real status",
|
|
15
|
+
"vanity-stat": "stat tile with a marketing-form number (10x, 500+, 24/7); show the measured figure with its unit and period, or remove the tile",
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { lineLocator } from "../accent-stripes/findings"
|
|
2
|
+
import type { SourceFile } from "../accent-stripes/lint"
|
|
3
|
+
import type { DraftFinding } from "./findings"
|
|
4
|
+
import { checkNode } from "./rules"
|
|
5
|
+
import { type MarkupKind, textNodes } from "./text-nodes"
|
|
6
|
+
|
|
7
|
+
function markupKind(path: string): MarkupKind | null {
|
|
8
|
+
if (/\.html?$/i.test(path)) return "html"
|
|
9
|
+
if (/\.[cm]?[jt]sx$/i.test(path)) return "jsx"
|
|
10
|
+
if (/\.(?:vue|svelte|astro)$/i.test(path)) return "sfc"
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function excerptAt(text: string, offset: number): string {
|
|
15
|
+
const lineStart = text.lastIndexOf("\n", offset - 1) + 1
|
|
16
|
+
const lineEnd = text.indexOf("\n", offset)
|
|
17
|
+
return text.slice(lineStart, lineEnd < 0 ? undefined : lineEnd).trim().slice(0, 140)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function lintDraft(files: readonly SourceFile[]): DraftFinding[] {
|
|
21
|
+
const findings: DraftFinding[] = []
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const kind = markupKind(file.path)
|
|
24
|
+
if (kind === null) continue
|
|
25
|
+
const locate = lineLocator(file.text)
|
|
26
|
+
const seen = new Set<string>()
|
|
27
|
+
for (const node of textNodes(file.text, kind)) {
|
|
28
|
+
for (const hit of checkNode(node)) {
|
|
29
|
+
const at = locate(hit.offset)
|
|
30
|
+
const key = `${hit.offset}:${hit.rule}`
|
|
31
|
+
if (seen.has(key)) continue
|
|
32
|
+
seen.add(key)
|
|
33
|
+
findings.push({ file: file.path, ...at, rule: hit.rule, excerpt: excerptAt(file.text, hit.offset) })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column)
|
|
38
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { DraftRuleId } from "./findings"
|
|
2
|
+
import { type Element, isStatElement, type TextNode } from "./text-nodes"
|
|
3
|
+
|
|
4
|
+
export type RuleHit = { readonly rule: DraftRuleId; readonly offset: number }
|
|
5
|
+
|
|
6
|
+
const PROMO_PHRASES = new RegExp(
|
|
7
|
+
[
|
|
8
|
+
String.raw`\bseamless(?:ly)?\b`,
|
|
9
|
+
String.raw`\beffortless(?:ly)?\b`,
|
|
10
|
+
String.raw`\bsupercharge[sd]?\b`,
|
|
11
|
+
String.raw`\bunlock (?:the (?:full )?power|the full potential|your (?:full )?potential)\b`,
|
|
12
|
+
String.raw`\bat a glance\b`,
|
|
13
|
+
String.raw`\bcutting[- ]edge\b`,
|
|
14
|
+
String.raw`\bgame[- ]chang(?:er|ing)\b`,
|
|
15
|
+
String.raw`\brevolutioni[sz]e\b`,
|
|
16
|
+
String.raw`\bnext[- ]level\b`,
|
|
17
|
+
String.raw`\bworld[- ]class\b`,
|
|
18
|
+
String.raw`\belevate your\b`,
|
|
19
|
+
"νλμ",
|
|
20
|
+
"μμ½κ²",
|
|
21
|
+
"μ€λ§νΈν",
|
|
22
|
+
"νμ μ μΈ",
|
|
23
|
+
].join("|"),
|
|
24
|
+
"giu",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const EMOJI = /\p{Emoji_Presentation}|\p{Extended_Pictographic}\uFE0F/u
|
|
28
|
+
const EMOJI_ALL = /\p{Emoji_Presentation}|\p{Extended_Pictographic}\uFE0F?/gu
|
|
29
|
+
const NUMERIC_ENTITY = /&#(?:x([0-9a-f]+)|(\d+));/gi
|
|
30
|
+
const BADGE_CLASS = /^(?:badge|chip|pill|tag)(?:$|[-_])/
|
|
31
|
+
const BADGE_COMPONENT = /^(?:Badge|Chip|Pill|Tag)$/
|
|
32
|
+
const HYPE_LABEL = /^(?:new!?|hot|trending|popular|featured|ai|ai[- ]powered|premium|recommended|best|μ κ·|μΈκΈ°|μΆμ²|λ² μ€νΈ)$/i
|
|
33
|
+
const AMOUNT = String.raw`(?:\d{1,3}(?:[.,]\d{3})+|\d+)(?:[.,]\d+)?`
|
|
34
|
+
const MARKETING_NUMBER = new RegExp(String.raw`^(?:[$β¬Β£Β₯β©]?${AMOUNT}\s*(?:[kmb]|%)?\s*\+|${AMOUNT}\s*[xΓ]|24\/7)$`, "i")
|
|
35
|
+
|
|
36
|
+
const isBadge = (element: Element | undefined): boolean =>
|
|
37
|
+
element !== undefined && !element.stateful && (BADGE_COMPONENT.test(element.name) || element.classes.some((c) => BADGE_CLASS.test(c)))
|
|
38
|
+
|
|
39
|
+
function leadingOffset(node: TextNode): number {
|
|
40
|
+
return node.offset + (node.text.length - node.text.trimStart().length)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function emojiOffset(text: string): number | null {
|
|
44
|
+
const direct = EMOJI.exec(text)
|
|
45
|
+
if (direct) return direct.index
|
|
46
|
+
for (const m of text.matchAll(NUMERIC_ENTITY)) {
|
|
47
|
+
const code = m[1] === undefined ? Number(m[2]) : Number.parseInt(m[1], 16)
|
|
48
|
+
if (Number.isInteger(code) && code <= 0x10ffff && EMOJI.test(String.fromCodePoint(code))) return m.index ?? 0
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A stat is a vanity number only in marketing form (10x, 500+, $2M+, 24/7); a measured figure such as 42% or 1.2k passes. */
|
|
54
|
+
export function checkNode(node: TextNode): RuleHit[] {
|
|
55
|
+
const hits: RuleHit[] = []
|
|
56
|
+
const label = node.text.trim()
|
|
57
|
+
if (node.kind !== "value") {
|
|
58
|
+
for (const m of node.text.matchAll(PROMO_PHRASES)) hits.push({ rule: "promo-copy", offset: node.offset + (m.index ?? 0) })
|
|
59
|
+
const emoji = emojiOffset(node.text)
|
|
60
|
+
if (emoji !== null) hits.push({ rule: "emoji-icon", offset: node.offset + emoji })
|
|
61
|
+
}
|
|
62
|
+
if (node.kind === "text" && isBadge(node.stack.at(-1)) && HYPE_LABEL.test(label.replace(EMOJI_ALL, "").trim())) {
|
|
63
|
+
hits.push({ rule: "decorative-badge", offset: leadingOffset(node) })
|
|
64
|
+
}
|
|
65
|
+
if (node.kind !== "label" && node.stack.some(isStatElement) && MARKETING_NUMBER.test(label)) {
|
|
66
|
+
hits.push({ rule: "vanity-stat", offset: leadingOffset(node) })
|
|
67
|
+
}
|
|
68
|
+
return hits
|
|
69
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export type Element = {
|
|
2
|
+
readonly tag: string
|
|
3
|
+
readonly name: string
|
|
4
|
+
readonly classes: readonly string[]
|
|
5
|
+
readonly stateful: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** `text` is visible copy, `label` an announced attribute (aria-label, title, alt, placeholder), `value` a stat component's value prop. */
|
|
9
|
+
export type NodeKind = "text" | "label" | "value"
|
|
10
|
+
export type TextNode = { readonly kind: NodeKind; readonly text: string; readonly offset: number; readonly stack: readonly Element[] }
|
|
11
|
+
|
|
12
|
+
export type MarkupKind = "html" | "jsx" | "sfc"
|
|
13
|
+
|
|
14
|
+
const NEVER_RENDERED = /<(script|style|pre|code|textarea|noscript)\b[^>]*>[\s\S]*?<\/\1\s*>|<!--[\s\S]*?-->|\{\/\*[\s\S]*?\*\/\}/gi
|
|
15
|
+
const HTML_TEMPLATE = /<template\b(?![^>]*\s(?:x-[\w-]+|v-[\w-]+|[:@#][\w.-]+))[^>]*>[\s\S]*?<\/template\s*>/gi
|
|
16
|
+
const TAG = /<(\/?)([A-Za-z][\w.:-]*)((?:[^>"'{}]|"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\})*)>/g
|
|
17
|
+
const CLASS_ATTR = /\b(?:class|className)\s*=\s*(?:"([^"]*)"|'([^']*)'|\{((?:[^{}]|\{[^{}]*\})*)\})/
|
|
18
|
+
const STRING_LITERAL = /["'`]([^"'`]*)["'`]/g
|
|
19
|
+
const ANNOUNCED_NAME = /(?:^|\s)(?:aria-label|title|alt|placeholder)\s*=/g
|
|
20
|
+
const VALUE_NAME = /(?:^|\s)value\s*=/
|
|
21
|
+
const LITERAL_VALUE = /^\s*(?:"([^"]*)"|'([^']*)'|\{\s*["'`]([^"'`]*)["'`]\s*\})/
|
|
22
|
+
const HIDDEN_ATTR = /(?:^|\s)hidden(?=\s|=|\/|$)/
|
|
23
|
+
const STATE_ATTR = /(?:^|\s)(?:data-status|data-state|aria-(?:current|selected|pressed|checked|expanded|busy|invalid|disabled|valuenow))\s*=/
|
|
24
|
+
const ATTR_VALUES = /"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}/g
|
|
25
|
+
const STAT_COMPONENT = /^(?:Stats?|Metrics?|Kpis?|KPIs?)(?:[A-Z]\w*)?$/
|
|
26
|
+
const STAT_CLASS = /^(?:stat|stats|metric|metrics|kpi)(?:$|[-_])/
|
|
27
|
+
const VOID_TAGS = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"])
|
|
28
|
+
const JSX_LEAD = new Set(["(", "[", "{", "}", ",", ":", "?", "=", ">", "&", "|", ";"])
|
|
29
|
+
|
|
30
|
+
function blank(text: string, pattern: RegExp): string {
|
|
31
|
+
return text.replace(pattern, (m) => m.replace(/[^\n]/g, " "))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function classesOf(attrs: string): string[] {
|
|
35
|
+
const m = CLASS_ATTR.exec(attrs)
|
|
36
|
+
if (!m) return []
|
|
37
|
+
const literal = m[1] ?? m[2]
|
|
38
|
+
const raw = literal ?? [...(m[3] ?? "").matchAll(STRING_LITERAL)].map((s) => s[1] ?? "").join(" ")
|
|
39
|
+
return raw.split(/\s+/).filter(Boolean)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isStatElement(element: Element): boolean {
|
|
43
|
+
return STAT_COMPONENT.test(element.name) || element.classes.some((c) => STAT_CLASS.test(c))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function closeTo(stack: Element[], tag: string): void {
|
|
47
|
+
const at = stack.map((e) => e.tag).lastIndexOf(tag)
|
|
48
|
+
if (at >= 0) stack.length = at
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** In a script, `<` opens JSX only where an expression can start; `Array<string>`, `pick<T>(` and `a<b` stay code. */
|
|
52
|
+
function opensJsx(text: string, start: number, name: string, attrs: string): boolean {
|
|
53
|
+
if (/^\s*,|^\s+extends\b/.test(attrs)) return false
|
|
54
|
+
if (/^(?:const|in|out)$/.test(name) && /^\s+[A-Za-z_$]/.test(attrs)) return false
|
|
55
|
+
const before = text.slice(0, start).trimEnd()
|
|
56
|
+
if (before === "" || /\b(?:return|yield|default)$/.test(before)) return true
|
|
57
|
+
return JSX_LEAD.has(before.at(-1) ?? "")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function literalAfter(attrs: string, from: number): { readonly value: string; readonly at: number } | null {
|
|
61
|
+
const m = LITERAL_VALUE.exec(attrs.slice(from))
|
|
62
|
+
if (!m) return null
|
|
63
|
+
const value = m[1] ?? m[2] ?? m[3] ?? ""
|
|
64
|
+
return { value, at: from + m[0].lastIndexOf(value) }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function keepLiterals(segment: string): string {
|
|
68
|
+
return segment.replace(/\{[^{}]*\}/g, (m) => {
|
|
69
|
+
const lit = /^\{\s*(["'`])([^"'`]*)\1\s*\}$/.exec(m)
|
|
70
|
+
if (!lit) return " ".repeat(m.length)
|
|
71
|
+
const inner = lit[2] ?? ""
|
|
72
|
+
const at = m.indexOf(lit[1] ?? "") + 1
|
|
73
|
+
return " ".repeat(at) + inner + " ".repeat(m.length - at - inner.length)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Copy a reader sees or hears, each piece with the elements open around it. Code, hidden elements and never-rendered blocks are skipped. */
|
|
78
|
+
export function textNodes(source: string, kind: MarkupKind): TextNode[] {
|
|
79
|
+
const text = kind === "html" ? blank(blank(source, NEVER_RENDERED), HTML_TEMPLATE) : blank(source, NEVER_RENDERED)
|
|
80
|
+
const nodes: TextNode[] = []
|
|
81
|
+
const stack: Element[] = []
|
|
82
|
+
let hiddenDepth = 0
|
|
83
|
+
let cursor = 0
|
|
84
|
+
const flush = (end: number) => {
|
|
85
|
+
if (stack.length === 0 || hiddenDepth > 0) return
|
|
86
|
+
const segment = keepLiterals(text.slice(cursor, end))
|
|
87
|
+
if (segment.trim()) nodes.push({ kind: "text", text: segment, offset: cursor, stack: [...stack] })
|
|
88
|
+
}
|
|
89
|
+
for (const m of text.matchAll(TAG)) {
|
|
90
|
+
const start = m.index ?? 0
|
|
91
|
+
const [whole, closing, rawName = "", attrs = ""] = m
|
|
92
|
+
const end = start + whole.length
|
|
93
|
+
if (kind === "jsx" && !closing && stack.length === 0 && !opensJsx(text, start, rawName, attrs)) continue
|
|
94
|
+
flush(start)
|
|
95
|
+
cursor = end
|
|
96
|
+
const tag = rawName.toLowerCase()
|
|
97
|
+
if (closing) {
|
|
98
|
+
const at = stack.map((e) => e.tag).lastIndexOf(tag)
|
|
99
|
+
if (at >= 0 && hiddenDepth > at) hiddenDepth = 0
|
|
100
|
+
closeTo(stack, tag)
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
const names = attrs.replace(ATTR_VALUES, (v) => " ".repeat(v.length))
|
|
104
|
+
const element: Element = { tag, name: rawName, classes: classesOf(attrs), stateful: STATE_ATTR.test(names) }
|
|
105
|
+
const attrsAt = start + 1 + rawName.length
|
|
106
|
+
const hidden = hiddenDepth > 0 || HIDDEN_ATTR.test(names)
|
|
107
|
+
if (!hidden) {
|
|
108
|
+
for (const a of names.matchAll(ANNOUNCED_NAME)) {
|
|
109
|
+
const lit = literalAfter(attrs, (a.index ?? 0) + a[0].length)
|
|
110
|
+
if (lit?.value.trim()) nodes.push({ kind: "label", text: lit.value, offset: attrsAt + lit.at, stack: [...stack, element] })
|
|
111
|
+
}
|
|
112
|
+
const v = isStatElement(element) ? VALUE_NAME.exec(names) : null
|
|
113
|
+
const lit = v ? literalAfter(attrs, v.index + v[0].length) : null
|
|
114
|
+
if (lit?.value.trim()) nodes.push({ kind: "value", text: lit.value, offset: attrsAt + lit.at, stack: [...stack, element] })
|
|
115
|
+
}
|
|
116
|
+
if (VOID_TAGS.has(tag) || attrs.trimEnd().endsWith("/")) continue
|
|
117
|
+
stack.push(element)
|
|
118
|
+
if (hiddenDepth === 0 && HIDDEN_ATTR.test(names)) hiddenDepth = stack.length
|
|
119
|
+
}
|
|
120
|
+
return nodes
|
|
121
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function Bar() {
|
|
2
|
+
return (
|
|
3
|
+
<div data-title="Supercharge your team">
|
|
4
|
+
<StatusBar><span>24/7</span></StatusBar>
|
|
5
|
+
<span className="badge" aria-current="page">New</span>
|
|
6
|
+
<div data-x="a title='Supercharge your team'" />
|
|
7
|
+
<Stat data-value="10x" />
|
|
8
|
+
</div>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function Summary({ count, status, uptime }: { count: number; status: string; uptime: string }) {
|
|
2
|
+
return (
|
|
3
|
+
<section>
|
|
4
|
+
<div className="stat"><span className="stat-value">{count}</span><span>open tickets</span></div>
|
|
5
|
+
<div className="metric">{uptime}</div>
|
|
6
|
+
<span className="badge" data-status={status}>{status}</span>
|
|
7
|
+
<span className="badge">3</span>
|
|
8
|
+
<span className="chip">Failed</span>
|
|
9
|
+
<p>Seams in the fabric are reinforced.</p>
|
|
10
|
+
</section>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const labels: Array<string> = ["Unlock the power", "π launch"]
|
|
2
|
+
export const pick = <T,>(xs: T[]): T | undefined => xs[0]
|
|
3
|
+
export const first = <T extends object>(xs: T[]) => xs[0]
|
|
4
|
+
export const flag = (a: number, b: number) => (a<b && b>a ? "seamless sync" : "x")
|
|
5
|
+
export function Saved() {
|
|
6
|
+
return <p>Saved 3 files</p>
|
|
7
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<main>
|
|
2
|
+
<h1>Monthly revenue</h1>
|
|
3
|
+
<p>Revenue rose 4% after the refund fix shipped on 3 June.</p>
|
|
4
|
+
<pre><code>const seamless = "Supercharge"</code></pre>
|
|
5
|
+
<footer>Β© 2026 Example Inc. Next page β</footer>
|
|
6
|
+
<script>const label = "Unlock the power of your data π"</script>
|
|
7
|
+
<style>.stat::after { content: "10x"; }</style>
|
|
8
|
+
<!-- νλμ 보λ λ©λͺ¨ -->
|
|
9
|
+
</main>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<main>
|
|
2
|
+
<p>Unlock your account to continue.</p>
|
|
3
|
+
<p>μ΅μ μ κ²½λ‘λ₯Ό κ³μ°ν©λλ€.</p>
|
|
4
|
+
<p>Docs β</p>
|
|
5
|
+
<p>βΆ Play</p>
|
|
6
|
+
<li>Inbox <span class="badge" data-status="unread">New</span></li>
|
|
7
|
+
<span class="pill">Pro</span>
|
|
8
|
+
<div hidden>Seamless onboarding</div>
|
|
9
|
+
<noscript>Supercharge your browser</noscript>
|
|
10
|
+
<template><p>Unlock the power of templates</p></template>
|
|
11
|
+
</main>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<main>
|
|
2
|
+
<div class="block hidden md:flex"><p>Seamless sync</p></div>
|
|
3
|
+
<button title="Show hidden files"><span>Supercharge your team</span></button>
|
|
4
|
+
<div class="stat"><span>1,000,000+</span></div>
|
|
5
|
+
<span class="badge" aria-hidden="true">New</span>
|
|
6
|
+
<template x-if="open"><p>Effortless setup</p></template>
|
|
7
|
+
</main>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const cn = (...names: string[]) => names.join(" ")
|
|
2
|
+
export function Page() {
|
|
3
|
+
return (
|
|
4
|
+
<main>
|
|
5
|
+
<Badge>New</Badge>
|
|
6
|
+
<span className={cn("badge", "badge--hot")}>Hot</span>
|
|
7
|
+
<span className="badge">β¨ New</span>
|
|
8
|
+
<p>Launch 🚀 today</p>
|
|
9
|
+
<button type="button" aria-label="π Launch">Go</button>
|
|
10
|
+
<Stat value="10x" />
|
|
11
|
+
<StatCard><p>10x</p></StatCard>
|
|
12
|
+
<div className="metric">{"500+"}</div>
|
|
13
|
+
<input placeholder="Supercharge your search" />
|
|
14
|
+
</main>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const Beta = () => <p>(beta) Seamless sync</p>
|