novahiz 0.5.2 → 0.6.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/CHANGELOG.md +29 -0
- package/README.md +4 -4
- package/catalog/categories.json +14 -0
- package/catalog/providers.json +25 -0
- package/catalog/rules.json +19 -0
- package/dist/gate.js +85 -1
- package/package.json +1 -1
- package/skills/novahiz-stitch-fidelity/SKILL.md +113 -0
- package/skills/novahiz-stitch-fidelity/defaults/verify.config.defaults.json +29 -0
- package/skills/novahiz-stitch-fidelity/references/capture-recipes.md +74 -0
- package/skills/novahiz-stitch-fidelity/references/judgment-grid.md +67 -0
- package/skills/novahiz-stitch-fidelity/scripts/stitch_capture.mjs +598 -0
- package/skills/novahiz-stitch-fidelity/scripts/stitch_composite.mjs +271 -0
- package/src/gate.ts +77 -1
- package/src/spec.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2026-10-06
|
|
9
|
+
|
|
10
|
+
Feature release: the gate learns project scope, and Stitch design projects
|
|
11
|
+
get a capture → composite → fidelity-verdict loop.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`R16-stitch-fidelity` gate rule**: editing a UI file (`*.tsx`, `*.jsx`,
|
|
16
|
+
`*.vue`, `*.svelte`, `*.dart`, `*.css`, `*.scss`, `*.html`) now requires
|
|
17
|
+
the `novahiz-stitch-fidelity` skill — but only inside a project that
|
|
18
|
+
carries `stitch/**` references. This is the new `projectGlobs` selector
|
|
19
|
+
(`src/spec.ts`, `src/gate.ts`): nearest ancestor of the edited file
|
|
20
|
+
holding `.git`, `package.json` or `pubspec.yaml`, globs written from that
|
|
21
|
+
root. Projects without Stitch references are unaffected.
|
|
22
|
+
- **`novahiz-stitch-fidelity` skill**: `scripts/stitch_capture.mjs` (adb:
|
|
23
|
+
uiautomator dump, screencap, input, adb.autoStart), `scripts/stitch_composite.mjs`
|
|
24
|
+
(screenshot ↔ design overlay), the fidelity judgment grid, capture
|
|
25
|
+
recipes and a `verify.config.defaults.json`.
|
|
26
|
+
- **`stitch-fidelity` provider** (`kind: commands`): adb prerequisite with
|
|
27
|
+
per-OS bootstrap (winget / brew / apt), categories `design-ui`, `expo`,
|
|
28
|
+
`flutter`; `stitch_capture.mjs --check` reports the seven readiness items.
|
|
29
|
+
- Optional `stitch-verify` roadmap step (`kind: verify`) in two roadmaps.
|
|
30
|
+
- Tests: `tests/gate-stitch-fidelity.test.ts` — project-scoped enforcement.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- README headline and counts refreshed: 99 skills, 13 gate rules, 8 MCP
|
|
35
|
+
providers, version line at 0.6.0 (was advertising 96/11/7 and 0.4.1).
|
|
36
|
+
|
|
8
37
|
## [0.5.2] - 2026-10-05
|
|
9
38
|
|
|
10
39
|
Maintenance release closing the three leftovers traced right after `0.5.1`:
|
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> **Couche d'application sans dépendance pour les agents de code IA** — classe les prompts, attribue des roadmaps d'exécution, bloque les éditions non sûres tant que les bonnes skills ne sont pas chargées, et persiste les décisions d'une session à l'autre — tout cela de façon déterministe, sans appel de modèle.
|
|
4
4
|
|
|
5
|
-
17 catégories,
|
|
5
|
+
17 catégories, 99 skills, 13 gate rules, 8 MCP providers — tout déterministe, tout local, tout JSON.
|
|
6
6
|
|
|
7
|
-
> **Version 0.
|
|
7
|
+
> **Version 0.6.0** — publiée sur npm (`npm install -g --allow-scripts=novahiz novahiz` installe et configure tout : core, plugin, agent, skills packs et Playwright MCP). `CHANGELOG.md` et `scripts/` sont dans le tarball.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
┌──────────────────────────────────────────────────────────────────────────┐
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
└─ tier, skills ──────│ └─ block : charger les skills nommées, retry une fois
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
**17 catégories**, **
|
|
38
|
+
**17 catégories**, **99 skills**, **13 gate rules**, **8 MCP providers** — tout déterministe, tout local, tout JSON.
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
@@ -310,7 +310,7 @@ Deux pièces complémentaires : la session consomme moins sans perdre en puissan
|
|
|
310
310
|
|
|
311
311
|
## Skills installées
|
|
312
312
|
|
|
313
|
-
Novahiz livre
|
|
313
|
+
Novahiz livre 99 skills couvrant toutes les catégories :
|
|
314
314
|
|
|
315
315
|
| Catégorie | Skills | Objectif |
|
|
316
316
|
|-----------|--------|----------|
|
package/catalog/categories.json
CHANGED
|
@@ -198,6 +198,13 @@
|
|
|
198
198
|
"kind": "skill",
|
|
199
199
|
"requireSkills": ["dart-add-unit-test"]
|
|
200
200
|
},
|
|
201
|
+
{
|
|
202
|
+
"id": "stitch-verify",
|
|
203
|
+
"label": "Verify screens against Stitch references (projects carrying stitch/**)",
|
|
204
|
+
"kind": "verify",
|
|
205
|
+
"requireSkills": ["novahiz-stitch-fidelity"],
|
|
206
|
+
"optional": true
|
|
207
|
+
},
|
|
201
208
|
{
|
|
202
209
|
"id": "impeccable-critique",
|
|
203
210
|
"label": "Impeccable critique (UI work)",
|
|
@@ -307,6 +314,13 @@
|
|
|
307
314
|
"kind": "edit",
|
|
308
315
|
"requireSkills": ["novahiz-implement"]
|
|
309
316
|
},
|
|
317
|
+
{
|
|
318
|
+
"id": "stitch-verify",
|
|
319
|
+
"label": "Verify screens against Stitch references (projects carrying stitch/**)",
|
|
320
|
+
"kind": "verify",
|
|
321
|
+
"requireSkills": ["novahiz-stitch-fidelity"],
|
|
322
|
+
"optional": true
|
|
323
|
+
},
|
|
310
324
|
{
|
|
311
325
|
"id": "converge",
|
|
312
326
|
"label": "Converge",
|
package/catalog/providers.json
CHANGED
|
@@ -108,6 +108,31 @@
|
|
|
108
108
|
"categories": ["audit"],
|
|
109
109
|
"purpose": "Local stdio static-analysis server (zero npm dependency): scans files and directories with local security rules (SQL injection, XSS, command injection, hardcoded secrets, insecure deserialization), creates custom rules behind a ReDoS gate, exports json/sarif/csv/html. Original clean-room implementation for the audit category."
|
|
110
110
|
},
|
|
111
|
+
{
|
|
112
|
+
"id": "stitch-fidelity",
|
|
113
|
+
"label": "Stitch fidelity capture (adb + Android emulator)",
|
|
114
|
+
"kind": "commands",
|
|
115
|
+
"requires": ["adb"],
|
|
116
|
+
"bootstrap": {
|
|
117
|
+
"win32": [
|
|
118
|
+
"winget install --id Google.PlatformTools",
|
|
119
|
+
"winget install --id Google.AndroidStudio"
|
|
120
|
+
],
|
|
121
|
+
"darwin": [
|
|
122
|
+
"brew install --cask android-platform-tools",
|
|
123
|
+
"brew install --cask android-commandlinetools"
|
|
124
|
+
],
|
|
125
|
+
"linux": [
|
|
126
|
+
"sudo apt-get install -y adb",
|
|
127
|
+
"sudo apt-get install -y android-sdk"
|
|
128
|
+
],
|
|
129
|
+
"default": ["install Android platform-tools (adb) and the Android emulator on PATH"]
|
|
130
|
+
},
|
|
131
|
+
"source": "https://github.com/novahiz/novahiz",
|
|
132
|
+
"license": "Apache-2.0",
|
|
133
|
+
"categories": ["design-ui", "expo", "flutter"],
|
|
134
|
+
"purpose": "Prerequisite for skill novahiz-stitch-fidelity: `adb` drives the capture (uiautomator dump, screencap, input). `emulator` is only needed to auto-start a device when nothing is online (adb.autoStart); `stitch_capture.mjs --check` reports all seven readiness items."
|
|
135
|
+
},
|
|
111
136
|
{
|
|
112
137
|
"id": "novahiz-gate",
|
|
113
138
|
"label": "novahiz-gate",
|
package/catalog/rules.json
CHANGED
|
@@ -156,5 +156,24 @@
|
|
|
156
156
|
]
|
|
157
157
|
},
|
|
158
158
|
"require": ["token-economy"]
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"id": "R16-stitch-fidelity",
|
|
162
|
+
"description": "Load novahiz-stitch-fidelity before editing the UI of a project that carries Stitch references (stitch/**): capture, composite and fidelity verdict must be part of that loop.",
|
|
163
|
+
"when": {
|
|
164
|
+
"match": "all",
|
|
165
|
+
"pathGlobs": [
|
|
166
|
+
"**/*.tsx",
|
|
167
|
+
"**/*.jsx",
|
|
168
|
+
"**/*.vue",
|
|
169
|
+
"**/*.svelte",
|
|
170
|
+
"**/*.dart",
|
|
171
|
+
"**/*.css",
|
|
172
|
+
"**/*.scss",
|
|
173
|
+
"**/*.html"
|
|
174
|
+
],
|
|
175
|
+
"projectGlobs": ["stitch/**"]
|
|
176
|
+
},
|
|
177
|
+
"require": ["novahiz-stitch-fidelity"]
|
|
159
178
|
}
|
|
160
179
|
]
|
package/dist/gate.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
1
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
2
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
3
|
import { hasPlaceholder, hasProse, hasStyle, isTrivial } from "./content.js";
|
|
3
4
|
import { determineTier } from "./complexity.js";
|
|
4
5
|
// Shared ledger enforcement (audit P1-D/M1): the CLI command and the MCP
|
|
@@ -215,6 +216,76 @@ export function contentSatisfies(content, patterns) {
|
|
|
215
216
|
}
|
|
216
217
|
});
|
|
217
218
|
}
|
|
219
|
+
// R16: "only in projects that carry X" cannot be expressed per file — the gate
|
|
220
|
+
// sees the edited path, not the repository. `when.projectGlobs` closes that gap:
|
|
221
|
+
// the globs are resolved from the project root (the nearest ancestor of the
|
|
222
|
+
// edited file holding .git, package.json or pubspec.yaml) and scanned from the
|
|
223
|
+
// glob's literal prefix, so a rule never walks node_modules.
|
|
224
|
+
const PROJECT_ROOT_MARKERS = [".git", "package.json", "pubspec.yaml"];
|
|
225
|
+
const PROJECT_SKIP_DIRS = new Set([".git", "node_modules"]);
|
|
226
|
+
export function projectRootOf(filePath) {
|
|
227
|
+
let dir = dirname(resolve(filePath || "."));
|
|
228
|
+
for (;;) {
|
|
229
|
+
if (PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(dir, marker))))
|
|
230
|
+
return dir;
|
|
231
|
+
const parent = dirname(dir);
|
|
232
|
+
if (parent === dir)
|
|
233
|
+
return null;
|
|
234
|
+
dir = parent;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function listFilesUnder(dir, out) {
|
|
238
|
+
let entries;
|
|
239
|
+
try {
|
|
240
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return; // unreadable directory: contributes nothing rather than throwing
|
|
244
|
+
}
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
const full = join(dir, entry.name);
|
|
247
|
+
if (entry.isDirectory()) {
|
|
248
|
+
if (!PROJECT_SKIP_DIRS.has(entry.name))
|
|
249
|
+
listFilesUnder(full, out);
|
|
250
|
+
}
|
|
251
|
+
else if (entry.isFile()) {
|
|
252
|
+
out.push(full);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
export function projectGlobsMatch(root, globs) {
|
|
257
|
+
if (!root) {
|
|
258
|
+
console.error("[Novahiz] projectGlobs: project root could not be resolved — the rule will not apply");
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
for (const glob of globs) {
|
|
262
|
+
const normalized = glob.replace(/\\/g, "/");
|
|
263
|
+
// Scanning starts at the literal prefix: "stitch/**" only reads stitch/.
|
|
264
|
+
// A glob whose prefix is pure wildcards cannot be scoped — fail closed on
|
|
265
|
+
// that pattern (never matches) instead of walking the whole project.
|
|
266
|
+
const literalPrefix = normalized.split(/[?*[\]]/, 1)[0].replace(/\/+$/, "");
|
|
267
|
+
if (literalPrefix.length === 0) {
|
|
268
|
+
console.error(`[Novahiz] projectGlobs "${glob}" must start with a literal segment — skipped`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const base = join(root, literalPrefix);
|
|
272
|
+
const stats = statSync(base, { throwIfNoEntry: false });
|
|
273
|
+
if (!stats)
|
|
274
|
+
continue;
|
|
275
|
+
const candidates = [];
|
|
276
|
+
if (stats.isFile())
|
|
277
|
+
candidates.push(base);
|
|
278
|
+
else
|
|
279
|
+
listFilesUnder(base, candidates);
|
|
280
|
+
const pattern = globToRegExp(normalized);
|
|
281
|
+
for (const file of candidates) {
|
|
282
|
+
const rel = relative(root, file).split("\\").join("/");
|
|
283
|
+
if (pattern.test(rel))
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
218
289
|
function selectorMatches(rule, classification, path, categories, pathless = false) {
|
|
219
290
|
const when = rule.when;
|
|
220
291
|
const checks = [];
|
|
@@ -290,9 +361,22 @@ export function evaluateGate(input) {
|
|
|
290
361
|
const tier = input.tier ?? determineTier(prompt.length > 0 ? prompt : content);
|
|
291
362
|
const requiredSkills = [];
|
|
292
363
|
const matchedRules = [];
|
|
364
|
+
// Resolved lazily: only a rule carrying projectGlobs pays for the walk up to
|
|
365
|
+
// the project root (and the result is reused by every following rule).
|
|
366
|
+
let projectRoot;
|
|
293
367
|
for (const rule of input.spec.rules) {
|
|
294
368
|
if (!selectorMatches(rule, classification, path, categories, pathless))
|
|
295
369
|
continue;
|
|
370
|
+
if (rule.when.projectGlobs && rule.when.projectGlobs.length > 0) {
|
|
371
|
+
// Pathless probes carry no file: there is no repository to inspect, so
|
|
372
|
+
// an unprovable project condition never grants the rule (fail closed).
|
|
373
|
+
if (pathless)
|
|
374
|
+
continue;
|
|
375
|
+
if (projectRoot === undefined)
|
|
376
|
+
projectRoot = projectRootOf(path);
|
|
377
|
+
if (!projectGlobsMatch(projectRoot, rule.when.projectGlobs))
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
296
380
|
// Tier gating for R6-Novahiz: trivial = skip entirely, lite = only implement+converge
|
|
297
381
|
if (rule.id === "R6-Novahiz") {
|
|
298
382
|
if (tier === "trivial")
|
package/package.json
CHANGED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: novahiz-stitch-fidelity
|
|
3
|
+
description: Prove a screen matches its Stitch reference instead of asserting it — capture the running app, composite reference vs render with a fresh marker, judge on a fixed grid, then fix and re-verify until the verdict is clean. Use when a screen originates from a Stitch maquette, when the user asks whether the build is identical to the mockup, before shipping screens in a project that carries stitch/ exports, or when a fidelity todo must be closed with evidence. Different from novahiz-wcag-audit (accessibility) and impeccable (design quality judgement): this one returns a per-screen PASS or DIVERGE verdict against one specific reference image, with measured numbers. Works for Expo, Flutter and any adb-driven mobile app; needs stitch/verify.config.json in the project.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: novahiz
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# novahiz-stitch-fidelity
|
|
10
|
+
|
|
11
|
+
A fidelity claim is only true once a screenshot of the running app has been compared with the Stitch
|
|
12
|
+
reference **in the same session, with a marker that proves the image is current**. This skill is the
|
|
13
|
+
loop that produces that proof: reach the screen, capture it, build the comparison, judge it, fix it,
|
|
14
|
+
capture again.
|
|
15
|
+
|
|
16
|
+
The gate (rule `R16-stitch-fidelity`) blocks edits to screens in a project that carries Stitch
|
|
17
|
+
references until this skill is loaded. Load it before touching those files, ahead of the refusal.
|
|
18
|
+
|
|
19
|
+
## When this applies
|
|
20
|
+
|
|
21
|
+
- The screen was built from a Stitch maquette (a `stitchId` in the screen registry, or a reference
|
|
22
|
+
file in `stitch/`).
|
|
23
|
+
- The user asks "identique à la maquette ?", "does it match the mockup?", "vérifie la fidélité".
|
|
24
|
+
- A verification todo of kind `verify` targets screens of a Stitch project.
|
|
25
|
+
|
|
26
|
+
Not this skill: judging whether a design is *good* (use `impeccable`), whether it is *accessible*
|
|
27
|
+
(use `novahiz-wcag-audit`), whether the *code* is correct (use `novahiz-converge`).
|
|
28
|
+
|
|
29
|
+
## Contract: `stitch/verify.config.json`
|
|
30
|
+
|
|
31
|
+
Everything project-specific lives there, so both scripts stay framework-agnostic. Create it next to
|
|
32
|
+
the references if it does not exist.
|
|
33
|
+
|
|
34
|
+
```jsonc
|
|
35
|
+
{
|
|
36
|
+
"adb": { "serial": "emulator-5554", "density": 2.625 }, // density = dp -> px factor
|
|
37
|
+
"launch": {
|
|
38
|
+
// {route} is replaced by the route of the screen. Leave null to use the ladder below.
|
|
39
|
+
"deepLink": "exp://10.0.2.2:8081/--/{route}", // Expo Go example; Flutter: "myapp://"
|
|
40
|
+
"waitForIdleMs": 4500,
|
|
41
|
+
"batch": 3 // screens per shell command, max 3
|
|
42
|
+
},
|
|
43
|
+
"screens": [
|
|
44
|
+
{ "id": "otp", "route": "/otp", "ref": "stitch/10-otp-hi.png", "stitchId": "0e0fad..." },
|
|
45
|
+
{ "id": "home", "route": "/", "ref": "stitch/01-home-hi.png" }
|
|
46
|
+
],
|
|
47
|
+
"out": "stitch/_cmp", // git-ignored: captures, bounds, composites
|
|
48
|
+
"serve": { "root": "stitch", "port": 8123 },
|
|
49
|
+
"project": { "stitchProjectId": "14523755384223316097" } // optional, for MCP fetch
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## The loop
|
|
54
|
+
|
|
55
|
+
Scripts live in this skill's `scripts/` directory (base directory shown with the skill). Run them
|
|
56
|
+
from the project root so relative paths in the config resolve.
|
|
57
|
+
|
|
58
|
+
1. **References first.** Confirm every screen you are about to judge has a local reference PNG.
|
|
59
|
+
Missing one: fetch it from the Stitch MCP (`stitch.get_screen`, project id above, `htmlCode`
|
|
60
|
+
plus a screenshot) or from the exported `*-hi.png`, and write it to the path in `config.screens`.
|
|
61
|
+
No reference, no verdict — record the screen as *not evaluable*.
|
|
62
|
+
2. **Server up.** The app must be running (Metro / `flutter run`) in an **external terminal**, and
|
|
63
|
+
the device awake and unlocked.
|
|
64
|
+
3. **Capture:** `node <scripts>/stitch_capture.mjs --config stitch/verify.config.json [--only otp,home]`
|
|
65
|
+
Writes `<out>/<id>.png` and `<out>/<id>.bounds.json` (uiautomator bounds, for measured numbers)
|
|
66
|
+
and prints a per-screen status table (`ok` / `unreachable`). Non-zero exit if any selected screen
|
|
67
|
+
failed. Never run more than `launch.batch` screens in one command: adb drops past ~3.
|
|
68
|
+
4. **Composite:** `node <scripts>/stitch_composite.mjs --config stitch/verify.config.json`
|
|
69
|
+
Writes `<out>/verify_<nonce>.html`: reference and capture side by side, `<img>` cache-busted with
|
|
70
|
+
`?v=<nonce>`, a unique marker `STITCH_FRESH_<nonce>`, and a legend per screen with the deltas
|
|
71
|
+
computed from the bounds files (bottom inset px, tap target sizes, text bounds).
|
|
72
|
+
5. **Look at the composite, fresh.** Serve `serve.root` on `serve.port` and open
|
|
73
|
+
`http://localhost:<port>/_cmp/verify_<nonce>.html?v=<nonce>` in a **new browser tab**, then take a
|
|
74
|
+
screenshot of that tab. Before any verdict, confirm the marker `STITCH_FRESH_<nonce>` is visible
|
|
75
|
+
in what you just captured. Not visible → you are looking at a stale render: restart at step 5.
|
|
76
|
+
6. **Judge** every screen on the grid in `references/judgment-grid.md`. Each line gets PASS or
|
|
77
|
+
DIVERGE **with a number** (px, dp, hex, token name). A DIVERGE without a measurement is a guess.
|
|
78
|
+
7. **Fix** the divergences in the app code, then re-run steps 3 → 5 for those screens only.
|
|
79
|
+
8. **Close** with the table: screen, verdict, measurement, commit. Screens that stayed unreachable
|
|
80
|
+
are reported as *not evaluable* with the reason. Never report a screen as matching because the
|
|
81
|
+
code "looks like" the reference.
|
|
82
|
+
|
|
83
|
+
Repeat 3 → 7 until every reachable screen is PASS, or until the remaining divergences are recorded
|
|
84
|
+
as known and accepted.
|
|
85
|
+
|
|
86
|
+
## Reaching a screen
|
|
87
|
+
|
|
88
|
+
Not every screen is one deep link away. Use the ladder in order and stop when one works:
|
|
89
|
+
|
|
90
|
+
1. **Deep link** — `config.launch.deepLink`, cold start when the route is guarded.
|
|
91
|
+
2. **Initial route** — start the app and the screen appears first (useful for `/`).
|
|
92
|
+
3. **Taps** — drive the real UI: `uiautomator dump`, read `bounds`, `adb shell input tap x y`.
|
|
93
|
+
Prefer this over hacks; a screen only reachable through taps must be reached through taps.
|
|
94
|
+
4. **Not evaluable** — auth wall, backend state, or an animation that cannot be settled. Record the
|
|
95
|
+
screen with the reason and move on. **This is a valid outcome; a fabricated verdict is not.**
|
|
96
|
+
|
|
97
|
+
Details per framework (Expo vs Flutter, launch, wait, gesture insets) in
|
|
98
|
+
`references/capture-recipes.md`.
|
|
99
|
+
|
|
100
|
+
## Failure notes
|
|
101
|
+
|
|
102
|
+
- `uiautomator dump` fails with "idle state" while an animation runs: wait, retry once, then tap a
|
|
103
|
+
neutral point to settle the UI.
|
|
104
|
+
- adb silently truncates `screencap`: always use `adb exec-out screencap -p`.
|
|
105
|
+
- dp → px: multiply by `adb.density`. Never compare raw dp to raw px.
|
|
106
|
+
- Composites are git-ignored artifacts. Commit app code only — never `stitch/_cmp/`.
|
|
107
|
+
- The reference PNG must be the *hi* export, not a thumbnail; a scaled reference makes every
|
|
108
|
+
spacing comparison wrong.
|
|
109
|
+
|
|
110
|
+
## Sources
|
|
111
|
+
|
|
112
|
+
Loop, anti-staleness protocol and access ladder synthesised from the Novahiz Stitch integration
|
|
113
|
+
sessions (Expo + adb). Reference: Stitch exports and the project's own `stitch/` folder.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Global defaults for the novahiz-stitch-fidelity skill.
|
|
3
|
+
// Shipped with Novahiz: every project inherits these, a project's own
|
|
4
|
+
// stitch/verify.config.json only overrides what is really project specific
|
|
5
|
+
// (its screens, routes, references and deep link template).
|
|
6
|
+
"adb": {
|
|
7
|
+
"serial": "auto",
|
|
8
|
+
"autoStart": true,
|
|
9
|
+
"density": null,
|
|
10
|
+
"bootTimeoutMs": 180000
|
|
11
|
+
},
|
|
12
|
+
"launch": {
|
|
13
|
+
"deepLink": null,
|
|
14
|
+
"waitForIdleMs": 6000,
|
|
15
|
+
"maxWaitMs": 30000,
|
|
16
|
+
"pollMs": 800,
|
|
17
|
+
"tapSettleMs": 900,
|
|
18
|
+
"batch": 3,
|
|
19
|
+
"coldStart": false,
|
|
20
|
+
"resetScroll": true,
|
|
21
|
+
"scrollSettleMs": 900
|
|
22
|
+
},
|
|
23
|
+
"ready": {
|
|
24
|
+
"minTexts": 3,
|
|
25
|
+
"minNodes": 12
|
|
26
|
+
},
|
|
27
|
+
"out": "stitch/_cmp",
|
|
28
|
+
"serve": { "root": "stitch", "port": 8123 }
|
|
29
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Capture recipes
|
|
2
|
+
|
|
3
|
+
Per-framework launch, device commands, and the anti-staleness protocol. Everything else lives in
|
|
4
|
+
`SKILL.md` and `stitch/verify.config.json`.
|
|
5
|
+
|
|
6
|
+
## Device basics
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
adb devices # serial goes in config.adb.serial
|
|
10
|
+
adb -s <serial> shell wm size # e.g. 1080x2424
|
|
11
|
+
adb -s <serial> shell wm density # e.g. 420 -> 2.625 px per dp
|
|
12
|
+
adb -s <serial> shell input keyevent 82 # wake
|
|
13
|
+
adb -s <serial> exec-out screencap -p > out.png # never plain `screencap` (truncates)
|
|
14
|
+
adb -s <serial> shell uiautomator dump /sdcard/d.xml && adb -s <serial> pull /sdcard/d.xml
|
|
15
|
+
adb -s <serial> shell input tap <x> <y>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`uiautomator dump` fails with *"unable to get idle state"* while an animation runs. Wait, retry once,
|
|
19
|
+
then tap an empty area of the screen to settle it before the second attempt.
|
|
20
|
+
|
|
21
|
+
**Batch at most 3 screens per shell invocation.** Past that adb drops the connection and the batch
|
|
22
|
+
dies midway; the capture script enforces `launch.batch`.
|
|
23
|
+
|
|
24
|
+
## Expo (React Native / Expo Go)
|
|
25
|
+
|
|
26
|
+
- Metro runs in an **external terminal** (`npx expo start`), never as a background shell of the agent.
|
|
27
|
+
- Emulator reaches the host at `10.0.2.2`: `exp://10.0.2.2:8081/--/<route>`.
|
|
28
|
+
- Cold start a guarded route: `adb shell am force-stop <package>` first, then
|
|
29
|
+
`adb shell am start -a android.intent.action.VIEW -d "<deeplink>"`.
|
|
30
|
+
- Route `/foo` maps to `.../--/foo`; the app root is `--/` (with a trailing slash).
|
|
31
|
+
- If a route renders blank, the file may be misnamed (`index.tsx` serves `/`, not `/index`): verify
|
|
32
|
+
with `uiautomator dump` before blaming the capture.
|
|
33
|
+
|
|
34
|
+
## Flutter
|
|
35
|
+
|
|
36
|
+
- `flutter run` in an external terminal; keep the same hot-reload session while fixing.
|
|
37
|
+
- Deep link through the registered scheme: `adb shell am start -a android.intent.action.VIEW -d "<scheme>://<path>"`.
|
|
38
|
+
- Widgets that are not routable (dialogs, bottom sheets) are reached by tap: dump, read bounds,
|
|
39
|
+
`input tap`.
|
|
40
|
+
- Take screenshots from the device, never from the IDE preview or a widget test — neither renders
|
|
41
|
+
the real theme, real fonts, or real insets.
|
|
42
|
+
|
|
43
|
+
## Reaching a screen (the ladder)
|
|
44
|
+
|
|
45
|
+
1. Deep link from `launch.deepLink`.
|
|
46
|
+
2. Initial route (cold start, screen appears first).
|
|
47
|
+
3. Taps driven by `uiautomator dump` bounds.
|
|
48
|
+
4. **Not evaluable** — record `unreachable` with the reason (auth wall, backend state, animation
|
|
49
|
+
that cannot settle). A recorded `unreachable` is an honest result; a fabricated PASS is not.
|
|
50
|
+
|
|
51
|
+
## Anti-staleness protocol (mandatory before any verdict)
|
|
52
|
+
|
|
53
|
+
A rendered file read back by an agent can be stale — the viewer, the cache, or the tool may hand you
|
|
54
|
+
the previous image. Never compare what you did not just prove is fresh.
|
|
55
|
+
|
|
56
|
+
1. Build the composite (`stitch_composite.mjs`) — it emits `STITCH_FRESH_<nonce>` in the page and
|
|
57
|
+
appends `?v=<nonce>` to **every** `<img>`.
|
|
58
|
+
2. Serve `serve.root` on `serve.port` (`npx http-server stitch -p 8123`, or
|
|
59
|
+
`python -m http.server 8123 -d stitch`, in an external terminal).
|
|
60
|
+
3. Open `http://localhost:<port>/_cmp/verify_<nonce>.html?v=<nonce>` in a **new browser tab**.
|
|
61
|
+
4. Screenshot that tab.
|
|
62
|
+
5. Confirm the marker string is present in the screenshot / DOM you just captured. Missing → the
|
|
63
|
+
render is stale: reload with a new `?v=` and repeat from step 3.
|
|
64
|
+
6. Only then form a verdict.
|
|
65
|
+
|
|
66
|
+
## Fix loop
|
|
67
|
+
|
|
68
|
+
Change the screen code → hot reload → `stitch_capture.mjs --only <ids>` → `stitch_composite.mjs` →
|
|
69
|
+
steps 3-5 of the protocol → re-judge **only those screens**. Cap iterations; after the cap, record
|
|
70
|
+
the remaining divergences with their measurements instead of looping silently.
|
|
71
|
+
|
|
72
|
+
## Out of scope for commits
|
|
73
|
+
|
|
74
|
+
`stitch/_cmp/` holds captures, bounds JSON and composites — all git-ignored. Commit app code only.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Judgment grid
|
|
2
|
+
|
|
3
|
+
Run every reachable screen through these eight lines. Each line ends with **PASS** or **DIVERGE**,
|
|
4
|
+
and a DIVERGE carries a measurement: px, dp (after `adb.density`), hex, or token name. A verdict
|
|
5
|
+
without a number is a guess — discard it.
|
|
6
|
+
|
|
7
|
+
## 1. Layout and hierarchy
|
|
8
|
+
|
|
9
|
+
- Same skeleton as the reference: header, primary block, action zone, tab bar. No extra block, no
|
|
10
|
+
missing block.
|
|
11
|
+
- Measure with the anchor bounds from `<id>.bounds.json`, not by eye. Overlay the reference at the
|
|
12
|
+
same device width and compare the block edges.
|
|
13
|
+
|
|
14
|
+
## 2. Spacing and rhythm
|
|
15
|
+
|
|
16
|
+
- Padding, gaps and margins equal the reference within 1 dp. Report the delta in dp *and* px.
|
|
17
|
+
- Rhythm: the vertical gaps inside a stack are consistent with each other, not just with the ref.
|
|
18
|
+
|
|
19
|
+
## 3. Color and surfaces
|
|
20
|
+
|
|
21
|
+
- Every fill matches a design token (`tokens.ts`, `theme`, `ColorScheme`) — no raw hex in a screen
|
|
22
|
+
file that duplicates an existing token.
|
|
23
|
+
- Opaque vs translucent: a scrim or sticky footer must be opaque or explicitly conditional. A
|
|
24
|
+
`rgba()` backdrop painted outside its modal is a DIVERGE even when the modal is closed.
|
|
25
|
+
- Check the reference at full resolution for subtle tints; a thumbnail hides them.
|
|
26
|
+
|
|
27
|
+
## 4. Typography
|
|
28
|
+
|
|
29
|
+
- Sizes, weights and line heights match the reference token scale. Compare the token values against
|
|
30
|
+
the reference's own type ramp, not against each other.
|
|
31
|
+
- No default platform font creeping in where the project declares one.
|
|
32
|
+
|
|
33
|
+
## 5. Copy
|
|
34
|
+
|
|
35
|
+
- Strings identical to the reference: wording, punctuation, casing, plural.
|
|
36
|
+
- Project forbidden-terms list respected (for a West-African FCFA app: no supranational-institution
|
|
37
|
+
names in product copy). Exact match required — this line has zero tolerance.
|
|
38
|
+
|
|
39
|
+
## 6. States
|
|
40
|
+
|
|
41
|
+
- Whatever state the reference shows (empty, loading, error, disabled, focused) exists in the build,
|
|
42
|
+
and a state the reference does *not* show is not painted unconditionally.
|
|
43
|
+
|
|
44
|
+
## 7. Native-ness
|
|
45
|
+
|
|
46
|
+
- Real platform components (native button, native list row) instead of web-shaped markup. Nested
|
|
47
|
+
interactive elements, `<div>`-style wrappers, and browser-only behaviour are DIVERGE.
|
|
48
|
+
|
|
49
|
+
## 8. Gesture and safe-area clearance
|
|
50
|
+
|
|
51
|
+
- Bottom action bars clear the system gesture area: measure the distance from the last interactive
|
|
52
|
+
element to the screen bottom in px, convert to dp, and compare with the reference.
|
|
53
|
+
- Pattern to check in code: `paddingBottom` driven by an inset **plus** a constant (`max(inset, 16) +
|
|
54
|
+
32`), never the raw inset value.
|
|
55
|
+
- No tappable control within 8 dp of another (fat-finger collisions).
|
|
56
|
+
|
|
57
|
+
## Tolerance policy
|
|
58
|
+
|
|
59
|
+
| Dimension | Tolerance |
|
|
60
|
+
|---|---|
|
|
61
|
+
| Copy | exact |
|
|
62
|
+
| Colors | exact token match |
|
|
63
|
+
| Layout blocks, spacing | 1 dp |
|
|
64
|
+
| Gesture clearance | must exceed the device inset |
|
|
65
|
+
|
|
66
|
+
Anything outside tolerance is DIVERGE. When two lines disagree (looks right, measures wrong),
|
|
67
|
+
**the measurement wins**.
|