@asrunjs/asrun 0.6.0 → 0.7.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/.claude/settings.json +5 -5
- package/.claude/skills/exploring-cases/references/assertion-targets.md +103 -103
- package/.claude/skills/exploring-cases/references/handoff-card.js +128 -128
- package/.claude/skills/making-cases/references/gaps.md +36 -36
- package/.claude/skills/making-cases/references/splitting.md +28 -28
- package/AGENTS.md +3 -0
- package/README.md +16 -2
- package/bin/asrun.mjs +2 -1
- package/dist/scripts/cli.js +14 -6
- package/dist/scripts/doctor.js +7 -2
- package/dist/scripts/go.mjs +36 -6
- package/dist/scripts/heal-evidence.mjs +149 -143
- package/dist/scripts/init.d.ts +9 -1
- package/dist/scripts/init.js +101 -26
- package/dist/scripts/reset.d.ts +1 -0
- package/dist/scripts/reset.js +233 -0
- package/dist/scripts/run.js +23 -23
- package/docs/agents/framework-dev.md +29 -1
- package/docs/commands.md +25 -8
- package/docs/install.md +52 -10
- package/docs/troubleshooting.md +31 -4
- package/package.json +34 -30
- package/templates/.claude/settings.json +5 -5
- package/templates/CLAUDE.md +1 -1
- package/templates/gitignore +13 -13
- package/templates/package.json +33 -31
- package/templates/playwright.config.ts +3 -3
- package/templates/pnpm-workspace.yaml +8 -5
- package/templates/seed.spec.ts +9 -9
- package/templates/tsconfig.json +14 -14
- package/ui/dist/favicon.svg +11 -11
- package/ui/dist/logos/claude.svg +1 -1
- package/ui/dist/logos/opencode.svg +1 -1
package/.claude/settings.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
{
|
|
2
|
-
"sandbox": {
|
|
3
|
-
"excludedCommands": ["pnpm asrun *", "pnpm status *", "pnpm heal:evidence *", "tail *", "head *"]
|
|
4
|
-
}
|
|
5
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"sandbox": {
|
|
3
|
+
"excludedCommands": ["pnpm asrun *", "pnpm status *", "pnpm heal:evidence *", "tail *", "head *"]
|
|
4
|
+
}
|
|
5
|
+
}
|
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
# Assertion targets: never guess what a check looks at
|
|
2
|
-
|
|
3
|
-
Shared by `exploring-cases`, `generating-specs` and healing. It is binding in all three.
|
|
4
|
-
|
|
5
|
-
A check that looks at an image, a logo, a file, a piece of text or a place on the page must say **exactly which one**.
|
|
6
|
-
"The logo", "the header", "the first image" are not targets. A spec built on a guessed target passes or fails for
|
|
7
|
-
the wrong reason: a baseline once saved a content image instead of a logo, and a composer check once looked at a
|
|
8
|
-
close icon instead of the logo.
|
|
9
|
-
|
|
10
|
-
**When in doubt, ask. Never pick.** A wrong guess that passes is worse than a question.
|
|
11
|
-
|
|
12
|
-
## The target card
|
|
13
|
-
Every such check gets a card in the case, under `## Targets` (after `## Visual checks`). The checks refer to it as
|
|
14
|
-
`[T1]`, `[T2]`, ... (in "Expected", "Visual checks" and in steps).
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
## Targets
|
|
18
|
-
### T1 Preview logo
|
|
19
|
-
- What: the product logo (the ACME wordmark), not the avatar or a content image.
|
|
20
|
-
- Where: top-right corner of the preview page (`/preview/view/documents/...`).
|
|
21
|
-
- Identified by: the image with alt text "<observed alt>" inside the page header region (role and name, or test id; never `.first()`).
|
|
22
|
-
- Not: any element that shows item data (images, amounts, "Content #1").
|
|
23
|
-
- Ready when: the image is visible and its natural width is above 0.
|
|
24
|
-
- Reference example-staging: .explore/targets/documents/published-flow/T1-example-staging.png
|
|
25
|
-
- Reference example-production: .explore/targets/documents/published-flow/T1-example-production.png
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Fields:
|
|
29
|
-
- `What` (required): the thing itself, in words a QA person agrees with. Say what it is and, when a look-alike
|
|
30
|
-
exists on the page, what it is not.
|
|
31
|
-
- `Where` (required): the region of the page, dialog, tab or file page.
|
|
32
|
-
- `Identified by` (required): how it is told apart from every look-alike: role and accessible name, test id (`data-tn`),
|
|
33
|
-
alt text, `title`, or an anchor element and a relation to it. Never a position (`.first()`, `.nth()`, "the first
|
|
34
|
-
image") unless the case's author wrote that exact rule (then quote them).
|
|
35
|
-
- `Not` (recommended): the look-alikes that must not be matched. It is what `/gen-spec` and a reviewer check a baseline against.
|
|
36
|
-
- `Ready when` (required when the content loads after an action): see `readiness.md`.
|
|
37
|
-
- For a PDF or another multi-page file, also `Pages` (`all`, `first`, `last`, `1, 3`, plus the case's own words when it limits
|
|
38
|
-
the pages: see "Which pages are checked" in `readiness.md`; the default is every page) and `Region` (a corner or a box,
|
|
39
|
-
as fractions of the page: `top-left 30% x 12%`). Text that must or must not appear goes under `What` / `Not`.
|
|
40
|
-
- `Reference <profile-id>` (one per profile explored): a screenshot of the page with the target outlined
|
|
41
|
-
(`browser_highlight`, then `browser_take_screenshot`), saved under `.explore/targets/<case-id>/T<n>-<profile-id>.png`.
|
|
42
|
-
`.explore/` is git-ignored, because the pages are logged in. Write `none: <reason>` when it could not be taken.
|
|
43
|
-
A reference is evidence for the reader; it is never a test baseline.
|
|
44
|
-
|
|
45
|
-
`pnpm case:check` fails on a card without What/Where/Identified by and warns on a check about an image, logo, file,
|
|
46
|
-
screenshot or PDF that has no `[T<n>]`.
|
|
47
|
-
|
|
48
|
-
## Which checks need a card
|
|
49
|
-
Any check whose words include logo, image, icon, photo, screenshot, PDF, file, download, color, or "shows <profile>", and
|
|
50
|
-
every check that gets a screenshot baseline or an image/hash comparison. Text checks need a card when the text can
|
|
51
|
-
occur more than once on the page (a product name in a header and in a footer).
|
|
52
|
-
|
|
53
|
-
## When to ask (a `🚫 TODO [blocking]` block, not a guess)
|
|
54
|
-
Ask instead of choosing when ANY of these is true:
|
|
55
|
-
1. **More than one candidate** matches the description (two logos, a logo and a watermark, several images with alt text).
|
|
56
|
-
2. **The only way to find it is by position** (`.first()`, `.nth()`, "the image at the top").
|
|
57
|
-
3. **The only locator is generic**: `img`, `svg`, `img, svg`, `[alt]`, a CSS class chain.
|
|
58
|
-
4. **The area contains dynamic data**: rows, prices, dates, avatars, counts. A screenshot of it is never stable.
|
|
59
|
-
5. **It has no stable identity**: no alt, title, name or test id, so a rename or a reorder silently changes what is checked.
|
|
60
|
-
6. **The case says something different from the page** (the case says "top-right corner", the page has the logo top-left).
|
|
61
|
-
7. **It could not be opened or seen** in this run (a dialog, a file, a tab that needs data that is gone).
|
|
62
|
-
8. **A value is unknown** for a profile (no entry in the case's `## Values`), or the profiles differ where the case
|
|
63
|
-
says "same".
|
|
64
|
-
|
|
65
|
-
The question names the exact thing wanted: image, text, place or UI element. Write it like this, with the candidates
|
|
66
|
-
as evidence, so the person answers by pointing:
|
|
67
|
-
|
|
68
|
-
```
|
|
69
|
-
- The preview shows the product logo in its top-right corner [T1].
|
|
70
|
-
- 🚫 TODO [blocking]: Which element is the logo? Candidates on example-staging: (a) the image with alt "Example" in the page header, (b) the first content image "Content #1", (c) the avatar. (candidates highlighted in .explore/targets/documents/published-flow/T1-candidates-example-staging.png)
|
|
71
|
-
- ✍️ Answer:
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
The parent sentence is the guess, so a `[blocking]` card is never turned into a check. `/gen-spec` writes
|
|
75
|
-
`test.fixme(true, '<the question>')` for it.
|
|
76
|
-
|
|
77
|
-
In an **attended** run, do not write the question first: ask the person to outline the target in the browser with
|
|
78
|
-
`browser_annotate`, then write the card from the annotation (the annotated screenshot is the Reference).
|
|
79
|
-
|
|
80
|
-
## How a card is made (exploring)
|
|
81
|
-
1. Find every candidate: `browser_find` / `browser_snapshot`, and count them. Do not stop at the first match.
|
|
82
|
-
2. Decide with the rules above: exactly one candidate with a stable identity and no dynamic data means a card;
|
|
83
|
-
anything else means a question.
|
|
84
|
-
3. Outline the target with `browser_highlight`, take the screenshot to `.explore/targets/...`, then `browser_hide_highlight`.
|
|
85
|
-
4. **Look at the screenshot** (read the image file) and say in the card what it shows. If it shows something other than
|
|
86
|
-
the card's `What` (a content image instead of a logo), the card is wrong: fix it before going on. This is the step that
|
|
87
|
-
would have caught the wrong baseline.
|
|
88
|
-
5. Do this for every profile that has an entry in the case's `## Values`. A target that differs between profiles is not
|
|
89
|
-
settled: write a `[blocking]` question instead of encoding one profile's locator as if it held for all.
|
|
90
|
-
|
|
91
|
-
## What the spec does with a card (generating)
|
|
92
|
-
- The check is written against `Identified by`, and the line before it is `// target: T<n>`. `pnpm spec:lint` fails a
|
|
93
|
-
screenshot, a logo/image locator or a position pick that has no target comment.
|
|
94
|
-
- A semantic assertion always comes before a pixel one: the element's alt/title/src or role matches, and it is where
|
|
95
|
-
`Where` says, then `toHaveScreenshot` on that element only.
|
|
96
|
-
- A card with `Not:` gets a check that the matched element is not one of those (for example it shows no item text).
|
|
97
|
-
- Screenshot only elements without dynamic data. If the target card cannot give such an element, it is a question.
|
|
98
|
-
|
|
99
|
-
## What healing does with a card
|
|
100
|
-
Before deciding "product changed" or "test is wrong", check **the assertion itself**: does the locator still point to
|
|
101
|
-
what the card's `What` says? Open the baseline and the failure screenshot and compare each with the card. A baseline
|
|
102
|
-
that shows something else is a test bug: delete it and report it (a person creates the new one). A card that no longer
|
|
103
|
-
matches the page is a question for the person, not something to rewrite quietly.
|
|
1
|
+
# Assertion targets: never guess what a check looks at
|
|
2
|
+
|
|
3
|
+
Shared by `exploring-cases`, `generating-specs` and healing. It is binding in all three.
|
|
4
|
+
|
|
5
|
+
A check that looks at an image, a logo, a file, a piece of text or a place on the page must say **exactly which one**.
|
|
6
|
+
"The logo", "the header", "the first image" are not targets. A spec built on a guessed target passes or fails for
|
|
7
|
+
the wrong reason: a baseline once saved a content image instead of a logo, and a composer check once looked at a
|
|
8
|
+
close icon instead of the logo.
|
|
9
|
+
|
|
10
|
+
**When in doubt, ask. Never pick.** A wrong guess that passes is worse than a question.
|
|
11
|
+
|
|
12
|
+
## The target card
|
|
13
|
+
Every such check gets a card in the case, under `## Targets` (after `## Visual checks`). The checks refer to it as
|
|
14
|
+
`[T1]`, `[T2]`, ... (in "Expected", "Visual checks" and in steps).
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
## Targets
|
|
18
|
+
### T1 Preview logo
|
|
19
|
+
- What: the product logo (the ACME wordmark), not the avatar or a content image.
|
|
20
|
+
- Where: top-right corner of the preview page (`/preview/view/documents/...`).
|
|
21
|
+
- Identified by: the image with alt text "<observed alt>" inside the page header region (role and name, or test id; never `.first()`).
|
|
22
|
+
- Not: any element that shows item data (images, amounts, "Content #1").
|
|
23
|
+
- Ready when: the image is visible and its natural width is above 0.
|
|
24
|
+
- Reference example-staging: .explore/targets/documents/published-flow/T1-example-staging.png
|
|
25
|
+
- Reference example-production: .explore/targets/documents/published-flow/T1-example-production.png
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Fields:
|
|
29
|
+
- `What` (required): the thing itself, in words a QA person agrees with. Say what it is and, when a look-alike
|
|
30
|
+
exists on the page, what it is not.
|
|
31
|
+
- `Where` (required): the region of the page, dialog, tab or file page.
|
|
32
|
+
- `Identified by` (required): how it is told apart from every look-alike: role and accessible name, test id (`data-tn`),
|
|
33
|
+
alt text, `title`, or an anchor element and a relation to it. Never a position (`.first()`, `.nth()`, "the first
|
|
34
|
+
image") unless the case's author wrote that exact rule (then quote them).
|
|
35
|
+
- `Not` (recommended): the look-alikes that must not be matched. It is what `/gen-spec` and a reviewer check a baseline against.
|
|
36
|
+
- `Ready when` (required when the content loads after an action): see `readiness.md`.
|
|
37
|
+
- For a PDF or another multi-page file, also `Pages` (`all`, `first`, `last`, `1, 3`, plus the case's own words when it limits
|
|
38
|
+
the pages: see "Which pages are checked" in `readiness.md`; the default is every page) and `Region` (a corner or a box,
|
|
39
|
+
as fractions of the page: `top-left 30% x 12%`). Text that must or must not appear goes under `What` / `Not`.
|
|
40
|
+
- `Reference <profile-id>` (one per profile explored): a screenshot of the page with the target outlined
|
|
41
|
+
(`browser_highlight`, then `browser_take_screenshot`), saved under `.explore/targets/<case-id>/T<n>-<profile-id>.png`.
|
|
42
|
+
`.explore/` is git-ignored, because the pages are logged in. Write `none: <reason>` when it could not be taken.
|
|
43
|
+
A reference is evidence for the reader; it is never a test baseline.
|
|
44
|
+
|
|
45
|
+
`pnpm case:check` fails on a card without What/Where/Identified by and warns on a check about an image, logo, file,
|
|
46
|
+
screenshot or PDF that has no `[T<n>]`.
|
|
47
|
+
|
|
48
|
+
## Which checks need a card
|
|
49
|
+
Any check whose words include logo, image, icon, photo, screenshot, PDF, file, download, color, or "shows <profile>", and
|
|
50
|
+
every check that gets a screenshot baseline or an image/hash comparison. Text checks need a card when the text can
|
|
51
|
+
occur more than once on the page (a product name in a header and in a footer).
|
|
52
|
+
|
|
53
|
+
## When to ask (a `🚫 TODO [blocking]` block, not a guess)
|
|
54
|
+
Ask instead of choosing when ANY of these is true:
|
|
55
|
+
1. **More than one candidate** matches the description (two logos, a logo and a watermark, several images with alt text).
|
|
56
|
+
2. **The only way to find it is by position** (`.first()`, `.nth()`, "the image at the top").
|
|
57
|
+
3. **The only locator is generic**: `img`, `svg`, `img, svg`, `[alt]`, a CSS class chain.
|
|
58
|
+
4. **The area contains dynamic data**: rows, prices, dates, avatars, counts. A screenshot of it is never stable.
|
|
59
|
+
5. **It has no stable identity**: no alt, title, name or test id, so a rename or a reorder silently changes what is checked.
|
|
60
|
+
6. **The case says something different from the page** (the case says "top-right corner", the page has the logo top-left).
|
|
61
|
+
7. **It could not be opened or seen** in this run (a dialog, a file, a tab that needs data that is gone).
|
|
62
|
+
8. **A value is unknown** for a profile (no entry in the case's `## Values`), or the profiles differ where the case
|
|
63
|
+
says "same".
|
|
64
|
+
|
|
65
|
+
The question names the exact thing wanted: image, text, place or UI element. Write it like this, with the candidates
|
|
66
|
+
as evidence, so the person answers by pointing:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
- The preview shows the product logo in its top-right corner [T1].
|
|
70
|
+
- 🚫 TODO [blocking]: Which element is the logo? Candidates on example-staging: (a) the image with alt "Example" in the page header, (b) the first content image "Content #1", (c) the avatar. (candidates highlighted in .explore/targets/documents/published-flow/T1-candidates-example-staging.png)
|
|
71
|
+
- ✍️ Answer:
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The parent sentence is the guess, so a `[blocking]` card is never turned into a check. `/gen-spec` writes
|
|
75
|
+
`test.fixme(true, '<the question>')` for it.
|
|
76
|
+
|
|
77
|
+
In an **attended** run, do not write the question first: ask the person to outline the target in the browser with
|
|
78
|
+
`browser_annotate`, then write the card from the annotation (the annotated screenshot is the Reference).
|
|
79
|
+
|
|
80
|
+
## How a card is made (exploring)
|
|
81
|
+
1. Find every candidate: `browser_find` / `browser_snapshot`, and count them. Do not stop at the first match.
|
|
82
|
+
2. Decide with the rules above: exactly one candidate with a stable identity and no dynamic data means a card;
|
|
83
|
+
anything else means a question.
|
|
84
|
+
3. Outline the target with `browser_highlight`, take the screenshot to `.explore/targets/...`, then `browser_hide_highlight`.
|
|
85
|
+
4. **Look at the screenshot** (read the image file) and say in the card what it shows. If it shows something other than
|
|
86
|
+
the card's `What` (a content image instead of a logo), the card is wrong: fix it before going on. This is the step that
|
|
87
|
+
would have caught the wrong baseline.
|
|
88
|
+
5. Do this for every profile that has an entry in the case's `## Values`. A target that differs between profiles is not
|
|
89
|
+
settled: write a `[blocking]` question instead of encoding one profile's locator as if it held for all.
|
|
90
|
+
|
|
91
|
+
## What the spec does with a card (generating)
|
|
92
|
+
- The check is written against `Identified by`, and the line before it is `// target: T<n>`. `pnpm spec:lint` fails a
|
|
93
|
+
screenshot, a logo/image locator or a position pick that has no target comment.
|
|
94
|
+
- A semantic assertion always comes before a pixel one: the element's alt/title/src or role matches, and it is where
|
|
95
|
+
`Where` says, then `toHaveScreenshot` on that element only.
|
|
96
|
+
- A card with `Not:` gets a check that the matched element is not one of those (for example it shows no item text).
|
|
97
|
+
- Screenshot only elements without dynamic data. If the target card cannot give such an element, it is a question.
|
|
98
|
+
|
|
99
|
+
## What healing does with a card
|
|
100
|
+
Before deciding "product changed" or "test is wrong", check **the assertion itself**: does the locator still point to
|
|
101
|
+
what the card's `What` says? Open the baseline and the failure screenshot and compare each with the card. A baseline
|
|
102
|
+
that shows something else is a test bug: delete it and report it (a person creates the new one). A card that no longer
|
|
103
|
+
matches the page is a question for the person, not something to rewrite quietly.
|
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
async (page) => {
|
|
2
|
-
// Floating "YOUR TURN" card for a handoff to a person. Do not copy, shorten or rewrite this file: load it with
|
|
3
|
-
// browser_run_code_unsafe({filename}) after setting the options (see user-handoff.md, "The wait loop").
|
|
4
|
-
const ctx = page.context();
|
|
5
|
-
const cfg = ctx.__handoffCfg || {};
|
|
6
|
-
const message = cfg.message || 'Do the step, then press Done';
|
|
7
|
-
const copy = Array.isArray(cfg.copy) ? cfg.copy : [];
|
|
8
|
-
const doneText = cfg.doneText || null;
|
|
9
|
-
const maxMs = cfg.maxMs || 600000;
|
|
10
|
-
await ctx.grantPermissions(['clipboard-read', 'clipboard-write']).catch(() => {});
|
|
11
|
-
|
|
12
|
-
const paint = (p) => p.evaluate(({message, copy}) => {
|
|
13
|
-
if (document.getElementById('__ASRUN_panel')) return;
|
|
14
|
-
const KEY = '__ASRUN_panel_state';
|
|
15
|
-
let state = {minimized: false, left: null, top: null};
|
|
16
|
-
try { state = {...state, ...JSON.parse(sessionStorage.getItem(KEY) || '{}')}; } catch (e) {}
|
|
17
|
-
const save = () => { try { sessionStorage.setItem(KEY, JSON.stringify(state)); } catch (e) {} };
|
|
18
|
-
const icon = (body) => '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + body + '</svg>';
|
|
19
|
-
const ICON = {
|
|
20
|
-
copy: icon('<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>'),
|
|
21
|
-
check: icon('<polyline points="20 6 9 17 4 12"/>'),
|
|
22
|
-
up: icon('<polyline points="18 15 12 9 6 15"/>'),
|
|
23
|
-
down: icon('<polyline points="6 9 12 15 18 9"/>'),
|
|
24
|
-
move: icon('<polyline points="5 9 2 12 5 15"/><polyline points="9 5 12 2 15 5"/><polyline points="15 19 12 22 9 19"/><polyline points="19 9 22 12 19 15"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="12" y1="2" x2="12" y2="22"/>'),
|
|
25
|
-
};
|
|
26
|
-
const host = document.createElement('div');
|
|
27
|
-
host.id = '__ASRUN_panel';
|
|
28
|
-
host.style.cssText = 'all:initial;position:fixed;z-index:2147483647;left:16px;bottom:16px';
|
|
29
|
-
const root = host.attachShadow({mode: 'open'});
|
|
30
|
-
root.innerHTML = '<style>' +
|
|
31
|
-
'.card{width:300px;max-width:calc(100vw - 32px);background:#1e3340;color:#fff;font:13px/1.4 system-ui,sans-serif;border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.35);overflow:hidden}' +
|
|
32
|
-
'.card.min{width:auto}.card.ok{background:#15803d}' +
|
|
33
|
-
'.bar{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 12px;background:#b45309;cursor:grab;user-select:none;touch-action:none}' +
|
|
34
|
-
'.ok .bar{background:#166534;align-items:flex-start;padding-top:9px}.bar b{flex:1;min-width:0;font-size:12px;letter-spacing:.04em;white-space:nowrap}.ok .bar b{white-space:normal;line-height:1.35}.grip{display:inline-flex;opacity:.8}' +
|
|
35
|
-
'button{all:unset;box-sizing:border-box;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:3px 8px;border-radius:4px;background:rgba(255,255,255,.18);color:#fff;font:600 12px system-ui,sans-serif}' +
|
|
36
|
-
'button:hover{background:rgba(255,255,255,.3)}' +
|
|
37
|
-
'.body{padding:10px 12px 12px;display:flex;flex-direction:column;gap:8px}.min .body,.ok .body{display:none}' +
|
|
38
|
-
'.mini-done{display:none}.min .mini-done{display:inline-flex}' +
|
|
39
|
-
'.chip{display:flex;align-items:center;gap:6px;background:rgba(0,0,0,.28);padding:4px 4px 4px 8px;border-radius:4px}' +
|
|
40
|
-
'.chip span{flex:1;font:12px ui-monospace,Menlo,monospace;word-break:break-all;user-select:all}' +
|
|
41
|
-
'.row{display:flex;gap:8px}.row .primary{background:#15803d}.row .primary:hover{background:#16a34a}' +
|
|
42
|
-
'</style>' +
|
|
43
|
-
'<div class="card" id="card"><div class="bar" id="bar" title="Drag to move"><span class="grip">' + ICON.move + '</span><b id="title">YOUR TURN</b>' +
|
|
44
|
-
'<button class="mini-done" data-a="done">Done</button><button id="toggle" title="Minimize or expand" aria-label="Minimize or expand"></button></div>' +
|
|
45
|
-
'<div class="body"><div id="msg"></div><div id="chips"></div><div class="row">' +
|
|
46
|
-
'<button class="primary" data-a="done">Done</button><button data-a="skip">Cannot do this</button></div></div></div>';
|
|
47
|
-
const card = root.getElementById('card'), bar = root.getElementById('bar'), toggle = root.getElementById('toggle');
|
|
48
|
-
root.getElementById('msg').textContent = message;
|
|
49
|
-
const chips = root.getElementById('chips');
|
|
50
|
-
chips.style.cssText = 'display:' + (copy.length ? 'flex' : 'none') + ';flex-direction:column;gap:6px';
|
|
51
|
-
for (const text of copy) {
|
|
52
|
-
const chip = document.createElement('div');
|
|
53
|
-
chip.className = 'chip';
|
|
54
|
-
chip.innerHTML = '<span></span><button aria-label="Copy to clipboard" title="Copy">' + ICON.copy + '</button>';
|
|
55
|
-
chip.firstChild.textContent = text;
|
|
56
|
-
const btn = chip.lastChild;
|
|
57
|
-
btn.addEventListener('click', async () => {
|
|
58
|
-
try { await navigator.clipboard.writeText(text); } catch (e) {
|
|
59
|
-
const area = document.createElement('textarea');
|
|
60
|
-
area.value = text; area.style.cssText = 'position:fixed;opacity:0';
|
|
61
|
-
document.body.appendChild(area); area.select(); document.execCommand('copy'); area.remove();
|
|
62
|
-
}
|
|
63
|
-
btn.innerHTML = ICON.check + 'Copied';
|
|
64
|
-
setTimeout(() => { btn.innerHTML = ICON.copy; }, 1200);
|
|
65
|
-
});
|
|
66
|
-
chips.appendChild(chip);
|
|
67
|
-
}
|
|
68
|
-
const apply = () => {
|
|
69
|
-
card.classList.toggle('min', state.minimized);
|
|
70
|
-
toggle.innerHTML = state.minimized ? ICON.up : ICON.down;
|
|
71
|
-
root.getElementById('title').textContent = state.minimized ? 'YOUR TURN' : 'YOUR TURN: control is yours';
|
|
72
|
-
};
|
|
73
|
-
const place = () => {
|
|
74
|
-
if (state.left === null) return;
|
|
75
|
-
const r = host.getBoundingClientRect();
|
|
76
|
-
const left = Math.max(0, Math.min(innerWidth - Math.min(r.width, 80), state.left));
|
|
77
|
-
const top = Math.max(0, Math.min(innerHeight - 36, state.top));
|
|
78
|
-
host.style.left = left + 'px'; host.style.top = top + 'px'; host.style.bottom = 'auto';
|
|
79
|
-
};
|
|
80
|
-
root.addEventListener('mousedown', (e) => { if (e.target.closest('button')) e.preventDefault(); });
|
|
81
|
-
root.addEventListener('click', (e) => { const a = e.target.closest('[data-a]'); if (a) window.__handoff = a.dataset.a; });
|
|
82
|
-
toggle.addEventListener('click', () => { state.minimized = !state.minimized; save(); apply(); place(); });
|
|
83
|
-
let drag = null;
|
|
84
|
-
bar.addEventListener('pointerdown', (e) => {
|
|
85
|
-
if (e.target.closest('button')) return;
|
|
86
|
-
const r = host.getBoundingClientRect();
|
|
87
|
-
drag = {dx: e.clientX - r.left, dy: e.clientY - r.top};
|
|
88
|
-
bar.setPointerCapture(e.pointerId); bar.style.cursor = 'grabbing';
|
|
89
|
-
});
|
|
90
|
-
bar.addEventListener('pointermove', (e) => {
|
|
91
|
-
if (!drag) return;
|
|
92
|
-
state.left = e.clientX - drag.dx; state.top = e.clientY - drag.dy; place();
|
|
93
|
-
});
|
|
94
|
-
bar.addEventListener('pointerup', () => { drag = null; bar.style.cursor = 'grab'; save(); });
|
|
95
|
-
host.__finish = () => {
|
|
96
|
-
card.classList.remove('min'); card.classList.add('ok');
|
|
97
|
-
toggle.style.display = 'none';
|
|
98
|
-
root.getElementById('title').textContent = 'Done: control is back with Playwright. Do not touch the page.';
|
|
99
|
-
setTimeout(() => host.remove(), 2000);
|
|
100
|
-
};
|
|
101
|
-
apply();
|
|
102
|
-
document.documentElement.appendChild(host);
|
|
103
|
-
place();
|
|
104
|
-
if (!document.title.startsWith('YOUR TURN | ')) document.title = 'YOUR TURN | ' + document.title;
|
|
105
|
-
}, {message, copy}).catch(() => {});
|
|
106
|
-
|
|
107
|
-
for (const p of ctx.pages()) await p.evaluate(() => { window.__handoff = null; }).catch(() => {});
|
|
108
|
-
const started = Date.now();
|
|
109
|
-
let outcome = null;
|
|
110
|
-
while (!outcome && Date.now() - started < maxMs) {
|
|
111
|
-
for (const p of ctx.pages()) {
|
|
112
|
-
await paint(p);
|
|
113
|
-
const flag = await p.evaluate(() => window.__handoff || null).catch(() => null);
|
|
114
|
-
if (flag) outcome = flag;
|
|
115
|
-
else if (doneText && await p.getByText(doneText).first().isVisible().catch(() => false)) outcome = 'condition';
|
|
116
|
-
}
|
|
117
|
-
if (!outcome) await page.waitForTimeout(500);
|
|
118
|
-
}
|
|
119
|
-
for (const p of ctx.pages()) {
|
|
120
|
-
await p.evaluate(() => {
|
|
121
|
-
document.getElementById('__ASRUN_panel')?.__finish();
|
|
122
|
-
document.title = document.title.replace(/^YOUR TURN \| /, '');
|
|
123
|
-
}).catch(() => {});
|
|
124
|
-
}
|
|
125
|
-
await page.waitForTimeout(2300);
|
|
126
|
-
for (const p of ctx.pages()) await p.evaluate(() => sessionStorage.removeItem('__ASRUN_panel_state')).catch(() => {});
|
|
127
|
-
return {outcome: outcome || 'timeout', waitedMs: Date.now() - started};
|
|
128
|
-
}
|
|
1
|
+
async (page) => {
|
|
2
|
+
// Floating "YOUR TURN" card for a handoff to a person. Do not copy, shorten or rewrite this file: load it with
|
|
3
|
+
// browser_run_code_unsafe({filename}) after setting the options (see user-handoff.md, "The wait loop").
|
|
4
|
+
const ctx = page.context();
|
|
5
|
+
const cfg = ctx.__handoffCfg || {};
|
|
6
|
+
const message = cfg.message || 'Do the step, then press Done';
|
|
7
|
+
const copy = Array.isArray(cfg.copy) ? cfg.copy : [];
|
|
8
|
+
const doneText = cfg.doneText || null;
|
|
9
|
+
const maxMs = cfg.maxMs || 600000;
|
|
10
|
+
await ctx.grantPermissions(['clipboard-read', 'clipboard-write']).catch(() => {});
|
|
11
|
+
|
|
12
|
+
const paint = (p) => p.evaluate(({message, copy}) => {
|
|
13
|
+
if (document.getElementById('__ASRUN_panel')) return;
|
|
14
|
+
const KEY = '__ASRUN_panel_state';
|
|
15
|
+
let state = {minimized: false, left: null, top: null};
|
|
16
|
+
try { state = {...state, ...JSON.parse(sessionStorage.getItem(KEY) || '{}')}; } catch (e) {}
|
|
17
|
+
const save = () => { try { sessionStorage.setItem(KEY, JSON.stringify(state)); } catch (e) {} };
|
|
18
|
+
const icon = (body) => '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + body + '</svg>';
|
|
19
|
+
const ICON = {
|
|
20
|
+
copy: icon('<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>'),
|
|
21
|
+
check: icon('<polyline points="20 6 9 17 4 12"/>'),
|
|
22
|
+
up: icon('<polyline points="18 15 12 9 6 15"/>'),
|
|
23
|
+
down: icon('<polyline points="6 9 12 15 18 9"/>'),
|
|
24
|
+
move: icon('<polyline points="5 9 2 12 5 15"/><polyline points="9 5 12 2 15 5"/><polyline points="15 19 12 22 9 19"/><polyline points="19 9 22 12 19 15"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="12" y1="2" x2="12" y2="22"/>'),
|
|
25
|
+
};
|
|
26
|
+
const host = document.createElement('div');
|
|
27
|
+
host.id = '__ASRUN_panel';
|
|
28
|
+
host.style.cssText = 'all:initial;position:fixed;z-index:2147483647;left:16px;bottom:16px';
|
|
29
|
+
const root = host.attachShadow({mode: 'open'});
|
|
30
|
+
root.innerHTML = '<style>' +
|
|
31
|
+
'.card{width:300px;max-width:calc(100vw - 32px);background:#1e3340;color:#fff;font:13px/1.4 system-ui,sans-serif;border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.35);overflow:hidden}' +
|
|
32
|
+
'.card.min{width:auto}.card.ok{background:#15803d}' +
|
|
33
|
+
'.bar{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 12px;background:#b45309;cursor:grab;user-select:none;touch-action:none}' +
|
|
34
|
+
'.ok .bar{background:#166534;align-items:flex-start;padding-top:9px}.bar b{flex:1;min-width:0;font-size:12px;letter-spacing:.04em;white-space:nowrap}.ok .bar b{white-space:normal;line-height:1.35}.grip{display:inline-flex;opacity:.8}' +
|
|
35
|
+
'button{all:unset;box-sizing:border-box;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:3px 8px;border-radius:4px;background:rgba(255,255,255,.18);color:#fff;font:600 12px system-ui,sans-serif}' +
|
|
36
|
+
'button:hover{background:rgba(255,255,255,.3)}' +
|
|
37
|
+
'.body{padding:10px 12px 12px;display:flex;flex-direction:column;gap:8px}.min .body,.ok .body{display:none}' +
|
|
38
|
+
'.mini-done{display:none}.min .mini-done{display:inline-flex}' +
|
|
39
|
+
'.chip{display:flex;align-items:center;gap:6px;background:rgba(0,0,0,.28);padding:4px 4px 4px 8px;border-radius:4px}' +
|
|
40
|
+
'.chip span{flex:1;font:12px ui-monospace,Menlo,monospace;word-break:break-all;user-select:all}' +
|
|
41
|
+
'.row{display:flex;gap:8px}.row .primary{background:#15803d}.row .primary:hover{background:#16a34a}' +
|
|
42
|
+
'</style>' +
|
|
43
|
+
'<div class="card" id="card"><div class="bar" id="bar" title="Drag to move"><span class="grip">' + ICON.move + '</span><b id="title">YOUR TURN</b>' +
|
|
44
|
+
'<button class="mini-done" data-a="done">Done</button><button id="toggle" title="Minimize or expand" aria-label="Minimize or expand"></button></div>' +
|
|
45
|
+
'<div class="body"><div id="msg"></div><div id="chips"></div><div class="row">' +
|
|
46
|
+
'<button class="primary" data-a="done">Done</button><button data-a="skip">Cannot do this</button></div></div></div>';
|
|
47
|
+
const card = root.getElementById('card'), bar = root.getElementById('bar'), toggle = root.getElementById('toggle');
|
|
48
|
+
root.getElementById('msg').textContent = message;
|
|
49
|
+
const chips = root.getElementById('chips');
|
|
50
|
+
chips.style.cssText = 'display:' + (copy.length ? 'flex' : 'none') + ';flex-direction:column;gap:6px';
|
|
51
|
+
for (const text of copy) {
|
|
52
|
+
const chip = document.createElement('div');
|
|
53
|
+
chip.className = 'chip';
|
|
54
|
+
chip.innerHTML = '<span></span><button aria-label="Copy to clipboard" title="Copy">' + ICON.copy + '</button>';
|
|
55
|
+
chip.firstChild.textContent = text;
|
|
56
|
+
const btn = chip.lastChild;
|
|
57
|
+
btn.addEventListener('click', async () => {
|
|
58
|
+
try { await navigator.clipboard.writeText(text); } catch (e) {
|
|
59
|
+
const area = document.createElement('textarea');
|
|
60
|
+
area.value = text; area.style.cssText = 'position:fixed;opacity:0';
|
|
61
|
+
document.body.appendChild(area); area.select(); document.execCommand('copy'); area.remove();
|
|
62
|
+
}
|
|
63
|
+
btn.innerHTML = ICON.check + 'Copied';
|
|
64
|
+
setTimeout(() => { btn.innerHTML = ICON.copy; }, 1200);
|
|
65
|
+
});
|
|
66
|
+
chips.appendChild(chip);
|
|
67
|
+
}
|
|
68
|
+
const apply = () => {
|
|
69
|
+
card.classList.toggle('min', state.minimized);
|
|
70
|
+
toggle.innerHTML = state.minimized ? ICON.up : ICON.down;
|
|
71
|
+
root.getElementById('title').textContent = state.minimized ? 'YOUR TURN' : 'YOUR TURN: control is yours';
|
|
72
|
+
};
|
|
73
|
+
const place = () => {
|
|
74
|
+
if (state.left === null) return;
|
|
75
|
+
const r = host.getBoundingClientRect();
|
|
76
|
+
const left = Math.max(0, Math.min(innerWidth - Math.min(r.width, 80), state.left));
|
|
77
|
+
const top = Math.max(0, Math.min(innerHeight - 36, state.top));
|
|
78
|
+
host.style.left = left + 'px'; host.style.top = top + 'px'; host.style.bottom = 'auto';
|
|
79
|
+
};
|
|
80
|
+
root.addEventListener('mousedown', (e) => { if (e.target.closest('button')) e.preventDefault(); });
|
|
81
|
+
root.addEventListener('click', (e) => { const a = e.target.closest('[data-a]'); if (a) window.__handoff = a.dataset.a; });
|
|
82
|
+
toggle.addEventListener('click', () => { state.minimized = !state.minimized; save(); apply(); place(); });
|
|
83
|
+
let drag = null;
|
|
84
|
+
bar.addEventListener('pointerdown', (e) => {
|
|
85
|
+
if (e.target.closest('button')) return;
|
|
86
|
+
const r = host.getBoundingClientRect();
|
|
87
|
+
drag = {dx: e.clientX - r.left, dy: e.clientY - r.top};
|
|
88
|
+
bar.setPointerCapture(e.pointerId); bar.style.cursor = 'grabbing';
|
|
89
|
+
});
|
|
90
|
+
bar.addEventListener('pointermove', (e) => {
|
|
91
|
+
if (!drag) return;
|
|
92
|
+
state.left = e.clientX - drag.dx; state.top = e.clientY - drag.dy; place();
|
|
93
|
+
});
|
|
94
|
+
bar.addEventListener('pointerup', () => { drag = null; bar.style.cursor = 'grab'; save(); });
|
|
95
|
+
host.__finish = () => {
|
|
96
|
+
card.classList.remove('min'); card.classList.add('ok');
|
|
97
|
+
toggle.style.display = 'none';
|
|
98
|
+
root.getElementById('title').textContent = 'Done: control is back with Playwright. Do not touch the page.';
|
|
99
|
+
setTimeout(() => host.remove(), 2000);
|
|
100
|
+
};
|
|
101
|
+
apply();
|
|
102
|
+
document.documentElement.appendChild(host);
|
|
103
|
+
place();
|
|
104
|
+
if (!document.title.startsWith('YOUR TURN | ')) document.title = 'YOUR TURN | ' + document.title;
|
|
105
|
+
}, {message, copy}).catch(() => {});
|
|
106
|
+
|
|
107
|
+
for (const p of ctx.pages()) await p.evaluate(() => { window.__handoff = null; }).catch(() => {});
|
|
108
|
+
const started = Date.now();
|
|
109
|
+
let outcome = null;
|
|
110
|
+
while (!outcome && Date.now() - started < maxMs) {
|
|
111
|
+
for (const p of ctx.pages()) {
|
|
112
|
+
await paint(p);
|
|
113
|
+
const flag = await p.evaluate(() => window.__handoff || null).catch(() => null);
|
|
114
|
+
if (flag) outcome = flag;
|
|
115
|
+
else if (doneText && await p.getByText(doneText).first().isVisible().catch(() => false)) outcome = 'condition';
|
|
116
|
+
}
|
|
117
|
+
if (!outcome) await page.waitForTimeout(500);
|
|
118
|
+
}
|
|
119
|
+
for (const p of ctx.pages()) {
|
|
120
|
+
await p.evaluate(() => {
|
|
121
|
+
document.getElementById('__ASRUN_panel')?.__finish();
|
|
122
|
+
document.title = document.title.replace(/^YOUR TURN \| /, '');
|
|
123
|
+
}).catch(() => {});
|
|
124
|
+
}
|
|
125
|
+
await page.waitForTimeout(2300);
|
|
126
|
+
for (const p of ctx.pages()) await p.evaluate(() => sessionStorage.removeItem('__ASRUN_panel_state')).catch(() => {});
|
|
127
|
+
return {outcome: outcome || 'timeout', waitedMs: Date.now() - started};
|
|
128
|
+
}
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
# Gap analysis
|
|
2
|
-
|
|
3
|
-
Ask every question in one message, each with a suggested default, so the user can answer "yes to all" or correct
|
|
4
|
-
single lines. Never ask a question whose answer is already in the notes, `AGENTS.md`, `profiles.yaml` or the existing cases.
|
|
5
|
-
|
|
6
|
-
## What to look for
|
|
7
|
-
| Gap | Typical question | Suggested default |
|
|
8
|
-
|---|---|---|
|
|
9
|
-
| "check X" without an expected result | What exactly must be true for X (text, logo, count, color)? | Visible and not empty; the value per profile goes in `## Values` |
|
|
10
|
-
| Logo checks | Which logo element and which value per profile? | The product logo is visible and not empty; the value per profile goes in `## Values`, and name the element as a target to settle in `/explore-case` (`[T<n>]`, see `exploring-cases/references/assertion-targets.md`). Ask the user for the exact image/text/place when the notes say only "the logo" and the page may hold several. |
|
|
11
|
-
| A check on an image, logo, file or PDF | Which exact image, text, place or UI element, and where? | Write the check with a `[T<n>]` reference and leave the card to `/explore-case`; never describe it as "the first image" |
|
|
12
|
-
| Page loads data after an action | What shows that the content is really there? | Add `Ready when: <signal>` if the notes name one; else leave it to `/explore-case` |
|
|
13
|
-
| Starting state | Does the case need existing data (a published document, a shared document)? | The case creates what it needs; never depends on pre-existing data |
|
|
14
|
-
| Delete path | How is the created data removed, and from where? | From the list page, with the item's menu; keep a `[blocking]` TODO block until confirmed |
|
|
15
|
-
| Write level | Does this step write? | Yes if it creates, edits, publishes, shares or deletes |
|
|
16
|
-
| Email | Does this step send email? | Yes for share/send/invite; skipped in production and for `email: false` profiles |
|
|
17
|
-
| Environments | Must this case run in production, or only staging? | All environments; production writes need the usual confirmation |
|
|
18
|
-
| Suites | Smoke, regression, or both? | A read-only landing check: `[smoke, regression]`; a flow that writes: `[regression]` |
|
|
19
|
-
| Profiles | Do all profiles see the same UI? | Yes; the exact literals that differ go in `## Values`, never in the steps |
|
|
20
|
-
| Unclear words | What does "collaborator button" / "..." button open? | Describe only what is observed; mark `(observed, 🔍 to be confirmed by QA)` |
|
|
21
|
-
| Unused lines | A statement that fits no case | Ask where it belongs; never drop it silently |
|
|
22
|
-
|
|
23
|
-
## Marking unknowns in a case
|
|
24
|
-
- Unknown expected value: write the suggested default as a normal sentence and hang a TODO block under it, in the
|
|
25
|
-
shape of `<framework>/.claude/skills/exploring-cases/references/open-questions.md`:
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
- The preview header shows the product logo, visible and not empty.
|
|
29
|
-
- ❓ TODO [non-blocking]: Which logo must it be? (👁️ not observed)
|
|
30
|
-
- ✍️ Answer:
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
The user accepts the default by deleting the block, or types an answer. Never write a bare `TODO: ...` line, a
|
|
34
|
-
question as the only text, or `Suggested:`.
|
|
35
|
-
- Seen on a page during exploration but not stated in the notes: `- <observation> (observed, 🔍 to be confirmed by QA)`.
|
|
36
|
-
- Never turn an observation into a hard requirement on your own.
|
|
1
|
+
# Gap analysis
|
|
2
|
+
|
|
3
|
+
Ask every question in one message, each with a suggested default, so the user can answer "yes to all" or correct
|
|
4
|
+
single lines. Never ask a question whose answer is already in the notes, `AGENTS.md`, `profiles.yaml` or the existing cases.
|
|
5
|
+
|
|
6
|
+
## What to look for
|
|
7
|
+
| Gap | Typical question | Suggested default |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| "check X" without an expected result | What exactly must be true for X (text, logo, count, color)? | Visible and not empty; the value per profile goes in `## Values` |
|
|
10
|
+
| Logo checks | Which logo element and which value per profile? | The product logo is visible and not empty; the value per profile goes in `## Values`, and name the element as a target to settle in `/explore-case` (`[T<n>]`, see `exploring-cases/references/assertion-targets.md`). Ask the user for the exact image/text/place when the notes say only "the logo" and the page may hold several. |
|
|
11
|
+
| A check on an image, logo, file or PDF | Which exact image, text, place or UI element, and where? | Write the check with a `[T<n>]` reference and leave the card to `/explore-case`; never describe it as "the first image" |
|
|
12
|
+
| Page loads data after an action | What shows that the content is really there? | Add `Ready when: <signal>` if the notes name one; else leave it to `/explore-case` |
|
|
13
|
+
| Starting state | Does the case need existing data (a published document, a shared document)? | The case creates what it needs; never depends on pre-existing data |
|
|
14
|
+
| Delete path | How is the created data removed, and from where? | From the list page, with the item's menu; keep a `[blocking]` TODO block until confirmed |
|
|
15
|
+
| Write level | Does this step write? | Yes if it creates, edits, publishes, shares or deletes |
|
|
16
|
+
| Email | Does this step send email? | Yes for share/send/invite; skipped in production and for `email: false` profiles |
|
|
17
|
+
| Environments | Must this case run in production, or only staging? | All environments; production writes need the usual confirmation |
|
|
18
|
+
| Suites | Smoke, regression, or both? | A read-only landing check: `[smoke, regression]`; a flow that writes: `[regression]` |
|
|
19
|
+
| Profiles | Do all profiles see the same UI? | Yes; the exact literals that differ go in `## Values`, never in the steps |
|
|
20
|
+
| Unclear words | What does "collaborator button" / "..." button open? | Describe only what is observed; mark `(observed, 🔍 to be confirmed by QA)` |
|
|
21
|
+
| Unused lines | A statement that fits no case | Ask where it belongs; never drop it silently |
|
|
22
|
+
|
|
23
|
+
## Marking unknowns in a case
|
|
24
|
+
- Unknown expected value: write the suggested default as a normal sentence and hang a TODO block under it, in the
|
|
25
|
+
shape of `<framework>/.claude/skills/exploring-cases/references/open-questions.md`:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
- The preview header shows the product logo, visible and not empty.
|
|
29
|
+
- ❓ TODO [non-blocking]: Which logo must it be? (👁️ not observed)
|
|
30
|
+
- ✍️ Answer:
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The user accepts the default by deleting the block, or types an answer. Never write a bare `TODO: ...` line, a
|
|
34
|
+
question as the only text, or `Suggested:`.
|
|
35
|
+
- Seen on a page during exploration but not stated in the notes: `- <observation> (observed, 🔍 to be confirmed by QA)`.
|
|
36
|
+
- Never turn an observation into a hard requirement on your own.
|