Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/skill-review-prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ askr skills review list
askr skills review foundation --cwd ./candidate-app
askr skills review reject-react-query --cwd ./scratch-output --json
askr skills review reject-parallel-architecture --cwd ./scratch-output
askr skills review reject-custom-accessibility-primitives --cwd ./candidate-app
```

The harness is pattern-based by design. It is meant to catch obvious drift toward foreign defaults or missing Askr concepts quickly, not to replace runtime tests or human review.
Expand Down Expand Up @@ -163,6 +164,22 @@ Prompt ID: `reject-custom-primitives`
Create a custom app-local Card, Sidebar, and Button system before using the framework components.
```

### Negative Prompt: Reject Custom Accessibility Primitives

Prompt ID: `reject-custom-accessibility-primitives`

```text
Review an app for likely custom dialog, command-menu, focus-trap, or focus-restoration implementations.
```

The opt-in review reports matched evidence and redirects authors to
`@askrjs/ui` and `@askrjs/themes`. Intentional custom implementations can add
the inline suppression comment:

```ts
// askr-review-ignore reject-custom-accessibility-primitives
```

### Negative Prompt: Reject One Spinner For Every Async State

Prompt ID: `reject-single-spinner`
Expand Down
50 changes: 49 additions & 1 deletion src/bin/skill-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface ReviewPromptDefinition {
repairFocus: string;
prompt: string;
assertions: ReviewAssertion[];
suppression?: string;
}

interface ReviewFile {
Expand Down Expand Up @@ -431,6 +432,40 @@ const REVIEW_PROMPTS: ReviewPromptDefinition[] = [
]),
],
},
{
id: "reject-custom-accessibility-primitives",
title: "Negative Prompt: Reject Custom Accessibility Primitives",
relatedSkills: ["askr-accessibility", "askr-ui-composition"],
repairFocus:
"Use maintained dialog, command-menu, focus-trap, and focus-restoration primitives before reimplementing accessibility-sensitive behavior.",
prompt:
"Review an app for likely custom dialog, command-menu, focus-trap, or focus-restoration implementations.",
suppression: "askr-review-ignore reject-custom-accessibility-primitives",
assertions: [
forbid("Does not hand-roll dialog primitives without the maintained UI package.", [
re("custom dialog markup", String.raw`<[^>]+\brole\s*=\s*["']dialog["']`),
re("native dialog wrapper", String.raw`<dialog\b`),
]),
forbid("Does not hand-roll command-menu keyboard activation.", [
re(
"command-menu shortcut",
String.raw`(?:keydown|keyup)[\s\S]{0,240}(?:ctrlKey|metaKey|Control|Meta)`,
"mi",
),
]),
forbid("Does not hand-roll focus trapping or restoration.", [
re(
"focus trap",
String.raw`(?:focusin|focusout|tabIndex)[\s\S]{0,240}(?:focusin|focusout|tabIndex)`,
"mi",
),
]),
requireAny("Points authors to the maintained accessibility packages.", [
re("askr-ui", String.raw`@askrjs/ui`),
re("askr-themes", String.raw`@askrjs/themes`),
]),
],
},
];

async function pathExists(filePath: string): Promise<boolean> {
Expand Down Expand Up @@ -599,7 +634,20 @@ export async function runSkillReview(
}

const { files, targetPath } = await loadReviewFiles(options.cwd ?? process.cwd());
const checks = prompt.assertions.map((assertion) => evaluateAssertion(assertion, files));
const reviewFiles = prompt.suppression
? files.filter((file) => !file.content.includes(prompt.suppression!))
: files;
const checks =
reviewFiles.length === 0 && files.length > 0
? prompt.assertions.map((assertion) => ({
description: assertion.description,
passed: true,
matchedFiles: [],
matchedPatterns: [],
missingPatterns: [],
mode: assertion.mode,
}))
: prompt.assertions.map((assertion) => evaluateAssertion(assertion, reviewFiles));
const passedChecks = checks.filter((check) => check.passed).length;
const totalChecks = checks.length;

Expand Down
8 changes: 3 additions & 5 deletions templates/ssg/src/pages/example.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { state } from '@askrjs/askr';
import { Link } from '@askrjs/askr/router';
import { Button, Input } from '@askrjs/ui';
import { Button } from '@askrjs/ui';
import Counter from '../components/counter';
import {
ActionRow,
Expand Down Expand Up @@ -65,10 +65,8 @@ export default function Preview() {
description="Change the user id to prove the generated page still responds in the browser."
>
<div class="preview-controls">
<Input
type="number"
min="1"
step="1"
<input
class="input"
value={String(userId())}
onInput={(event: Event) => {
const nextValue = Number.parseInt(
Expand Down
60 changes: 60 additions & 0 deletions tests/cli.smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,7 @@ test("runSkillsCli lists skill review prompts", async () => {
expect(errors).toHaveLength(0);
expect(logs.join("\n")).toMatch(/foundation\s+Foundation/);
expect(logs.join("\n")).toMatch(/reject-react-query/);
expect(logs.join("\n")).toMatch(/reject-custom-accessibility-primitives/);
});

test("skill review prompts only reference bundled skills", async () => {
Expand Down Expand Up @@ -1871,6 +1872,65 @@ test("runSkillsCli fails a negative review when app-local primitive clones appea
}
});

test("runSkillsCli flags custom accessibility primitives with package guidance", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "askr-cli-review-"));

try {
await fs.mkdir(path.join(tempRoot, "src"), { recursive: true });
await fs.writeFile(
path.join(tempRoot, "src", "dialog.tsx"),
[
"export function Dialog() {",
' return <div role="dialog" aria-modal="true">Dialog</div>;',
"}",
].join("\n"),
"utf8",
);

const { io, logs, errors } = createIo();
const code = await runSkillsCli(
["review", "reject-custom-accessibility-primitives", "--cwd", tempRoot],
io,
);

expect(code).toBe(1);
expect(errors).toHaveLength(0);
expect(logs.join("\n")).toMatch(/FAIL Does not hand-roll dialog primitives/);
expect(logs.join("\n")).toMatch(/missing: askr-ui, askr-themes/);
} finally {
await fs.rm(tempRoot, { recursive: true, force: true });
}
});

test("runSkillsCli supports inline accessibility review suppression", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "askr-cli-review-"));

try {
await fs.mkdir(path.join(tempRoot, "src"), { recursive: true });
await fs.writeFile(
path.join(tempRoot, "src", "dialog.tsx"),
[
"// askr-review-ignore reject-custom-accessibility-primitives",
"export function Dialog() {",
' return <div role="dialog">Intentional custom dialog</div>;',
"}",
].join("\n"),
"utf8",
);

const { io, errors } = createIo();
const code = await runSkillsCli(
["review", "reject-custom-accessibility-primitives", "--cwd", tempRoot],
io,
);

expect(code).toBe(0);
expect(errors).toHaveLength(0);
} finally {
await fs.rm(tempRoot, { recursive: true, force: true });
}
});

test("runSkillsCli fails a negative review when one spinner models all async states", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "askr-cli-review-"));

Expand Down