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 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, 96 skills, 11 gate rules, 7 MCP providers — tout déterministe, tout local, tout JSON.
5
+ 17 catégories, 99 skills, 13 gate rules, 8 MCP providers — tout déterministe, tout local, tout JSON.
6
6
 
7
- > **Version 0.4.1** — 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.
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**, **96 skills**, **11 gate rules**, **7 MCP providers** — tout déterministe, tout local, tout JSON.
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 98 skills couvrant toutes les catégories :
313
+ Novahiz livre 99 skills couvrant toutes les catégories :
314
314
 
315
315
  | Catégorie | Skills | Objectif |
316
316
  |-----------|--------|----------|
@@ -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",
@@ -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",
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novahiz",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Enforce AI agent skills before every file edit: prompt classifier, skill catalog, and pre-edit gate.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -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**.