@caddis/cli 0.2.2 → 0.2.4
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/bundles/antigravity-plugin/agents/anchor.md +60 -0
- package/bundles/antigravity-plugin/agents/claude-md-curator.md +141 -0
- package/bundles/antigravity-plugin/agents/codebase-audit.md +53 -0
- package/bundles/antigravity-plugin/agents/data-engineer.md +51 -0
- package/bundles/antigravity-plugin/agents/debug.md +48 -0
- package/bundles/antigravity-plugin/agents/knowledge-transfer.md +139 -0
- package/bundles/antigravity-plugin/agents/security-analyst.md +49 -0
- package/bundles/antigravity-plugin/agents/sql-expert.md +46 -0
- package/bundles/antigravity-plugin/agents/tester.md +45 -0
- package/bundles/antigravity-plugin/agents/ui-design-reviewer.md +110 -0
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/skills/_registry.md +2 -0
- package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +7 -4
- package/bundles/antigravity-plugin/skills/dashboard-perf-sweep/SKILL.md +132 -0
- package/bundles/antigravity-plugin/skills/dashboard-perf-sweep/scripts/perf_sweep.py +249 -0
- package/bundles/antigravity-plugin/skills/explainer-doc/SKILL.md +97 -0
- package/bundles/antigravity-plugin/warm_start_agy.py +7 -4
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +6 -3
- package/bundles/manifest.json +3 -3
- package/dist/cli.js +132 -71
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ui-design-reviewer
|
|
3
|
+
description: Use this agent to critique a running UI's aesthetics, UX, and accessibility. Give it a URL (and optionally a design-doc path). It screenshots the app at desktop + mobile viewports, reads the images, and returns structured findings. Does NOT edit code. Use proactively after a frontend phase is complete, before shipping.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are a senior product designer and accessibility specialist doing a focused visual review of a
|
|
7
|
+
running UI. You **do not edit code** — you observe and report. Keep findings concrete and actionable.
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
- **URL** (required) — the running app, e.g. `http://localhost:5173`
|
|
11
|
+
- **Design doc / token file** (optional) — pass a path if the project has a design system doc;
|
|
12
|
+
you'll compare against it
|
|
13
|
+
- **Scope** (optional) — specific page or flow to focus on; default is the root page
|
|
14
|
+
|
|
15
|
+
## How to review
|
|
16
|
+
|
|
17
|
+
### Step 1 — Screenshot
|
|
18
|
+
Take screenshots at two viewports. Use `npx playwright screenshot` (preferred — no script needed):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Desktop (1280×800)
|
|
22
|
+
npx --yes playwright screenshot --browser chromium --viewport-size "1280,800" <URL> /tmp/ui-review-desktop.png
|
|
23
|
+
|
|
24
|
+
# Mobile (390×844 — iPhone 14)
|
|
25
|
+
npx --yes playwright screenshot --browser chromium --viewport-size "390,844" --device-scale-factor 2 <URL> /tmp/ui-review-mobile.png
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If `npx playwright` is unavailable, try `python -m playwright screenshot` or use Playwright's CLI
|
|
29
|
+
directly at the project path. If screenshots still fail, report the blocker and stop — do not guess.
|
|
30
|
+
|
|
31
|
+
Read both screenshots with the Read tool (Claude is multimodal — treat images as first-class input):
|
|
32
|
+
```
|
|
33
|
+
Read /tmp/ui-review-desktop.png
|
|
34
|
+
Read /tmp/ui-review-mobile.png
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Step 2 — Gather design context (if available)
|
|
38
|
+
- Glob for `**/AGENTS.md`, `**/CLAUDE.md`, `**/design-tokens*`, `**/design-system*`, `**/tailwind.config*`
|
|
39
|
+
- Read any design doc the caller supplied
|
|
40
|
+
- Note the declared color palette, spacing scale, and type scale (for token-drift findings)
|
|
41
|
+
|
|
42
|
+
### Step 3 — Judge on these axes
|
|
43
|
+
|
|
44
|
+
**Aesthetics**
|
|
45
|
+
- Visual hierarchy: does the most important content read first?
|
|
46
|
+
- Spacing rhythm: consistent scale or arbitrary gaps?
|
|
47
|
+
- Color: intentional palette? Sufficient contrast between adjacent elements?
|
|
48
|
+
- Typography: correct weight/size progression (h1 > h2 > body > caption)? Line-length ≤ 75ch?
|
|
49
|
+
- Imagery / iconography: consistent style? Decorative elements earning their space?
|
|
50
|
+
|
|
51
|
+
**UX**
|
|
52
|
+
- Primary call-to-action: visible above the fold? Clear label?
|
|
53
|
+
- Interactive affordances: are links/buttons obviously clickable?
|
|
54
|
+
- Empty states / loading: handled or blank?
|
|
55
|
+
- Responsive: no horizontal scroll on mobile? Touch targets ≥ 44×44px?
|
|
56
|
+
- Information density: not overwhelming; not sparse/wasteful?
|
|
57
|
+
- Cognitive load: consistent layout patterns page to page (if multiple visible)?
|
|
58
|
+
|
|
59
|
+
**Accessibility (a11y)**
|
|
60
|
+
- Color contrast: text on background — WCAG AA minimum 4.5:1 for body (3:1 for large text/UI).
|
|
61
|
+
Estimate from what you see; flag candidates, note that axe/Lighthouse should confirm.
|
|
62
|
+
- Focus indicators: would keyboard users see where they are?
|
|
63
|
+
- Text alternatives: images with visible icons/illustrations — is alt text likely present?
|
|
64
|
+
- Heading structure: logical nesting (h1 once per page, not skipping levels)?
|
|
65
|
+
- Form labels: inputs visually labeled (not placeholder-only)?
|
|
66
|
+
- Motion: autoplay video / animation present? (Flag — needs `prefers-reduced-motion` guard.)
|
|
67
|
+
|
|
68
|
+
## Severity
|
|
69
|
+
- **blocking** — ships broken (WCAG AA contrast fail on body text, missing primary CTA, horizontal
|
|
70
|
+
scroll on mobile, no visible focus indicator on interactive elements).
|
|
71
|
+
- **should-fix** — real UX/design problem; fix before launch.
|
|
72
|
+
- **nit** — polish / nice-to-have; flag but won't block.
|
|
73
|
+
|
|
74
|
+
## Return format (always end with this)
|
|
75
|
+
```
|
|
76
|
+
ui-review:
|
|
77
|
+
url: <what was screenshotted>
|
|
78
|
+
viewports: [desktop-1280x800, mobile-390x844]
|
|
79
|
+
verdict: approved | changes-requested
|
|
80
|
+
|
|
81
|
+
blocking:
|
|
82
|
+
- axis: <aesthetics|ux|a11y>
|
|
83
|
+
location: <element or region>
|
|
84
|
+
issue: <what's wrong>
|
|
85
|
+
fix: <concrete suggestion>
|
|
86
|
+
|
|
87
|
+
should_fix:
|
|
88
|
+
- axis: <aesthetics|ux|a11y>
|
|
89
|
+
location: <element or region>
|
|
90
|
+
issue: <what's wrong>
|
|
91
|
+
fix: <suggestion>
|
|
92
|
+
|
|
93
|
+
nits:
|
|
94
|
+
- axis: <aesthetics|ux|a11y>
|
|
95
|
+
location: <element or region>
|
|
96
|
+
note: <what>
|
|
97
|
+
|
|
98
|
+
token_drift: # only if design doc was provided
|
|
99
|
+
- token: <name> # token declared in design doc but not matching what's rendered
|
|
100
|
+
expected: <value>
|
|
101
|
+
observed: <value>
|
|
102
|
+
|
|
103
|
+
good: <one line on what's done well>
|
|
104
|
+
|
|
105
|
+
next_tools: <recommend axe DevTools / Lighthouse / pa11y if a11y blockers found>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`verdict: approved` requires an empty `blocking` list.
|
|
109
|
+
Be specific about location (e.g. "nav bar link text on dark bg", "hero CTA button", "mobile footer").
|
|
110
|
+
Do not edit files. Do not suggest a "quick fix" that requires a design decision — flag it and move on.
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
| Code Review | `code-review/` | Automated code review for pull requests using specialized review patterns. Analyzes code for quality, security, performance, and best practices. Use when reviewing code changes, PRs, or doing code audits. |
|
|
17
17
|
| Codebase Audit | `codebase-audit/` | Systematic codebase audit methodology for unfamiliar codebases before architecture or implementation work. Use for new codebase, unfamiliar codebase, pre-implementation audit, codebase review, technical due diligence, or onboarding to an existing project. Produces AUDIT-FINDINGS.md and QUESTIONS.md. Routes to Architect, Planner, or Code Reviewer agents. |
|
|
18
18
|
| Cross Review | `cross-review/` | Cross-vendor code review of the current diff — a second-vendor model (DeepSeek/GLM/any OpenAI-compatible endpoint) reviews your changes to catch bugs a same-vendor reviewer misses |
|
|
19
|
+
| Dashboard Perf Sweep | `dashboard-perf-sweep/` | Re-measure an app's dashboard/API endpoints for load-time regressions and decide, with a documented checklist (not a black-box verdict), whether a slow one is a candidate for the rs-kit summary-table refresher pattern — or needs an index, keyset paging, gzip, or nothing at all. Use when the user says "sweep dashboard performance", "check if this app needs rs-kit", "re-measure the perf tracker", "is this endpoint a refresher candidate", or when a dashboard-performance-tracker.md is stale and needs re-validating. Read-only by default (dev servers only; prod requires an explicit opt-in and human confirmation) and human-invoked, never scheduled -- it surfaces candidates, it does not enroll them. |
|
|
19
20
|
| Error Handling | `error-handling/` | Error handling patterns for Python and TypeScript applications. Use when designing error hierarchies, implementing retry logic, building error boundaries, or establishing logging strategies. Covers custom exceptions, result types, circuit breakers, and user-facing error messages. |
|
|
20
21
|
| Fastapi Dev | `fastapi-dev/` | Build FastAPI backends with standard patterns, error handling, and testing |
|
|
21
22
|
| Javascript Typescript | `javascript-typescript/` | JavaScript and TypeScript development with ES6+, Node.js, React, and modern web frameworks. Use for frontend, backend, or full-stack JavaScript/TypeScript projects. |
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
| Skill | Path | When to Use |
|
|
47
48
|
|-------|------|-------------|
|
|
48
49
|
| Code Documentation | `code-documentation/` | Writing effective code documentation - API docs, README files, inline comments, and technical guides. Use for documenting codebases, APIs, or writing developer guides. |
|
|
50
|
+
| Explainer Doc | `explainer-doc/` | Write a document that hands off work to a team who did NOT do the investigation — an external vendor, another team, a contractor — and who must be able to act on it alone. Use when the user says "write a spec for the X team", "document this for someone outside the team", "explainer doc", "implementation spec for a vendor", "hand this off to Y", or is about to write a design doc whose primary reader did not live through the debugging/design process. NOT for internal specs where the reader already has full context (use technical-writing or writing-plans instead) — this is specifically for the "stranger must follow it alone" case. |
|
|
49
51
|
| Technical Writing | `technical-writing/` | Technical documentation best practices for READMEs, API docs, architecture docs, runbooks, and developer guides. Use when writing or reviewing documentation, creating onboarding guides, or establishing documentation standards. |
|
|
50
52
|
| Writing Plans | `writing-plans/` | Use when you have a spec or requirements for a multi-step task, before touching code |
|
|
51
53
|
|
|
@@ -17,11 +17,14 @@ overridable via `CADDIS_KEYS_FILE`). If your keys file is populated, no env setu
|
|
|
17
17
|
Details: the `coding/cross-review` skill and the **Providers & keys** guide.
|
|
18
18
|
|
|
19
19
|
## Run it
|
|
20
|
-
|
|
20
|
+
Locate the tool (stdlib-only, no install), in order:
|
|
21
|
+
- `.github/tools/oss_review.py` — this project vendors its own copy.
|
|
22
|
+
- `${CLAUDE_PLUGIN_ROOT}/scripts/oss_review.py` — the plugin-bundled copy (every install has this;
|
|
23
|
+
use it whenever the project has no local copy).
|
|
21
24
|
```
|
|
22
|
-
python
|
|
23
|
-
python
|
|
24
|
-
python
|
|
25
|
+
python <resolved-path>/oss_review.py # DeepSeek eyes (default)
|
|
26
|
+
python <resolved-path>/oss_review.py --provider glm # GLM eyes
|
|
27
|
+
python <resolved-path>/oss_review.py --range $ARGUMENTS # review a git range, e.g. origin/main..HEAD
|
|
25
28
|
```
|
|
26
29
|
Optional: `--base-url <url>`, `--model <id>` (env always overrides the preset).
|
|
27
30
|
**Alternate the provider between reviews** (DeepSeek one diff, GLM the next) — different model
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dashboard-perf-sweep
|
|
3
|
+
context: fork
|
|
4
|
+
description: Re-measure an app's dashboard/API endpoints for load-time regressions and decide, with a documented checklist (not a black-box verdict), whether a slow one is a candidate for the rs-kit summary-table refresher pattern — or needs an index, keyset paging, gzip, or nothing at all. Use when the user says "sweep dashboard performance", "check if this app needs rs-kit", "re-measure the perf tracker", "is this endpoint a refresher candidate", or when a dashboard-performance-tracker.md is stale and needs re-validating. Read-only by default (dev servers only; prod requires an explicit opt-in and human confirmation) and human-invoked, never scheduled -- it surfaces candidates, it does not enroll them.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# dashboard-perf-sweep — re-measure, then judge the shape, not just the time
|
|
8
|
+
|
|
9
|
+
Today an app "earns" the rs-kit refresher pattern only when someone happens to be doing unrelated
|
|
10
|
+
work on it, hits a slow page, and measures it properly. That worked for rev-sight and serve-sight,
|
|
11
|
+
but a snapshot measurement goes stale — a page added last month, or a table that crossed a growth
|
|
12
|
+
threshold since, has no mechanism to surface itself. This skill re-measures on demand.
|
|
13
|
+
|
|
14
|
+
**What already exists and must NOT be rebuilt:** every app in the fleet has its own
|
|
15
|
+
`.caddis/kb/dashboard-performance-tracker.md` with real baseline measurements. The gap this closes
|
|
16
|
+
is **re-measurement and re-judgment**, not first-time discovery.
|
|
17
|
+
|
|
18
|
+
## Explicit scope guardrails — do not relax without a new conversation
|
|
19
|
+
|
|
20
|
+
- **Read-only except the final tracker update.** No code changes, no schema changes, ever. The only
|
|
21
|
+
write this skill makes is a diffable update to the target app's own `dashboard-performance-tracker.md`.
|
|
22
|
+
- **Dev by default.** Measuring against prod requires an explicit opt-in flag from the user AND, per
|
|
23
|
+
this project's convention for risky/hard-to-reverse actions, confirmation before hitting a live
|
|
24
|
+
service.
|
|
25
|
+
- **Human-invoked, not scheduled.** Run this when a person chooses to — never wire it into a cron job
|
|
26
|
+
or background automation. It surfaces candidates; it does not enroll them.
|
|
27
|
+
- **rs-kit is one of several possible recommendations, never the only output.** The checklist below
|
|
28
|
+
must be able to conclude "needs an index," "needs keyset paging," "needs gzip/trimmed fields,"
|
|
29
|
+
"not currently a candidate" — recommending rs-kit only for the specific aggregation shape.
|
|
30
|
+
|
|
31
|
+
## The decision model — two gates, not one number
|
|
32
|
+
|
|
33
|
+
**Gate 1 — time (mechanical).** `DASHBOARD-PERFORMANCE-STANDARD.md` Rule 6: the first, uncached load
|
|
34
|
+
of any dashboard endpoint must finish in **under ~1 second**. The harness script below applies this
|
|
35
|
+
gate automatically per endpoint. Passing gate 1 does **not** mean rs-kit — it just means "look closer."
|
|
36
|
+
|
|
37
|
+
**Gate 2 — shape (judgment, applied by you the agent, never automated).** rs-kit fixes exactly one
|
|
38
|
+
shape: a query that recomputes `O(all rows ever stored)` per request, against data that keeps
|
|
39
|
+
growing. Time alone is not sufficient evidence — the fleet's own history proves it:
|
|
40
|
+
|
|
41
|
+
| App | Cold time | Root cause | rs-kit? |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| serve-sight | worst >60s, 5 real breaches | recurring full-table aggregation | **YES** |
|
|
44
|
+
| rev-sight (pre-fix) | 14s–60s+ across several endpoints | recurring full-table aggregation | **YES** |
|
|
45
|
+
| appointment-assist | 10.3s | a single ~158MB LOB payload on 9.1k rows, not aggregation | **NO** — SQL aggregate + gzip |
|
|
46
|
+
| nps-lens | 1.9s | cold JSON parse, no DB at all in prod | **NO** — warm the provider |
|
|
47
|
+
| app-sight | 202ms | n/a — never crossed gate 1 | **NO** |
|
|
48
|
+
|
|
49
|
+
A naive "slow ⇒ recommend rs-kit" tool would have wrongly flagged appointment-assist and nps-lens.
|
|
50
|
+
That already happened once before the shape distinction was made explicit — don't repeat it.
|
|
51
|
+
|
|
52
|
+
## Step 1 — build the endpoint list for this app
|
|
53
|
+
|
|
54
|
+
The tracker files are semi-structured (page names + backtick route fragments in a markdown table),
|
|
55
|
+
not a clean machine-parseable list — extract the list yourself, per app, each run:
|
|
56
|
+
|
|
57
|
+
1. Read the target app's `.caddis/kb/dashboard-performance-tracker.md` — pull every "Page / endpoint"
|
|
58
|
+
entry, in particular anything marked ⏳ pending or not re-measured recently.
|
|
59
|
+
2. If the app has a `UI_PAGE_GUIDE.md` (check root and `docs/reference/`), cross-check it for pages
|
|
60
|
+
**not yet in the tracker** — this is what catches drift, not just re-running the same old list. Not
|
|
61
|
+
every app has one; if it's missing, say so explicitly rather than silently only using the tracker.
|
|
62
|
+
3. Write the result as a JSON file matching the harness's input shape:
|
|
63
|
+
```json
|
|
64
|
+
[
|
|
65
|
+
{"name": "Historical Trends chart", "method": "GET", "path": "/api/trends/daily"},
|
|
66
|
+
{"name": "IW grid", "method": "POST", "path": "/raworkbench/investigation/query", "json": {}}
|
|
67
|
+
]
|
|
68
|
+
```
|
|
69
|
+
4. Confirm with the user (or the app's own `run`/`webapp-testing` conventions) how to reach the dev
|
|
70
|
+
instance safely, and what auth the endpoints need (open / token / session cookie) — pass it via
|
|
71
|
+
`--header`.
|
|
72
|
+
|
|
73
|
+
## Step 2 — start the dev server, then run the harness
|
|
74
|
+
|
|
75
|
+
Use the app's own `run` skill/convention to start it — this skill does not start dev servers itself,
|
|
76
|
+
only measures an already-reachable URL.
|
|
77
|
+
|
|
78
|
+
**Use `127.0.0.1`, never the hostname `localhost`, in `--base-url`.** On Windows, Python's `urllib`
|
|
79
|
+
resolves `localhost` dual-stack (tries IPv6 `::1` first) and can add a **spurious, rock-steady
|
|
80
|
+
~1000ms** to every single request — cold *and* warm, on every endpoint including the framework's own
|
|
81
|
+
static docs page. It looks exactly like a real, uniform perf problem (equal cold/warm, present on
|
|
82
|
+
trivial routes too) and is not one. This cost real time during this skill's own validation (§
|
|
83
|
+
Validating below) before `127.0.0.1` was substituted and the true numbers — sub-15ms — appeared. If a
|
|
84
|
+
sweep ever comes back with a suspiciously uniform floor across unrelated endpoints, first time a
|
|
85
|
+
`/docs` or `/openapi.json` route on the same base URL before trusting the data.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python <skill-dir>/scripts/perf_sweep.py --base-url http://127.0.0.1:<port> --endpoints endpoints.json
|
|
89
|
+
python <skill-dir>/scripts/perf_sweep.py --base-url http://127.0.0.1:<port> --endpoints endpoints.json \
|
|
90
|
+
--header "Cookie: session=..." --output report.json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Cold = first request after the dev server's fresh start (or right after a cache-bust if it can't be
|
|
94
|
+
restarted cheaply); warm = the immediate second request. The report flags gate-1 breaches and prints
|
|
95
|
+
the cold/warm ratio — a big ratio with no cache in between already points at a query-shape problem,
|
|
96
|
+
not a cache-TTL one. **A page that errors or times out is a finding, not a script failure** — it's
|
|
97
|
+
printed as `[ERROR]`, never silently dropped.
|
|
98
|
+
|
|
99
|
+
## Step 3 — apply the gate-2 checklist to every gate-1 breach
|
|
100
|
+
|
|
101
|
+
Force these questions, in order, for each slow endpoint — don't skip to a verdict:
|
|
102
|
+
|
|
103
|
+
1. **Does response size explain most of the time** (large payload, not a slow query)? → not rs-kit;
|
|
104
|
+
recommend trimming fields / gzip / virtualization.
|
|
105
|
+
2. **Is the slow part parsing/serialization, not the DB round-trip?** → not rs-kit; recommend
|
|
106
|
+
caching/warming the parsed form (nps-lens precedent).
|
|
107
|
+
3. **Does the query scale with total stored rows rather than the response size** — read the actual
|
|
108
|
+
SQL/query plan, not just the timing — **and is the same aggregation recomputed per request on data
|
|
109
|
+
that keeps growing?** → candidate for rs-kit.
|
|
110
|
+
4. **Is it slow only at deep pagination (OFFSET)?** → not rs-kit; recommend keyset paging
|
|
111
|
+
(`exception_id`-style cursor, rev-sight R6/P3 precedent).
|
|
112
|
+
5. **Is a specific WHERE clause unseekable** (function-wrapped column, NVARCHAR/VARCHAR param
|
|
113
|
+
mismatch)? → not rs-kit; recommend an index or a param-type fix.
|
|
114
|
+
|
|
115
|
+
Link findings to real fleet precedent (the table above, the `nvarchar-param-varchar-column-scan`
|
|
116
|
+
pattern, the keyset-paging work) rather than describing fixes in the abstract — every category here
|
|
117
|
+
has already been solved once in this fleet.
|
|
118
|
+
|
|
119
|
+
## Step 4 — update the tracker, diffably
|
|
120
|
+
|
|
121
|
+
Update the target app's own `dashboard-performance-tracker.md` in place with the new measurements and
|
|
122
|
+
verdicts, so drift becomes visible over time in the diff. Never silently overwrite it without the user
|
|
123
|
+
being able to see a before/after. Print a summary alongside the file edit.
|
|
124
|
+
|
|
125
|
+
## Validating this skill against known ground truth
|
|
126
|
+
|
|
127
|
+
Before trusting a new app's output, or after changing the checklist, run the sweep against an
|
|
128
|
+
already-known app and confirm it reproduces the **already-established, human-verified** conclusion:
|
|
129
|
+
**app-sight** (202ms, should conclude "not a candidate") and **serve-sight** (known breach, should
|
|
130
|
+
conclude "aggregation-shaped, rs-kit candidate" for the pages already identified). If the output
|
|
131
|
+
disagrees with the known-good conclusion for either, that's a checklist bug — fix the checklist, don't
|
|
132
|
+
rationalize the disagreement away.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Cold/warm HTTP timing harness for the dashboard-perf-sweep skill.
|
|
3
|
+
|
|
4
|
+
Given a base URL and a list of endpoints, measures a COLD request (first hit) and a WARM
|
|
5
|
+
request (immediately after) per endpoint, safely: a timeout or connection error on one endpoint
|
|
6
|
+
is recorded as a finding, never a crash of the whole sweep (DASHBOARD-PERFORMANCE-STANDARD.md
|
|
7
|
+
Rule 6 gate-1 mechanics; the huge cold/warm gap without a cache in between is itself diagnostic
|
|
8
|
+
of a query-shape problem, not a cache-TTL one).
|
|
9
|
+
|
|
10
|
+
This harness does NOT start dev servers or know anything about a specific app -- that's the
|
|
11
|
+
`run` skill's job. It only measures a URL that is already reachable. stdlib-only (urllib), no
|
|
12
|
+
install, matching the other caddis tool scripts (oss_review.py etc.).
|
|
13
|
+
|
|
14
|
+
CLI:
|
|
15
|
+
python perf_sweep.py --base-url http://localhost:8000 --endpoints endpoints.json
|
|
16
|
+
python perf_sweep.py --base-url http://localhost:8000 --endpoints endpoints.json --output report.json
|
|
17
|
+
python perf_sweep.py --base-url http://localhost:8000 --endpoints endpoints.json --gate1-seconds 1.0
|
|
18
|
+
|
|
19
|
+
endpoints.json shape (a JSON list):
|
|
20
|
+
[
|
|
21
|
+
{"name": "Historical Trends chart", "method": "GET", "path": "/api/trends/daily"},
|
|
22
|
+
{"name": "IW grid", "method": "POST", "path": "/raworkbench/investigation/query",
|
|
23
|
+
"json": {"filters": {}}, "headers": {"Authorization": "Bearer ..."}}
|
|
24
|
+
]
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import json
|
|
30
|
+
import sys
|
|
31
|
+
import time
|
|
32
|
+
import urllib.error
|
|
33
|
+
import urllib.request
|
|
34
|
+
from dataclasses import asdict, dataclass, field
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
DEFAULT_TIMEOUT_SECONDS = 30.0
|
|
38
|
+
DEFAULT_GATE1_SECONDS = 1.0 # DASHBOARD-PERFORMANCE-STANDARD.md Rule 6
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class RequestResult:
|
|
43
|
+
ok: bool
|
|
44
|
+
status: int | None = None
|
|
45
|
+
elapsed_ms: float | None = None
|
|
46
|
+
error: str | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class EndpointResult:
|
|
51
|
+
name: str
|
|
52
|
+
method: str
|
|
53
|
+
path: str
|
|
54
|
+
cold: RequestResult
|
|
55
|
+
warm: RequestResult | None = None
|
|
56
|
+
gate1_breach: bool = False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class SweepReport:
|
|
61
|
+
base_url: str
|
|
62
|
+
gate1_seconds: float
|
|
63
|
+
endpoints: list[EndpointResult] = field(default_factory=list)
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict[str, Any]:
|
|
66
|
+
return {
|
|
67
|
+
"base_url": self.base_url,
|
|
68
|
+
"gate1_seconds": self.gate1_seconds,
|
|
69
|
+
"endpoints": [
|
|
70
|
+
{
|
|
71
|
+
"name": e.name,
|
|
72
|
+
"method": e.method,
|
|
73
|
+
"path": e.path,
|
|
74
|
+
"cold": asdict(e.cold),
|
|
75
|
+
"warm": asdict(e.warm) if e.warm else None,
|
|
76
|
+
"gate1_breach": e.gate1_breach,
|
|
77
|
+
}
|
|
78
|
+
for e in self.endpoints
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _do_request(
|
|
84
|
+
url: str,
|
|
85
|
+
method: str,
|
|
86
|
+
*,
|
|
87
|
+
json_body: dict[str, Any] | None = None,
|
|
88
|
+
headers: dict[str, str] | None = None,
|
|
89
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
90
|
+
) -> RequestResult:
|
|
91
|
+
"""One timed HTTP request. Never raises -- a timeout/connection/HTTP error becomes a
|
|
92
|
+
RequestResult with ok=False and a human-readable error, which is itself a sweep finding."""
|
|
93
|
+
data = None
|
|
94
|
+
req_headers = dict(headers or {})
|
|
95
|
+
if json_body is not None:
|
|
96
|
+
data = json.dumps(json_body).encode("utf-8")
|
|
97
|
+
req_headers.setdefault("Content-Type", "application/json")
|
|
98
|
+
|
|
99
|
+
req = urllib.request.Request(url, data=data, headers=req_headers, method=method)
|
|
100
|
+
start = time.perf_counter()
|
|
101
|
+
try:
|
|
102
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
103
|
+
resp.read()
|
|
104
|
+
elapsed_ms = (time.perf_counter() - start) * 1000.0
|
|
105
|
+
return RequestResult(ok=True, status=resp.status, elapsed_ms=elapsed_ms)
|
|
106
|
+
except urllib.error.HTTPError as exc:
|
|
107
|
+
# An HTTP error status still completed a round trip -- timing is a real finding.
|
|
108
|
+
elapsed_ms = (time.perf_counter() - start) * 1000.0
|
|
109
|
+
return RequestResult(ok=False, status=exc.code, elapsed_ms=elapsed_ms, error=f"HTTP {exc.code}")
|
|
110
|
+
except urllib.error.URLError as exc:
|
|
111
|
+
elapsed_ms = (time.perf_counter() - start) * 1000.0
|
|
112
|
+
return RequestResult(ok=False, elapsed_ms=elapsed_ms, error=f"connection error: {exc.reason}")
|
|
113
|
+
except TimeoutError:
|
|
114
|
+
elapsed_ms = (time.perf_counter() - start) * 1000.0
|
|
115
|
+
return RequestResult(ok=False, elapsed_ms=elapsed_ms, error=f"timeout after {timeout}s")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def sweep_endpoint(
|
|
119
|
+
base_url: str,
|
|
120
|
+
endpoint: dict[str, Any],
|
|
121
|
+
*,
|
|
122
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
123
|
+
gate1_seconds: float = DEFAULT_GATE1_SECONDS,
|
|
124
|
+
warm: bool = True,
|
|
125
|
+
extra_headers: dict[str, str] | None = None,
|
|
126
|
+
) -> EndpointResult:
|
|
127
|
+
method = endpoint.get("method", "GET").upper()
|
|
128
|
+
path = endpoint["path"]
|
|
129
|
+
url = base_url.rstrip("/") + path
|
|
130
|
+
headers = dict(extra_headers or {})
|
|
131
|
+
headers.update(endpoint.get("headers", {}))
|
|
132
|
+
json_body = endpoint.get("json")
|
|
133
|
+
|
|
134
|
+
cold = _do_request(url, method, json_body=json_body, headers=headers, timeout=timeout)
|
|
135
|
+
warm_result = None
|
|
136
|
+
if warm:
|
|
137
|
+
warm_result = _do_request(url, method, json_body=json_body, headers=headers, timeout=timeout)
|
|
138
|
+
|
|
139
|
+
gate1_breach = bool(cold.ok and cold.elapsed_ms is not None and cold.elapsed_ms / 1000.0 > gate1_seconds)
|
|
140
|
+
return EndpointResult(
|
|
141
|
+
name=endpoint.get("name", path),
|
|
142
|
+
method=method,
|
|
143
|
+
path=path,
|
|
144
|
+
cold=cold,
|
|
145
|
+
warm=warm_result,
|
|
146
|
+
gate1_breach=gate1_breach,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def sweep(
|
|
151
|
+
base_url: str,
|
|
152
|
+
endpoints: list[dict[str, Any]],
|
|
153
|
+
*,
|
|
154
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
155
|
+
gate1_seconds: float = DEFAULT_GATE1_SECONDS,
|
|
156
|
+
warm: bool = True,
|
|
157
|
+
extra_headers: dict[str, str] | None = None,
|
|
158
|
+
) -> SweepReport:
|
|
159
|
+
report = SweepReport(base_url=base_url, gate1_seconds=gate1_seconds)
|
|
160
|
+
for endpoint in endpoints:
|
|
161
|
+
report.endpoints.append(
|
|
162
|
+
sweep_endpoint(
|
|
163
|
+
base_url,
|
|
164
|
+
endpoint,
|
|
165
|
+
timeout=timeout,
|
|
166
|
+
gate1_seconds=gate1_seconds,
|
|
167
|
+
warm=warm,
|
|
168
|
+
extra_headers=extra_headers,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
return report
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def format_report(report: SweepReport) -> str:
|
|
175
|
+
lines = [f"Sweep: {report.base_url} (gate 1 = {report.gate1_seconds}s cold, uncached)", ""]
|
|
176
|
+
for e in report.endpoints:
|
|
177
|
+
if not e.cold.ok:
|
|
178
|
+
lines.append(f" [ERROR] {e.method:5s} {e.path:45s} {e.cold.error}")
|
|
179
|
+
continue
|
|
180
|
+
flag = "SLOW " if e.gate1_breach else " "
|
|
181
|
+
cold_s = f"{e.cold.elapsed_ms:8.1f}ms"
|
|
182
|
+
warm_s = f"{e.warm.elapsed_ms:8.1f}ms" if e.warm and e.warm.ok else "n/a"
|
|
183
|
+
gap = ""
|
|
184
|
+
if e.warm and e.warm.ok and e.cold.elapsed_ms and e.warm.elapsed_ms:
|
|
185
|
+
ratio = e.cold.elapsed_ms / max(e.warm.elapsed_ms, 0.01)
|
|
186
|
+
if ratio > 3:
|
|
187
|
+
gap = f" (cold/warm {ratio:.1f}x -- likely query-shape, not cache-TTL)"
|
|
188
|
+
lines.append(f" {flag}{e.method:5s} {e.path:45s} cold={cold_s} warm={warm_s}{gap}")
|
|
189
|
+
breaches = [e for e in report.endpoints if e.gate1_breach]
|
|
190
|
+
errors = [e for e in report.endpoints if not e.cold.ok]
|
|
191
|
+
problems = breaches + errors
|
|
192
|
+
lines.append("")
|
|
193
|
+
lines.append(
|
|
194
|
+
f"{len(problems)}/{len(report.endpoints)} endpoint(s) have a problem "
|
|
195
|
+
f"({len(breaches)} slow, {len(errors)} errored/timed out -- a timeout is a WORSE "
|
|
196
|
+
f"signal than a gate-1 breach, not a lesser one)."
|
|
197
|
+
)
|
|
198
|
+
if problems:
|
|
199
|
+
lines.append("Not itself a rs-kit verdict -- apply the shape checklist (SKILL.md) next.")
|
|
200
|
+
return "\n".join(lines)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _parse_header(value: str) -> tuple[str, str]:
|
|
204
|
+
if ":" not in value:
|
|
205
|
+
raise argparse.ArgumentTypeError(f"--header must be 'Key: Value', got {value!r}")
|
|
206
|
+
key, _, val = value.partition(":")
|
|
207
|
+
return key.strip(), val.strip()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def main(argv: list[str] | None = None) -> int:
|
|
211
|
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
212
|
+
parser.add_argument("--base-url", required=True, help="e.g. http://localhost:8703")
|
|
213
|
+
parser.add_argument("--endpoints", required=True, help="path to a JSON file (see module docstring)")
|
|
214
|
+
parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT_SECONDS)
|
|
215
|
+
parser.add_argument("--gate1-seconds", type=float, default=DEFAULT_GATE1_SECONDS)
|
|
216
|
+
parser.add_argument("--no-warm", action="store_true", help="skip the warm (second) request")
|
|
217
|
+
parser.add_argument("--header", action="append", default=[], type=_parse_header,
|
|
218
|
+
help="extra header applied to every request, repeatable: --header 'Cookie: session=...'")
|
|
219
|
+
parser.add_argument("--output", help="also write the JSON report to this path")
|
|
220
|
+
args = parser.parse_args(argv)
|
|
221
|
+
|
|
222
|
+
with open(args.endpoints, encoding="utf-8") as f:
|
|
223
|
+
endpoints = json.load(f)
|
|
224
|
+
if not isinstance(endpoints, list):
|
|
225
|
+
print(f"[FAIL] {args.endpoints} must contain a JSON list of endpoint objects", file=sys.stderr)
|
|
226
|
+
return 2
|
|
227
|
+
|
|
228
|
+
report = sweep(
|
|
229
|
+
args.base_url,
|
|
230
|
+
endpoints,
|
|
231
|
+
timeout=args.timeout,
|
|
232
|
+
gate1_seconds=args.gate1_seconds,
|
|
233
|
+
warm=not args.no_warm,
|
|
234
|
+
extra_headers=dict(args.header),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
print(format_report(report))
|
|
238
|
+
if args.output:
|
|
239
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
240
|
+
json.dump(report.to_dict(), f, indent=2)
|
|
241
|
+
print(f"\nWrote {args.output}")
|
|
242
|
+
|
|
243
|
+
breaches = [e for e in report.endpoints if e.gate1_breach]
|
|
244
|
+
errors = [e for e in report.endpoints if not e.cold.ok]
|
|
245
|
+
return 1 if (breaches or errors) else 0
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explainer-doc
|
|
3
|
+
context: fork
|
|
4
|
+
description: Write a document that hands off work to a team who did NOT do the investigation — an external vendor, another team, a contractor — and who must be able to act on it alone. Use when the user says "write a spec for the X team", "document this for someone outside the team", "explainer doc", "implementation spec for a vendor", "hand this off to Y", or is about to write a design doc whose primary reader did not live through the debugging/design process. NOT for internal specs where the reader already has full context (use technical-writing or writing-plans instead) — this is specifically for the "stranger must follow it alone" case.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# explainer-doc — write for the reader who has no other context
|
|
8
|
+
|
|
9
|
+
A spec written by the people who did the investigation is unreadable by the team who must implement
|
|
10
|
+
it. The authors compress away the reasoning, so every design choice looks arbitrary, and the reader
|
|
11
|
+
either follows it blindly or argues with it. **The fix is not "add more detail" — it is to lead with
|
|
12
|
+
*why*, and to make the document survive being read alone.**
|
|
13
|
+
|
|
14
|
+
Proven in anger on a Genesys→ETL cutover spec handed to an external vendor team. The user's verdict:
|
|
15
|
+
*"nailed it — the document was perfect, simple, this should be the standard for explainer documents."*
|
|
16
|
+
Reference implementation to study: `serve-sight/docs/genesys-cutover/etl-implementation-spec.md` (full
|
|
17
|
+
spec) and `pega-cticallid-change-request.md` (the same pattern compressed to one page for a single ask).
|
|
18
|
+
|
|
19
|
+
## When this is NOT the right skill
|
|
20
|
+
|
|
21
|
+
If the reader already lived through the investigation (a teammate, your future self, an internal
|
|
22
|
+
handoff with shared context), use `technical-writing` or `writing-plans` instead — those are lighter
|
|
23
|
+
and don't need the "assume zero shared context" discipline below. Reach for `explainer-doc` specifically
|
|
24
|
+
when the reader is a **stranger to the problem**: an external vendor, a different team, a contractor,
|
|
25
|
+
anyone who will act on the document with no one in the room to ask.
|
|
26
|
+
|
|
27
|
+
## The shape — in order. The order IS the pattern
|
|
28
|
+
|
|
29
|
+
Do not reorder these. Each section's position is load-bearing: later sections lean on earlier ones for
|
|
30
|
+
their justification, and a reader working top-to-bottom needs the "why" before the "what."
|
|
31
|
+
|
|
32
|
+
| § | Section | Why it must come here |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| 1 | **Why this exists** — what broke, in a diagram | Every later decision is justified by this. Lead with the failure, not the solution. |
|
|
35
|
+
| 2 | **What we're asking for, in one picture** + a size table | The reader decides here whether the ask is reasonable. Include what you are explicitly **not** asking for. |
|
|
36
|
+
| 3 | **Understanding the source data** — the 3-5 things that catch people out | Each one should have already caused a real bug. Not a data dictionary. |
|
|
37
|
+
| 4 | **The one thing that matters most** | Usually not the new build. Say so plainly, with the number that proves it. |
|
|
38
|
+
| 5 | **Where it runs** + access needed | The blocker nobody writes down until the first deploy fails. |
|
|
39
|
+
| 6 | **What to build**, with the reasoning *beside* each choice | Not in an appendix. Next to the DDL / the query / the config. |
|
|
40
|
+
| 6b | **Changes to what already exists** | Always under-specified. Ask "what did I only half-mention?" — this is where the gaps hide. |
|
|
41
|
+
| 7-8 | The actual implementation (extract/build queries, load procedure, etc.) | |
|
|
42
|
+
| 9 | Schedule / retention / first load | The one-off backfill is always forgotten. |
|
|
43
|
+
| 10 | **Validation, with EXPECTED VALUES** | "Run this, expect ~64%" beats "verify the data." |
|
|
44
|
+
| 11 | Priority · acceptance (who signs off) · **what we excluded** | The exclusion list stops speculative building. |
|
|
45
|
+
| 12 | **Glossary** | Every acronym, inline. Not a link. |
|
|
46
|
+
|
|
47
|
+
Adapt section names to the domain (this table is ETL-flavored from its origin; a UI handoff, an API
|
|
48
|
+
contract, or an infra runbook will rename §6-9) — but keep the **order and the intent of each slot**.
|
|
49
|
+
|
|
50
|
+
## The ten rules
|
|
51
|
+
|
|
52
|
+
1. **Self-contained.** If the reader needs another document to follow this one, the document has
|
|
53
|
+
failed. Companions are optional background, stated as such — never load-bearing.
|
|
54
|
+
2. **Lead with the failure mode, not the solution.** State what actually went wrong, concretely, before
|
|
55
|
+
proposing anything. Everything cautious later inherits its justification from that paragraph.
|
|
56
|
+
3. **Reasoning beside the artefact, not in an appendix.** "Cluster on the date not the GUID" with the
|
|
57
|
+
3-row comparison table right there. An appendix is never read.
|
|
58
|
+
4. **Every number carries its measurement date and a way to reproduce it.** "1.4% of reality (measured
|
|
59
|
+
2026-07-14, `SELECT ...`)" — not "a small fraction."
|
|
60
|
+
5. **Say what you are NOT asking for.** Prevents speculative work and pre-answers "why not X?" before
|
|
61
|
+
it's asked.
|
|
62
|
+
6. **Name what is not the reader's fault.** If a gap traces to a third system or a decision made
|
|
63
|
+
elsewhere, say so explicitly. Saves the reader a week of misdirected chasing and buys credibility for
|
|
64
|
+
the rest of the document.
|
|
65
|
+
7. **Diagrams for the shape, tables for the detail, prose only for judgement.** No diagram should repeat
|
|
66
|
+
what a table already says better.
|
|
67
|
+
8. **Validation states expected values.** "It ran" and "it is right" must be two different checks — give
|
|
68
|
+
the reader the number to compare against.
|
|
69
|
+
9. **Route by owner.** If an item belongs to a third team, mark it as theirs, explicitly, in the document
|
|
70
|
+
— don't let it read as part of the reader's scope.
|
|
71
|
+
10. **Glossary inline.** The reader will not follow a link mid-implementation; every acronym gets defined
|
|
72
|
+
where it's used, plus a glossary section as the fallback lookup.
|
|
73
|
+
|
|
74
|
+
## Process
|
|
75
|
+
|
|
76
|
+
1. **Confirm the reader.** Ask (or infer from context) who this is actually for and how much they
|
|
77
|
+
already know. If they have real shared context, stop and suggest `technical-writing`/`writing-plans`
|
|
78
|
+
instead — this skill's discipline is overhead they don't need.
|
|
79
|
+
2. **Draft §1 first, alone, and get it right before writing anything else.** Every other section's
|
|
80
|
+
justification traces back to it. If you can't state the failure mode plainly and concretely, the
|
|
81
|
+
rest of the document will inherit that vagueness.
|
|
82
|
+
3. **Write the shape table above as your outline**, adapting section names to the domain, then fill it
|
|
83
|
+
in top to bottom. Keep reasoning next to the artefact it justifies (rule 3), not in a separate
|
|
84
|
+
"design decisions" section.
|
|
85
|
+
4. **Before sending, ask the two questions below** — they are an exit gate, not a suggestion.
|
|
86
|
+
|
|
87
|
+
## The two-question exit gate
|
|
88
|
+
|
|
89
|
+
Ask both, honestly, before the document ships:
|
|
90
|
+
|
|
91
|
+
- **"Could someone who has read nothing else follow this end to end?"** If the answer requires "well,
|
|
92
|
+
they'd also need to know X" — X is either missing or should be a stated, explicit companion doc.
|
|
93
|
+
- **"What did I mention once and never specify?"** This is the question that surfaces the gaps — in the
|
|
94
|
+
origin case it found an entire missing section (§6b, changes to existing infrastructure) that had been
|
|
95
|
+
gestured at once and never actually written.
|
|
96
|
+
|
|
97
|
+
If either question fails, fix the document — don't ship it and hope the reader asks.
|