@bongos/core 1.19.463 → 1.19.465

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.
Files changed (31) hide show
  1. package/.bongos-core.json +82 -27
  2. package/.claude/skills/design/SKILL.md +5 -1
  3. package/docs/adr/0222-the-image-family-under-the-hero-only-rule.md +36 -0
  4. package/docs/adr/0223-the-detector-is-two-tiers-in-the-kit.md +33 -0
  5. package/docs/adr/README.md +2 -0
  6. package/docs/file-map.md +1 -1
  7. package/docs/module-api-changelog.md +4 -0
  8. package/docs/recipes/ui-look-before-you-ship.md +2 -0
  9. package/modules/ui-design/kit/README.md +12 -0
  10. package/modules/ui-design/kit/check-mock.js +18 -5
  11. package/modules/ui-design/kit/lib.js +22 -1
  12. package/modules/ui-design/kit/render.js +4 -2
  13. package/modules/ui-design/kit/tells.js +204 -0
  14. package/modules/ui-design/module.json +1 -1
  15. package/modules/ui-design/skills/README.md +6 -2
  16. package/modules/ui-design/skills/brandkit/SKILL.md +62 -0
  17. package/modules/ui-design/skills/image-to-code/SKILL.md +66 -0
  18. package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +62 -0
  19. package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +83 -0
  20. package/modules/ui-design/skills/impeccable/SKILL.md +1 -1
  21. package/modules/ui-design/styles/grove/DESIGN.md +1 -1
  22. package/modules/ui-design/styles/grove/assets/grove-pod-cut.webp +0 -0
  23. package/modules/ui-design/styles/grove/assets/grove-pod-cut.webp.json +12 -0
  24. package/modules/ui-design/styles/grove/assets/grove-pod.jpg +0 -0
  25. package/modules/ui-design/styles/grove/assets/grove-pod.jpg.json +12 -0
  26. package/modules/ui-design/styles/grove/mock.html +15 -1
  27. package/package-lock.json +2 -2
  28. package/package.json +1 -1
  29. package/src/module-api.js +1 -1
  30. package/tests/ui_design_kit.mjs +86 -0
  31. package/tests/ui_design_skills.mjs +52 -0
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.463",
6
- "core_contract": "1.19.463",
7
- "source_commit": "0dcce51cd7eb0c0853908fce4ba5627b7ab290b2",
5
+ "core_version": "1.19.465",
6
+ "core_contract": "1.19.465",
7
+ "source_commit": "82872e3e08c740d3499036c81775be82f4ab5538",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-02T01:01:19.599Z",
9
+ "built_at": "2026-09-02T01:57:06.137Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 400,
12
+ "docs_redacted": 406,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 1817,
14
+ "functional_verbatim": 1822,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2241,
20
- "tree_sha256": "be5d78d76396268d4dd0c0a5f0959ba65febbb9a4bbe518abc4a17911d9d5980",
19
+ "file_count": 2252,
20
+ "tree_sha256": "ac3006fbcebed3829d82962a2c54bd4245e7f1b736f92b13c46ab9d56b58180b",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -127,7 +127,7 @@
127
127
  {
128
128
  "path": ".claude/skills/design/SKILL.md",
129
129
  "mode": "0000644",
130
- "sha256": "c1ed5d4eaffffde255ac3308490d86f0235b9ab66a9349684652bb3eae82ae57"
130
+ "sha256": "bbb135d166a4e17b6059506f794c47c27740a063a068f1ea28c62a0aee7728b0"
131
131
  },
132
132
  {
133
133
  "path": ".claude/skills/dev/SKILL.md",
@@ -1614,10 +1614,20 @@
1614
1614
  "mode": "0000644",
1615
1615
  "sha256": "aed01aaa88a3deb1fbf15a1183f07954db0c7867272e7c6bd9dd0b74327c6c34"
1616
1616
  },
1617
+ {
1618
+ "path": "docs/adr/0222-the-image-family-under-the-hero-only-rule.md",
1619
+ "mode": "0000644",
1620
+ "sha256": "29a762aef81944134084fd1c4d36ef3e27fcf8c3ab5a1ef0e724bcb6d8d29a67"
1621
+ },
1622
+ {
1623
+ "path": "docs/adr/0223-the-detector-is-two-tiers-in-the-kit.md",
1624
+ "mode": "0000644",
1625
+ "sha256": "736f27c4a9e86d3169954a2555209e75dfc93f84b4581417188c71a02954e731"
1626
+ },
1617
1627
  {
1618
1628
  "path": "docs/adr/README.md",
1619
1629
  "mode": "0000644",
1620
- "sha256": "f27631d3aa68cf8855420f62c4de01dd0c817d3b014e738fa1579bf92bfa16a3"
1630
+ "sha256": "414345ac797782a5049cdae5b468149be343535055a261f257eec6979701c51b"
1621
1631
  },
1622
1632
  {
1623
1633
  "path": "docs/api-reference.md",
@@ -2097,7 +2107,7 @@
2097
2107
  {
2098
2108
  "path": "docs/file-map.md",
2099
2109
  "mode": "0000644",
2100
- "sha256": "1682c8c32b5cdab0d2a654da9b6e12cf7bbbccc1cf1d791fc6a5fc247268ecc7"
2110
+ "sha256": "6aa7d3d13404a55afe9ab461eb404d86a4072b70e1565dd89e9d71b5e636364f"
2101
2111
  },
2102
2112
  {
2103
2113
  "path": "docs/handoff-template.md",
@@ -2107,7 +2117,7 @@
2107
2117
  {
2108
2118
  "path": "docs/module-api-changelog.md",
2109
2119
  "mode": "0000644",
2110
- "sha256": "9b8886ab2a71a37fc86080c63c2458c1ae14124e1ccfee39d787f066730dfc20"
2120
+ "sha256": "5bf0804834ecb65b42a29cf2936f93021fb15762735a75129ab52294b0731246"
2111
2121
  },
2112
2122
  {
2113
2123
  "path": "docs/modules-contract.md",
@@ -2337,7 +2347,7 @@
2337
2347
  {
2338
2348
  "path": "docs/recipes/ui-look-before-you-ship.md",
2339
2349
  "mode": "0000644",
2340
- "sha256": "7484993a3437cc5b48841273b50b9015c3c92fbcd9f0f4bddba92d272c58e55f"
2350
+ "sha256": "02bd598f2a1552edb9086f84b8a9beff641cca3235ced4e2eae104875204a293"
2341
2351
  },
2342
2352
  {
2343
2353
  "path": "docs/recipes/upgrading-the-core.md",
@@ -6257,12 +6267,12 @@
6257
6267
  {
6258
6268
  "path": "modules/ui-design/kit/README.md",
6259
6269
  "mode": "0000644",
6260
- "sha256": "b2b98a60d054d774d2a30d03285589e780b4ab9bda6f526a971e09dd5500cab1"
6270
+ "sha256": "da56f82f7b020ea954b47a47e28c7bf2f5614171ef1d6192f8891d848008b9be"
6261
6271
  },
6262
6272
  {
6263
6273
  "path": "modules/ui-design/kit/check-mock.js",
6264
6274
  "mode": "0000644",
6265
- "sha256": "76c97faa8f04d51900ca00cb5f8a599d105ee44347bc8f285013f1439f6483f2"
6275
+ "sha256": "286268151ff986c22e2b04548ede0e978663ef0df5bb56593ebcbe9c6b7db5d9"
6266
6276
  },
6267
6277
  {
6268
6278
  "path": "modules/ui-design/kit/contrast.js",
@@ -6322,7 +6332,7 @@
6322
6332
  {
6323
6333
  "path": "modules/ui-design/kit/lib.js",
6324
6334
  "mode": "0000644",
6325
- "sha256": "c2e6471bf9623a2dbf3a816c355051315309221a03476f4f199f825bcef070f6"
6335
+ "sha256": "e619d3a55683defd9b3560567eeb40af28de71af63865f49e43bdcd44439c047"
6326
6336
  },
6327
6337
  {
6328
6338
  "path": "modules/ui-design/kit/png.js",
@@ -6337,17 +6347,22 @@
6337
6347
  {
6338
6348
  "path": "modules/ui-design/kit/render.js",
6339
6349
  "mode": "0000644",
6340
- "sha256": "9940f65e99d822c428ae8c76128752dbf55efb664ea40480bb098b9652e87da8"
6350
+ "sha256": "e521e6f1c9bc23dc16814303b04a3e95f9a31f8389a63ac20859959bbbec8ee3"
6341
6351
  },
6342
6352
  {
6343
6353
  "path": "modules/ui-design/kit/serve.js",
6344
6354
  "mode": "0000644",
6345
6355
  "sha256": "745fdc2ff5f72951b919ba49ab871b8cdbe0f09db73e79dfa248cd01e9232cdd"
6346
6356
  },
6357
+ {
6358
+ "path": "modules/ui-design/kit/tells.js",
6359
+ "mode": "0000644",
6360
+ "sha256": "26d8b2a846be3d1ff7a3798d4c3cea01a7d5a55c89bc223c2a8c4f263579e0c7"
6361
+ },
6347
6362
  {
6348
6363
  "path": "modules/ui-design/module.json",
6349
6364
  "mode": "0000644",
6350
- "sha256": "1b7e099867d2ecb1f752652a8e765fd421f87786847d0fe39527f2aa320b5167"
6365
+ "sha256": "0f88b29039e01fb908e4351d3fdac8198206782a0afdbf5156da6e02ad312d26"
6351
6366
  },
6352
6367
  {
6353
6368
  "path": "modules/ui-design/scripts/design-sync.js",
@@ -6372,7 +6387,12 @@
6372
6387
  {
6373
6388
  "path": "modules/ui-design/skills/README.md",
6374
6389
  "mode": "0000644",
6375
- "sha256": "29d570739f8b22fa030631ae623a78c2676d4dcd8f94bbf40674617b8414a7da"
6390
+ "sha256": "084b2a7757d7b6422683ac371f6448c563d2613c4c0061f077d27a335d1df2f0"
6391
+ },
6392
+ {
6393
+ "path": "modules/ui-design/skills/brandkit/SKILL.md",
6394
+ "mode": "0000644",
6395
+ "sha256": "7b5cd9e896dcdb79a4386c863a03c628b6187fe81a0380ac50ea5dabc91fa468"
6376
6396
  },
6377
6397
  {
6378
6398
  "path": "modules/ui-design/skills/design-taste-frontend-v1/SKILL.md",
@@ -6389,10 +6409,25 @@
6389
6409
  "mode": "0000644",
6390
6410
  "sha256": "eeb33e0a2d0a227d23b67c5e6d32c5757d8d5d7249a1a6d0fe3c67d8bd95fc91"
6391
6411
  },
6412
+ {
6413
+ "path": "modules/ui-design/skills/image-to-code/SKILL.md",
6414
+ "mode": "0000644",
6415
+ "sha256": "9c07a2328416874d69897bada333f955a93ef165eb128f073fb3bf40baef2d1e"
6416
+ },
6417
+ {
6418
+ "path": "modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md",
6419
+ "mode": "0000644",
6420
+ "sha256": "664f64d1f16afb10d3027fcd7dc50031e0f090f75cf5ee72258adc78e2e2ff73"
6421
+ },
6422
+ {
6423
+ "path": "modules/ui-design/skills/imagegen-frontend-web/SKILL.md",
6424
+ "mode": "0000644",
6425
+ "sha256": "75586cc997b927c7ca5ef7f3be574f4d81bd9238b01c9b66b1a5f8c587acaa14"
6426
+ },
6392
6427
  {
6393
6428
  "path": "modules/ui-design/skills/impeccable/SKILL.md",
6394
6429
  "mode": "0000644",
6395
- "sha256": "b82bde6df7fd759d57c11e072a427bf5e88f9558deb8ce746b89fa031d06d051"
6430
+ "sha256": "ec144ae6ee19f59743e42e035f0a0fefc342bfd292353be73d50419d9df98491"
6396
6431
  },
6397
6432
  {
6398
6433
  "path": "modules/ui-design/skills/impeccable/reference/adapt.md",
@@ -6542,12 +6577,32 @@
6542
6577
  {
6543
6578
  "path": "modules/ui-design/styles/grove/DESIGN.md",
6544
6579
  "mode": "0000644",
6545
- "sha256": "f88f3ff4b0475767f63d4665bdb07caeb36550e3bd8a6c66aebc7fdc1b657887"
6580
+ "sha256": "85da78fc0333ec15f59c45aa3e07c363832e72924acbf7518661d4cf76e56fc8"
6581
+ },
6582
+ {
6583
+ "path": "modules/ui-design/styles/grove/assets/grove-pod-cut.webp",
6584
+ "mode": "0000644",
6585
+ "sha256": "4479bf93fff0d067e435699b31197eed45aa2ff40effd737c0d0a58810ca64f9"
6586
+ },
6587
+ {
6588
+ "path": "modules/ui-design/styles/grove/assets/grove-pod-cut.webp.json",
6589
+ "mode": "0000644",
6590
+ "sha256": "c529d5fbe89908ac0e273020a3d82fe961c8591d1d2ff5396dd811a90a6f21ac"
6591
+ },
6592
+ {
6593
+ "path": "modules/ui-design/styles/grove/assets/grove-pod.jpg",
6594
+ "mode": "0000644",
6595
+ "sha256": "c1804e737f57b32f2d6e4a8a45e20fc5f3667b9d8082967811f3da153c3c8f78"
6596
+ },
6597
+ {
6598
+ "path": "modules/ui-design/styles/grove/assets/grove-pod.jpg.json",
6599
+ "mode": "0000644",
6600
+ "sha256": "b4dfeca544f49d97425a3a44f8170ce35b35ba1d105f4a5cfd69ea92d0cab67e"
6546
6601
  },
6547
6602
  {
6548
6603
  "path": "modules/ui-design/styles/grove/mock.html",
6549
6604
  "mode": "0000644",
6550
- "sha256": "ee2fdbe6a16ff00be4986e8c30a3d0fb37a8a077ad83a039610b22f2d20ce835"
6605
+ "sha256": "0cca9a34bc828c30067b1007be0aaa1751e3a6b31ced65dd43e1ecdd1e6bfaf8"
6551
6606
  },
6552
6607
  {
6553
6608
  "path": "modules/ui-design/styles/grove/mock.states.json",
@@ -6562,12 +6617,12 @@
6562
6617
  {
6563
6618
  "path": "package-lock.json",
6564
6619
  "mode": "0000644",
6565
- "sha256": "7822706326857983b25da07c35e2750870c686b1bcec12f587df6a329c5a589a"
6620
+ "sha256": "6db036356b674e4ea0fd8ef3a2d300b830ce95aa66ff9b19993188a41ca0bb66"
6566
6621
  },
6567
6622
  {
6568
6623
  "path": "package.json",
6569
6624
  "mode": "0000644",
6570
- "sha256": "dcc27e26e0975c93c878be3f06c4b4a1ad0c66074ffcef687475aebe1b66277f"
6625
+ "sha256": "fcd520ba5ac29e9f0db6fad4629c7316aed291ed6428b030af7543d5c6144883"
6571
6626
  },
6572
6627
  {
6573
6628
  "path": "public-docs/index.html",
@@ -8217,7 +8272,7 @@
8217
8272
  {
8218
8273
  "path": "src/module-api.js",
8219
8274
  "mode": "0000644",
8220
- "sha256": "1547fb781bcaa6916626669f3b662489da2858d540be6e11d20a9bc0ed20a846"
8275
+ "sha256": "641bc335f6f62f9d3a6a1fbfe9f83147c3e652b0816ad008616c3f93f3d16f13"
8221
8276
  },
8222
8277
  {
8223
8278
  "path": "src/module-loader/catalog.js",
@@ -11132,12 +11187,12 @@
11132
11187
  {
11133
11188
  "path": "tests/ui_design_kit.mjs",
11134
11189
  "mode": "0000644",
11135
- "sha256": "d98b6766db33020b379489011e72135eb00492c68f3faa5c0ab55366f12777c9"
11190
+ "sha256": "3559864426fc870316dbdbf9624e7b44e54f893f4cb84d07fc26206a809c0c14"
11136
11191
  },
11137
11192
  {
11138
11193
  "path": "tests/ui_design_skills.mjs",
11139
11194
  "mode": "0000644",
11140
- "sha256": "391f6e8c5ff83e4c3c1e2148aed74c3346200efabc59657ac99820cfcfdac1ac"
11195
+ "sha256": "155aa8b197f52ec73d3654ac8cb15134998e4de81fbbf37e8d7e423efebb94b1"
11141
11196
  },
11142
11197
  {
11143
11198
  "path": "tests/ui_design_styles.mjs",
@@ -61,13 +61,17 @@ Third-party design skills reach a builder only through the module's vendoring po
61
61
 
62
62
  - *None vendored (2026-08-28).* Both origins scanned for the scoped set — `pbakaus/impeccable`, and the eleven `Leonxlnx/taste-skill` skills (`minimalist-ui`, `industrial-brutalist-ui`, `gpt-taste`, `stitch-design-taste`, `image-to-code`, `imagegen-frontend-web`, `imagegen-frontend-mobile`, `brandkit`, and the four below) — reduced to `dangerous` at the scanner's deterministic floor and landed as rebuild specs (GDS tasks). The roster, the verdicts and the way to land one are in the README; vendoring one anyway is an owner override recorded in an ADR, not a session's call.
63
63
 
64
- **Rebuilt in-house from the specs** ([ADR 0220](../../../docs/adr/<redacted>.md), task 1003325; [ADR 0221](../../../docs/adr/<redacted>.md), task 1003326): first-party skills at `modules/ui-design/skills/<name>/`, each opening with the same preamble, each naming its spec and what changed in translation. They run INSIDE the loop above: the world first, the style library as the palette source for a variant, images as hero plates only, every motion rule inside the Kill Switch, the kit as the pre-flight.
64
+ **Rebuilt in-house from the specs** ([ADR 0220](../../../docs/adr/<redacted>.md), task 1003325; [ADR 0221](../../../docs/adr/<redacted>.md), task 1003326; [ADR 0222](../../../docs/adr/<redacted>.md), task 1003328): first-party skills at `modules/ui-design/skills/<name>/`, each opening with the same preamble, each naming its spec and what changed in translation. They run INSIDE the loop above: the world first, the style library as the palette source for a variant, images as hero plates only, every motion rule inside the Kill Switch, the kit as the pre-flight.
65
65
 
66
66
  - **design-taste-frontend** — the default taste skill for a landing page, a portfolio or a marketing surface: the brief read in one line, three dials from presets, one world locked for the page, hero and layout discipline, rationed decoration, a pre-flight the kit measures. Declines dashboards, tables, wizards and editors.
67
67
  - **design-taste-frontend-v1** — the earlier generation, kept distinct: a fixed baseline triple adapted in conversation, stack conventions for a served surface, the layout bans, full interaction cycles, the named vocabulary of premium patterns and the tile-grid archetypes.
68
68
  - **high-end-visual-design** — the agency-build polish pass: one texture archetype (the library's looks) and one layout archetype per project, the nested container anatomy where the world allows a container, the pill with its nested arrow disc, the spatial rhythm, choreography and scroll entry inside the Kill Switch.
69
69
  - **redesign-existing-projects** — the in-place upgrade: scan the stack, diagnose against the seven-group audit with the instance's world as the answer key, fix in impact-per-risk order, one group per commit; keeps the stack, never migrates frameworks, `probe.js` green before and after.
70
70
  - **impeccable** — the design-craft method as one skill with sub-commands: `impeccable <sub-command> <target>` loads exactly one of eighteen playbooks (shape, critique, audit, polish, harden, onboard, animate, colorize, typeset, layout, adapt, distill, clarify, optimize, document, extract, bolder, quieter); no argument prints a menu from live project signals and never auto-runs; a bare description is routed to the closest playbook; carries the craft floor (loaded right before UI is edited, never recited) and the bounded finish (build, one kit round, one batch of fixes, at most one confirming round, stop; a read-only reviewer that answers with one of four words). No hooks, no bypass agent, no telemetry, no live server: the live loop is the playwright plugin below and the headless loop is the kit; the detector is task 1003329; native mobile declines to this loop.
71
+ - **imagegen-frontend-web** — the plate-making skill under the hero-only rule: text-free hero plates, hero objects, backdrops and orbs for the slots a composition already has (the count is the slot count, committed out loud), a composition anchor per plate that is not text-left image-right, one material / light / lens across the family, the pack as the palette lock, the instance's art pipeline key, and the pair-and-sidecar output contract (a black-ground JPEG for dark, an alpha WebP cutout for light, a `<file>.json` beside each). Never a section image, a comp or a screen; writes no page code.
72
+ - **imagegen-frontend-mobile** — the same, for the 390 and 320 compositions: portrait mastheads and fields clear of the regions a phone reserves, the first view held to one focal point, a scrim under any text over a plate. A screen is the surface's states file rendered by the kit; no phone mockups, no flows of screens; a native app target declines to this loop.
73
+ - **image-to-code** — the analyse-then-implement loop with the owner's references and the instance's world as the source of truth: a structured per-image analysis, the extraction spec, a faithful translation with drift named as the failure mode, the hero and clutter rules, a clarity checklist the kit measures. The only image it may cause is the hero plate, through imagegen-frontend-web.
74
+ - **brandkit** — the identity-system method with the strategy inferred first, the logo standard and the five concept methods; the mark is drawn as inline SVG, the colour system is the pack, the type specimen is the pack's faces on a real page, the applications are the platform's surfaces rendered by the kit, the image direction is a hero-object family, and the nine-panel board is a contact sheet of those real parts. Never a generated logo, board or mockup.
71
75
 
72
76
  **Prerequisite, not vendored:** the **playwright plugin** (`claude plugin install playwright@claude-plugins-official`) is the LIVE browser-driving loop, where the kit in Step 2 is the headless one. Install it on the machine that runs a design session; the module never installs anything.
73
77
 
@@ -0,0 +1,36 @@
1
+ # ADR 0222 — The image family under the hero-only rule: plates and hero objects, never screens; the mark is drawn; the board is a contact sheet; the pair and the sidecar are the output contract
2
+
3
+ - **Status:** Accepted (2026-09-02)
4
+ - **Task:** [task 1003328](https://cloudbongos.com/builders#/task/1003328) (goal 1000083 — the ui-design module as a platform capability; the third rebuild-spec task after [ADR 0220](<redacted>.md) and [ADR 0221](<redacted>.md))
5
+ - **Owner intent (2026-08-28):** "a simple port, with ONE rewrite the upstream never had: the owner's rule that image generation is for text-free hero plates and hero objects only; UI is rebuilt from templates and the instance's world, never painted."
6
+
7
+ ## Context
8
+
9
+ The four image skills of `Leonxlnx/taste-skill` (`image-to-code`, `imagegen-frontend-web`, `imagegen-frontend-mobile`, `brandkit`) scanned `dangerous` with the rest of that origin ([ADR 0198](<redacted>.md)); their REBUILD-SPEC groups sit in the body of task 1003325. All four are built around generating **pictures of interfaces**: one design-reference image per section, one app screen per flow step inside a phone mockup, an identity board with the brand name and labels rendered into the image, and a build loop whose source of truth is the generated image. The panel flagged the eagerness mandate, the "do not be lazy with image count" rule and the must-invoke-any-image-tool directive by name. The owner's rule (2026-08-26, reaffirmed 2026-08-27, recorded in the preamble's item 4 and in the chrome world's materials table) forbids the whole category: an image with letters, a comp, a screen or a frame around one is not a hero plate.
10
+
11
+ A port that kept the method and changed only the subject had to answer what each skill *makes* when it may not make a picture of a screen.
12
+
13
+ ## Decision
14
+
15
+ 1. **The subject of every skill is the composition's plate slots, and the count is the slot count.** `imagegen-frontend-web` produces hero plates, hero objects, atmosphere backdrops and orbs for slots a surface already has (named by its `DESIGN.md`, a brief or a taste skill), commits to the list out loud, and never exceeds it; "one image per section" becomes "a plate with no slot is the floating blob the audit rejects". The composition-variety rules survive as **anchors** (field, beside-bleeding, masthead, orb, backdrop; the text-left image-right default refused by name), the palette lock becomes the pack, and cross-frame consistency becomes **one family**: one material, one key light, one lens, one ground across a surface.
16
+ 2. **A screen is the surface itself.** `imagegen-frontend-mobile` keeps the first-view rule, the mobile realities and the scrim rule, applied to portrait plates for the 390 and 320 compositions; the flow of screens is the states file rendered by the kit, the design bible is the instance's `DESIGN.md`, the phone mockup does not exist, and a native app target declines to `/design` (the same decline as `impeccable adapt`).
17
+ 3. **The source of truth for UI is the owner's references and the world, not a generated image.** `image-to-code` keeps its structured analysis, its extraction spec, its faithful-translation rule, its drift bans, its hero and clutter rules and its clarity checklist, reading the references; the one image it may cause is the hero plate, through `imagegen-frontend-web`. The "generate first, then analyse" order becomes read, extract, plate, implement.
18
+ 4. **The mark is drawn; the board is a contact sheet.** `brandkit` keeps the strategy inference, the logo standard, the five concept methods, the rhythm and the reference rule; the mark lands as inline SVG with a construction comment, the colour system as the branding pack, the type specimen as the pack's faces on a real page, the applications as the platform's surfaces rendered by the kit, the image direction as a hero-object family, and the nine-panel board as a contact sheet assembled from those real parts, a review artifact under the claim's scratch or `docs/design/`, never a generation and never under a surface's assets.
19
+ 5. **The output contract is DESIGN.md's pair and sidecar, and the key is the instance's.** What ships is the black-ground plate as JPEG for dark and the alpha cutout as WebP for light, at production size, with a `<file>.<ext>.json` sidecar beside every file (kind, model, aspect ratio, resolution, generated, round, the exact prompt, source, encoded, use), and `--plate-<name>` / `--plate-<name>-cut` tokens on `body` with root-absolute `url()`. Generation goes through the instance's art pipeline (the builder's own key at the hall's `/builders/settings#art-key` or the instance's shared key), never a credential a skill carries; the spend is logged with `cost.js` under the claim; a white ground (the model's one-in-six case) goes through the pipeline's background remover, and alpha is verified with `ffmpeg alphaextract` before a file is called a cutout. A plate is never a crop of an earlier plate.
20
+ 6. **The worked example lives in the style library's specimen, not on the front door.** The grove look's `DESIGN.md` said "this look brings none"; its specimen now carries one hero object (`styles/grove/assets/grove-pod.jpg` + `grove-pod-cut.webp`, sidecars beside them, tokens on the mock's `body`, the masthead anchor) as the proof that the skill, the pair convention and the kit's `--style` path agree, and the sentence now says so. A plate on the live front door would have been either dead weight or a design change to a surface the owner reviews; the specimen is the module's own.
21
+ 7. **The pins** (`tests/ui_design_skills.mjs`): the four exist and are declared; each names the sidecar, "text-free" and the art pipeline key in its own rules; none contains the eagerness mandate, an image-count mandate, a one-image-per-section or per-screen rule, or a must-invoke-any-image-tool directive (the ADR 0220 own-skill pins already refuse a step that mandates generating an image); `imagegen-frontend-mobile` and `brandkit` name what they will not generate. The grove specimen's plate is pinned by the existing style-library suite (the mock under the platform rules with `--pack grove`) and by the sidecar's presence beside each raster.
22
+
23
+ ## Consequences
24
+
25
+ - **Nine in-house skills ship with the module**; `/design`, the README, the module `CLAUDE.md` and `docs/file-map.md` name all nine. The core checkout still does not see them (task 1003330).
26
+ - **The style library now has one look with an asset.** The README's "a look brings no plates" default holds for the other two; a look's plates, where its owner adds them, follow this ADR's contract.
27
+ - **Three credits of the builder's own image key were spent on the worked example** (one 2k generation, one background removal), logged as art cost under the task.
28
+ - **`imagegen-frontend-mobile` is the thinnest skill of the family** by design: most of what its upstream did is the kit at 390. If an instance ever ships a native surface, the native playbooks are a new decision, not a widening of this one.
29
+
30
+ ## Rejected
31
+
32
+ - **Keeping "one image per section" for reference-only use** ("we will not ship them"): a generated section image is still a painted UI, still costs credits, and still drifts; the owner's objection was to the practice, not to the shipping.
33
+ - **Phone mockups as long as the screen inside is real** (a kit shot composited into a device frame): a frame around a screenshot is decoration the world does not have, and the shot is the evidence already.
34
+ - **A generated logo "as a starting point"**: a raster mark cannot scale, cannot be constructed, and would be reproduced from whatever the model had seen; the standard the upstream itself set (scalable, balanced, constructible) is met by drawing.
35
+ - **Landing the worked-example plate on the front door**: dead weight if unused, an owner-facing design change if used.
36
+ - **A shared `imagegen` base skill with three thin wrappers**: three files that differ only in aspect and slot are not a family; the mobile and web skills differ in what they refuse.
@@ -0,0 +1,33 @@
1
+ # ADR 0223 — The anti-pattern detector is two tiers inside the kit: mechanical floors fail, craft tells warn, a false positive is waived in the file with its reason, and nothing new is installed
2
+
3
+ - **Status:** Accepted (2026-09-02)
4
+ - **Task:** [task 1003329](https://cloudbongos.com/builders#/task/1003329) (goal 1000083 — the ui-design module as a platform capability; the fourth rebuild-spec task, after [ADR 0220](<redacted>.md), [ADR 0221](<redacted>.md) and [ADR 0222](<redacted>.md))
5
+ - **Owner intent (2026-08-28):** the parts of impeccable worth rebuilding include "a static anti-pattern detector"; the task body says to land it "as RULES in the kit that already exists, never as a second tool, never as a browser bundle, never with a hook".
6
+
7
+ ## Context
8
+
9
+ The [detector] group of impeccable's REBUILD-SPEC (the body of task 1003326) describes a deterministic rule engine of about sixty rules split between generated-design tells and general quality, usable standalone and inside the skill, over three input kinds, with machine-readable output and a gating exit code, a cascade resolver for computed values, per-project ignores, in-file waivers, a browser-injected variant, a screenshot contrast pass, and the rule that a clean scan is a floor and never proof. The origin shipped it as its own CLI plus a browser bundle, wired to PostToolUse and Stop hooks, and the panel condemned the hooks and the bundle ([ADR 0198](<redacted>.md)).
10
+
11
+ The kit already had the engine's shape: `check-mock.js` runs pure rules over a mock's source and keeps the platform's rules apart from the instance's `DESIGN.md` taste bans; `render.js` evaluates `lib.js`'s `factsInPage` in the real page per state, width and mode and audits the facts against the floors; `contrast.js` measures the pairs; every CLI exits 0 / 1 / 2 / 3. What it did not have was the detector's rule content, a place for rules that are reads rather than floors, and a way to name a false positive without deleting the rule.
12
+
13
+ ## Decision
14
+
15
+ 1. **The rules live in the kit, in two tiers, beside the two that exist.** A new pure module `kit/tells.js` carries the static rules; `check-mock.js` runs them after its platform rules and taste bans; `lib.js`'s `factsInPage` collects their computed-value twins as `tells`, which `render.js` prints as `~ tell:` lines and records per render in `report.json`. **Floors** are mechanical and unambiguous on every instance and report `FAIL`: a bounce easing (a `cubic-bezier` whose y control points leave 0..1; the platform's motion has no spring and the Kill Switch has nothing to overshoot) and a card nested in a card (from a tag-stack walk of the markup, not a DOM). **Tells** are the craft floor's generated-design signatures and general-quality reads and report `WARN`: the identical icon-heading-text grid, hero metrics, decorative section numbering, a kicker above the h1, a coloured side stripe, a hard zero-blur offset shadow, a zero-offset coloured halo, decorative blur, a default typeface as the voice, a wide measure, a long headline, the same entrance on every section, a geometric mask on a plate, unicode glyphs as icons, a rainbow or mesh gradient, the browser surfaces nobody themed, proportional numerals in data, tiny type; rendered: prose measure, tiny text, proportional numerals in cells, headline lines, an overshooting computed curve. A tell never fails a mock or a render, because **a clean scan is a floor, never proof**, and the spec's own rule is that every finding is verified in context and a false positive called out.
16
+ 2. **The world decides which tells are floors.** A kicker above the h1 is a tell everywhere and a `FAIL` where the instance's `DESIGN.md` bans the eyebrow (a new `eyebrow` key in `TASTE_BANS`, read from the Don't section like the five before it); the chrome world bans it by name, the three library looks do not, and the platform does not decide for them. The same shape is open to any tell an instance later promotes.
17
+ 3. **A false positive is waived in the file, with its reason, and never in a config.** `kit-ignore: <rule prefix>, <rule prefix> (reason)` in any HTML or CSS comment turns that rule's hit into a `WAIVE` row carrying the detail and the reason; `--raw` runs with no waivers; `--no-tells` runs the platform rules and taste bans only; `--json` prints the rows with `fails` and `warns` counts for a job that gates on them. The spec's per-project ignore file (by rule, by glob, by value, with an admin action set) is not built: the place a false positive lives is the file that triggers it, a reviewer reads the reason where the rule fires, and a repository-wide suppression is the mechanism by which a floor quietly stops being one.
18
+ 4. **Not ported, on purpose.** The purple-to-blue gradient tell (a literal colour is already a platform failure, and under the fifteen-token contract a gradient's hue is unresolvable from source); a browser-injected overlay and a namespaced console channel (the rendered twins run inside the kit's own page evaluation, which already proves the page can be evaluated); the screenshot contrast sampler (`contrast.js --url --glyphs` is that pass); a cascade resolver (the rendered tier reads computed values from the browser, which is the only honest resolver); any run on native app code; and every hook.
19
+ 5. **The pins.** `tests/ui_design_kit.mjs` holds one clean fixture that passes every floor and tell and, for every rule class, a fixture that trips it at its tier, with the case count asserted equal to the rule count so a rule cannot be added unpinned; the eyebrow promotion; the waiver, `--raw` and `--json` paths through the real CLI; the detector inside `checkMock` and `--no-tells`; `craftTells` as warning lines that `auditFloors` never counts. The hub mock `docs/design/mocks/hub/v2.html` and the three style specimens still pass every rule; `/projects` and the grove specimen still render ALL CLEAN.
20
+
21
+ ## Consequences
22
+
23
+ - **`impeccable audit` and `polish` now have the detector they pointed at.** The skill's "not here" note names `tells.js` and the waiver instead of a future task.
24
+ - **Existing mocks surface tells.** The hub mock and the three specimens each report one to three `WARN` lines (an untitled `::selection`, a `p.label` above the h1 in the specimens, one 3px side stripe in the expedition look); none fails, each is a reviewer's call, and the specimens' kicker is not a defect under their own looks.
25
+ - **The tag-stack walk is not a DOM.** It sees nesting and sibling order well enough for the structural tells and no further; a rule that needs layout belongs in the rendered tier.
26
+ - **Two of the origin's "sixty" are floors and eighteen are tells.** The rest of its count was the same rule over three input kinds, the platform rules the kit already had, and the mechanisms decision 4 declines.
27
+
28
+ ## Rejected
29
+
30
+ - **A second CLI beside `check-mock.js`** ("`kit/detect.js`"): one more thing to run, one more exit code to learn, and the same rules; the task body forbade it.
31
+ - **Every tell as a floor:** the tells are reads a world can legitimately contradict (a hard offset shadow in a brutalist look, a stripe in a ledger); failing a mock on them would make the kit the taste and the instance the exception, the inversion the module exists to prevent.
32
+ - **A per-project ignore config with an admin action set:** a suppression nobody sees where the rule fires; the file-scoped waiver with a mandatory place for a reason keeps the finding and its dismissal on the same line.
33
+ - **A hook that runs the detector on every edit:** the pre-flight is the kit and the ship gate is the sweep ([ADR 0221](<redacted>.md) D3).
@@ -308,5 +308,7 @@ This keeps the decision history honest and traceable.
308
308
  | 0219 | [**A look is a branding pack: the style library ships looks as `theme.ui` overlays, never as sheets** ([task 1003323](https://cloudbongos.com/builders#/task/1003323) · goal 1000083). The owner scoped "varying ui styles" for the ui-design module; the shape was decided by three facts: every served sheet already reads only the contract and is measured by a suite that resolves its own expressions under the pack (ADR 0196), the pack is one-mode (light values; dark is each sheet's black tier), and the world round's candidate token layers were sheets, not packs. Decisions: **D1** a look is `modules/ui-design/styles/<name>/` = `pack.json` (the `theme.ui` overlay, exactly the thirteen colours + the two font stacks and their links) + `DESIGN.md` (the derived recipes in the root DESIGN.md's frontmatter shape, thirteen in lockstep with the pack) + `mock.html` (a specimen with no `:root`, served through the stub with the pack spliced in), never a fork of a sheet; a sixteenth token is a contract change. **D2** `chrome-world` is canonical and pinned byte-for-byte to the neutral pack; its DESIGN.md points at the root one. **D3** the alternates `expedition` (natgeo-editorial) and `grove` (organic-cards) are ported by retuning the thirteen against the three sheets' own pair lists in both modes until every pair clears AA (the grove's sage lifted and its inks deepened for the hub's status vocabulary and the hall's chips); dark under both is the sheets' black. **D4** the picker rides `GDS_BRANDING_FILE`, the fixed knob `src/branding.js` already has: `STYLE=<name>` on the stub, `--style` on render/probe, `--pack <name>` on contrast/check-mock; no core change. **D5** the three token suites take `UI_DESIGN_PACK` and their drift tests keep measuring against the neutral pack; `tests/ui_design_styles.mjs` runs them once per alternate and proves the knob live with a pack that must fail. **D6** an instance adopts a look by copying `theme` into `config/branding.json`; what stays the platform's whichever look is on: the dark ground, the hall's verification vocabulary, every sheet's derived tier, the floors. Rejected: sheets per look beside world.css; a dark tier in the contract; a picker that writes the host boundary.](<redacted>.md) | ui-design / branding contract |
309
309
  | 0220 | [**An in-house rebuilt skill is a first-party skill: it lands at `skills/<name>/`, opens with the same preamble, carries a "Rebuilt from" section instead of a sidecar, and is counted** ([task 1003325](https://cloudbongos.com/builders#/task/1003325) · goal 1000083). ADR 0198 landed both scanned origins as rebuild specs and left the roster empty; this is the first rebuild — the taste core: `design-taste-frontend`, `design-taste-frontend-v1`, `high-end-visual-design`, `redesign-existing-projects` — and the policy had not said where a rebuild lives, what replaces `PROVENANCE.md`, whether a preamble written for vendored copies still fits, or whether five byte-identical copies of it trip the doc-entropy ratchet (they would have: baseline 6). Decisions: **D1** a rebuild is `modules/ui-design/skills/<name>/SKILL.md`, declared in `module.json`, materialised by step 1b's own-dir branch (no sidecar gate), first-party and counted by every ratchet — no `rebuilt/` exemption like `vendor/`, because these are our lines. **D2** one preamble worded for both origins ("vendored from a scanned origin, or rebuilt in-house from a spec"), still byte-identical everywhere; a rebuild's provenance is its `## Rebuilt from` section first after the block: the origin the SPEC was derived from (documentation only), the verdict, the spec task, "no upstream bytes consulted or copied", this ADR, and what changed in translation; no `PROVENANCE.md`, no `LICENSE`. **D3** the translation rules for every rebuilt taste skill: the palette is the pack and variants come from the style library (ADR 0219); the design system is the instance's `DESIGN.md`; imagery is hero plates and hero objects by token and never a step; every motion rule lives inside the Kill Switch (springs, magnetic hover, pointer-tracked borders are named *not available*); the upstream's bans are defaults an instance's `DESIGN.md` overrides, and where the world calls for what an audit would flag, the world is the answer key. **D4** `docs-entropy.js` skips a paragraph opening with the preamble marker beside its generated-block skip, calibrated against a plain duplicate that is still flagged. **D5** v1 is a distinct skill differing in content (the fixed triple, the stack conventions, the pattern vocabulary, the tile-grid archetypes), not a version stamp. **D6** the pins in `tests/ui_design_skills.mjs`: frontmatter name = dir, preamble first and once, the section's task link + ADR 0198 + "no upstream bytes", the style library and "hero plates" named, no image mandate, no sidecar, every door lists the four. Rejected: a sidecar with the spec's commit; a second preamble; a `rebuilt/` segment; vendoring anyway (an owner override, not taken); folding v1 into v2; raising the doc baseline.](<redacted>.md) | ui-design / skills |
310
310
  | 0221 | [**The impeccable method is one skill with sub-command playbooks: one preamble, one playbook per request, the three homes are the instance's own files, and the origin's mechanisms are dropped rather than disabled** ([task 1003326](https://cloudbongos.com/builders#/task/1003326) · goal 1000083). `pbakaus/impeccable` scanned `dangerous` (ADR 0198: hooks written into five harness configs, a child agent with permissions bypassed, an instruction override, a live-server credential in page source, an unverified remote bundle, default-on telemetry), so its method is rebuilt from the spec. ADR 0220 fixed where a rebuild lives; it had not said how a family of sub-commands lands, what the origin's three persisted homes become, or what to do with the six spec areas whose substance is the condemned mechanism. Decisions: **D1** one skill at `skills/impeccable/`: `SKILL.md` (invocation, the menu from live signals, session setup, the three homes, the routing table, the craft floor, the bounded finish, what is not here) plus `reference/<sub-command>.md`, eighteen playbooks, exactly one loaded per request (the spec's own entry rule); the preamble pinned once in `SKILL.md` and governing every playbook; the materialiser's tree copy ships `reference/` unchanged. **D2** the three homes are the instance's files (product truth = `docs/project-context.template.md` + the pack's identity; the visual system = the root `DESIGN.md` and a look's; the brief = the task's body + a fixed brief block in the ship notes); the skill creates none, `shape` proposes diffs, `document` drafts on the claim's branch with the refresh / overwrite / merge choice. **D3** dropped, not disabled, each with a destination: hooks → none (the ship gate is the sweep, the pre-flight is the kit); the bypass agent → an ordinary read-only subagent via the Agent tool; telemetry, the remote bundle, the credential → none; the live loop → the playwright plugin driving the kit's stub, variants as states, accept = commit, discard = checkout, nothing injected; the detector → task 1003329; the asset producer → the hero-plates rule and task 1003328; native → declines to `/design`; `init`/`doctor`/`pin`/installer → the materialiser is the install. **D4** the finish is the kit in bounded rounds (build, one batched inspection, one batch of fixes, at most one confirming round, stop, one budget) and the reviewer answers with one of `clean` / `recapture` / `defects` / `contradicted` after an evidence check and its own mock inventory. **D5** the craft floor is mechanics loaded late and never recited, translated for a world that may be hairlines by rule. **D6** the pins: sixteen routed playbooks, no orphan, no preamble copy in a playbook, and nothing in the tree directing a bypass, a hook write, a settings edit, telemetry, a remote download, a credential or an override. Rejected: eighteen skills; one inline file; a hooks-lite opt-in; an elevated reviewer; a `doctor`; the native playbooks.](<redacted>.md) | ui-design / skills |
311
+ | 0222 | [**The image family under the hero-only rule: plates and hero objects, never screens; the mark is drawn; the board is a contact sheet; the pair and the sidecar are the output contract** ([task 1003328](https://cloudbongos.com/builders#/task/1003328) · goal 1000083). The four image skills of `Leonxlnx/taste-skill` (scanned `dangerous`, ADR 0198) are built around pictures of interfaces: one reference image per section, one app screen per flow step in a phone mockup, an identity board with the brand name rendered in, and a build loop whose truth is the generated image; the panel flagged the eagerness mandate, the image-count rule and the must-invoke-any-tool directive by name, and the owner's rule (2026-08-26/27) forbids the category. Decisions: **D1** the subject is the composition's plate slots and the count is the slot count; variety survives as anchors (field, beside-bleeding, masthead, orb, backdrop; text-left image-right refused), the palette lock is the pack, cross-frame consistency is one family (material, light, lens, ground). **D2** a screen is the surface itself: the mobile skill makes portrait plates for 390 / 320, the flow is the states file rendered by the kit, the bible is `DESIGN.md`, no phone mockup, native declines to `/design`. **D3** the source of truth for UI is the owner's references and the world; `image-to-code` reads them (analysis, extraction spec, faithful translation, drift as the failure mode, the clarity checklist) and the only image it may cause is the hero plate. **D4** the mark is drawn as inline SVG, the colour system is the pack, the type specimen is the faces on a real page, the applications are the kit's renders, the image direction is a hero-object family, the nine-panel board is a contact sheet of real parts. **D5** the output contract is DESIGN.md's pair and sidecar (JPEG dark plate, WebP alpha cutout, `<file>.<ext>.json` beside each, `--plate-*` tokens on `body`) through the instance's art pipeline key, the spend logged, a white ground through the remover, alpha verified with ffmpeg, never a crop. **D6** the worked example is the grove specimen's hero object, not the front door. **D7** the pins: the four declared, each naming the sidecar, "text-free" and the art key; no eagerness / count / per-section / must-invoke directive. Rejected: per-section images for reference only; phone mockups around real shots; a generated logo as a start; the plate on the front door; a shared base skill with wrappers.](<redacted>.md) | ui-design / skills / imagery |
312
+ | 0223 | [**The anti-pattern detector is two tiers inside the kit: mechanical floors fail, craft tells warn, a false positive is waived in the file with its reason, and nothing new is installed** ([task 1003329](https://cloudbongos.com/builders#/task/1003329) · goal 1000083). The [detector] group of impeccable's rebuild spec (about sixty rules, a CLI plus a browser bundle wired to hooks, per-project ignores, in-file waivers, "a clean scan is a floor and never proof") lands as RULES in the kit that already exists, per the task body: no second tool, no bundle, no hook. Decisions: **D1** `kit/tells.js` carries the static rules and `check-mock.js` runs them after the platform rules and taste bans; `lib.js`'s `factsInPage` collects the computed-value twins as `tells` and `render.js` prints them as `~ tell:` lines per render, recorded in `report.json`, never counted as problems. Two tiers: floors FAIL (a bounce easing; a card nested in a card) and tells WARN (the icon-heading-text grid, hero metrics, section numbering, a kicker above the h1, a side stripe, a hard offset shadow, a coloured halo, decorative blur, a default typeface, a wide measure, a long headline, the same entrance everywhere, a geometric mask on a plate, glyphs as icons, a rainbow gradient, the browser surfaces nobody themed, proportional numerals, tiny type; rendered: measure, tiny text, numerals in cells, headline lines, an overshooting curve). **D2** the world decides which tells are floors: a kicker above the h1 is a FAIL where the instance's `DESIGN.md` bans the eyebrow (a new `eyebrow` taste key), a WARN elsewhere. **D3** a false positive is waived in the file with its reason (`kit-ignore: <rule prefix> (reason)` → a WAIVE row), `--raw` runs with none, `--no-tells` runs the platform rules only, `--json` prints rows with `fails` / `warns` counts; no per-project ignore config. **D4** not ported: the purple-to-blue tell (a literal colour already fails; hue is unresolvable under the fifteen), an injected overlay, the screenshot sampler (`contrast.js --url` is that pass), a cascade resolver (the browser is the resolver), native code, every hook. **D5** the pins: a clean fixture passes every rule, every rule class trips at its tier with the case count equal to the rule count, the promotion, the waiver / raw / json paths through the real CLI, `craftTells` as warnings; the hub mock and the three specimens still pass, `/projects` and the grove specimen still render ALL CLEAN. Rejected: a second CLI; every tell as a floor (the kit as the taste and the instance as the exception); a project-wide ignore config; an edit-time hook.](0223-the-detector-is-two-tiers-in-the-kit.md) | ui-design / kit |
311
313
 
312
314
  > ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
package/docs/file-map.md CHANGED
@@ -130,7 +130,7 @@
130
130
  │ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Three ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages) and `oversight.css` (the OVERSIGHT family — government, watch, harbor, gate, task 1003469: the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend, `people.css` the directory's identity cell (task 1003470: `/scouting` is the LEGACY DOOR to `/people` and ships the same body and script — `scouting.html` loads `people.js`; there is no `scouting.js`).
131
131
  │ ├── lifecycle/ ← CORE-DOMAIN carve (BV1.R86, default-on): the build state machine — tasks · claims · versions · done-when · goals · dependencies · claim→ship→grade→credit · github-push + merge-lock + conflict-resolve + publish-reconciler. The LAST, most-coupled carve; registers the `lifecycle` kernel port (createTask, classifyTaskKind, tallyPeerVotes, work-tracking reads). CLAUDE.md, module.json, routes/{tasks,claims,versions,done-when,goals,dependencies,gate-approvals,analytics}.js, lifecycle.js, github-push.js, merge-lock.js, conflict-resolve.js, publish-reconciler.js, task-visuals.js + task-visual-db.js + routes/visuals.js (the optional ship-time visual, task 1003109 — the route file is NOT named task-*.js on purpose; see the module CLAUDE.md).
132
132
  │ ├── copy-desk/ ← CORE-DOMAIN module (task 1003113 / R02 of goal 1000074, default-on): the COPY half of the design contract — ADR 0081 gave design TOKENS a repo-owned source of truth, text had none. Owns the FLAG (anyone in any role marks a string or a whole surface as needing an artist's attention, with a reason required in BOTH the route and a CHECK constraint) and the QUEUE an artist reads it from. READS R01's committed registry (docs/copy-registry.json, generated by scripts/gds/copy-inventory.js) and never writes it. NO LIVE CMS is the hard non-goal: no column here could hold replacement text, and copy_no_cms.mjs pins that. Key is `copy-desk`, not `copy` — the branding contract already owns a `copy` block, so the bare word reds the "core carries no module code" fitness check. CLAUDE.md, module.json, registry.js, flags.js, routes/copy-desk.js, migrations/copy_desk_001_flags.sql, tests/{copy_flags,copy_queue,copy_no_cms}.mjs. Its hall surface lives with hall-ui (public/copy-desk.{html,js,css}), per the one-key-per-web-surface precedent.
133
- │ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (modules/public-landing/public/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. skills/ (task 1003321 / ADR 0198) is the vendoring policy: policy.json (licence allowlist, verdicts that may land, sidecar fields, preamble markers), PREAMBLE.md (the platform preamble every vendored SKILL.md opens with), README.md (the roster — both origins scanned dangerous on 2026-08-28, landed as rebuild specs; nothing vendored — and the landing recipe); a landable copy goes under skills/vendor/<name>/ with PROVENANCE.md + LICENSE and is materialised into an instance's .claude/skills/ by claude-materialize.js step 1b. skills/<name>/ (task 1003325 / ADR 0220) are the in-house rebuilds from the specs, first-party and counted: design-taste-frontend (the default taste skill), design-taste-frontend-v1 (the earlier generation, kept distinct), high-end-visual-design (the polish pass), redesign-existing-projects (the in-place upgrade); each opens with the same preamble and carries a "Rebuilt from" section instead of a sidecar. impeccable/ (task 1003326 / ADR 0221) is the design-craft method as ONE skill with sub-commands: SKILL.md (the router, the menu, the craft floor, the bounded finish) + reference/<sub-command>.md (eighteen playbooks, one loaded per request); no hooks, no bypass agent, no telemetry, no live server. styles/ (task 1003323 / ADR 0219) is the style library: <name>/pack.json (the theme.ui overlay, exactly the fifteen) + DESIGN.md (the look's derived recipes, thirteen in lockstep with the pack) + mock.html (the specimen); chrome-world (pinned to the neutral pack), expedition, grove; STYLE=<name> on the kit lays a look over the instance's pack through GDS_BRANDING_FILE; tests/ui_design_styles.mjs runs the world/cosmos/hall token suites once per alternate (UI_DESIGN_PACK). config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
133
+ │ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans, then the detector), tells.js (task 1003329 / ADR 0223: the anti-pattern detector as two tiers — floors FAIL, craft tells WARN, kit-ignore waivers per file; the rendered twins live in lib.js factsInPage), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (modules/public-landing/public/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. skills/ (task 1003321 / ADR 0198) is the vendoring policy: policy.json (licence allowlist, verdicts that may land, sidecar fields, preamble markers), PREAMBLE.md (the platform preamble every vendored SKILL.md opens with), README.md (the roster — both origins scanned dangerous on 2026-08-28, landed as rebuild specs; nothing vendored — and the landing recipe); a landable copy goes under skills/vendor/<name>/ with PROVENANCE.md + LICENSE and is materialised into an instance's .claude/skills/ by claude-materialize.js step 1b. skills/<name>/ (task 1003325 / ADR 0220) are the in-house rebuilds from the specs, first-party and counted: design-taste-frontend (the default taste skill), design-taste-frontend-v1 (the earlier generation, kept distinct), high-end-visual-design (the polish pass), redesign-existing-projects (the in-place upgrade); each opens with the same preamble and carries a "Rebuilt from" section instead of a sidecar. impeccable/ (task 1003326 / ADR 0221) is the design-craft method as ONE skill with sub-commands: SKILL.md (the router, the menu, the craft floor, the bounded finish) + reference/<sub-command>.md (eighteen playbooks, one loaded per request); no hooks, no bypass agent, no telemetry, no live server. image-to-code/, imagegen-frontend-web/, imagegen-frontend-mobile/, brandkit/ (task 1003328 / ADR 0222) are the image family under the hero-only rule: plates and hero objects only, the pair (JPEG dark plate + WebP alpha cutout) with a <file>.json sidecar beside each, through the instance's art pipeline key; the grove specimen's styles/grove/assets/ is the worked example. styles/ (task 1003323 / ADR 0219) is the style library: <name>/pack.json (the theme.ui overlay, exactly the fifteen) + DESIGN.md (the look's derived recipes, thirteen in lockstep with the pack) + mock.html (the specimen); chrome-world (pinned to the neutral pack), expedition, grove; STYLE=<name> on the kit lays a look over the instance's pack through GDS_BRANDING_FILE; tests/ui_design_styles.mjs runs the world/cosmos/hall token suites once per alternate (UI_DESIGN_PACK). config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
134
134
  │ ├── character-anim/ ← consistent-character generative animation (ADR 0088) — graded painterly frame sets + manifest, the otb-character-review skill. CLAUDE.md, module.json.
135
135
  │ ├── provisioning/ ← stands up + operates a Cloud Bongos instance's own infra (server/DB/domain/DNS/TLS) via the API (ADR 0111); web-tier request+state only, no cloud tokens — a separate control-plane runner drains the intents. Distinct from the `hosting` module (ADR 0092), which brokers compute workloads. CLAUDE.md, module.json.
136
136
  │ └── demo/ ← BV1.R52 acceptance-proof module: one rank-gated write route + one namespaced migration proving the loader mounts a scaffolded module with zero core edits. CLAUDE.md, module.json.
@@ -1363,5 +1363,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1363
1363
  landed since 1.19.461 with no explicit bump. run 33576366095. (task 1002620)
1364
1364
  1.19.463 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1365
1365
  landed since 1.19.462 with no explicit bump. run 33577660347. (task 1002620)
1366
+ 1.19.464 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1367
+ landed since 1.19.463 with no explicit bump. run 33578746178. (task 1002620)
1368
+ 1.19.465 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1369
+ landed since 1.19.464 with no explicit bump. run 33581328823. (task 1002620)
1366
1370
  ---------------------------------------------------------------------------
1367
1371
  ```
@@ -101,6 +101,8 @@ node modules/ui-design/kit/contrast.js --url http://127.0.0.1:3412/projects --gl
101
101
 
102
102
  `check-mock.js` runs the mechanical rules over a self-contained mock before anyone looks at it: the **platform's** (no page-local `:root`, page tokens on `body`, every dark rule written twice byte-identical, no literal colour beyond black/white with alpha, no pack colour pinned, the `?state=` contract, a head comment after `<body>`, one header/main/h1 + a skip link, apex-only sentinels balanced, no `innerHTML`, no motion in JavaScript, transitions ≤ 400ms) and the **instance's taste bans**, read from its `DESIGN.md` "Don't" section (an em/en dash, emoji, gradient text, a coloured glow, a box shadow). The em dash ban is Cloud Bongos's, not the platform's: an instance whose `DESIGN.md` never mentions it is not held to it.
103
103
 
104
+ After those, the **anti-pattern detector** (task 1003329, ADR 0223; `kit/tells.js`, listed rule by rule in [`kit/README.md`](../../modules/ui-design/kit/README.md)): two floors that FAIL (a bounce easing, a card nested in a card) and eighteen craft tells that WARN (the identical card grid, hero metrics, section numbering, a kicker above the h1, a side stripe, a hard offset shadow, a coloured halo, decorative blur, a default typeface, a wide measure, a long headline, the same entrance everywhere, a geometric mask on a plate, glyphs as icons, a rainbow gradient, the browser surfaces nobody themed, proportional numerals, tiny type). A tell is a reviewer's read, never a failure, and a false positive is named where it fires: `<!-- kit-ignore: tell: tiny type (why) -->` prints it as WAIVE with the reason; `--raw` ignores waivers, `--no-tells` skips the detector, `--json` prints the rows for a job to gate on. `render.js` reports the computed-value twins (measure, tiny text, numerals in cells, headline lines, an overshooting curve) as `~ tell:` lines under each render and keeps them in `report.json`.
105
+
104
106
  `contrast.js --pack` measures the instance's own pack — every ink on every ground, light values — with no browser. `--url`/`--file` walks every visible text element in both modes and computes the real WCAG ratio against its *composited* background; `--glyphs` screenshots named elements and reads the glyph pixels against the local ground's median, for text over a moving or textured ground where the stylesheet cannot say what is under it.
105
107
 
106
108
  ## When it goes wrong
@@ -13,3 +13,15 @@ node modules/ui-design/kit/contrast.js --pack [look] | --url <url> [--glyphs sel
13
13
  A **look** is a branding pack from the module's style library (`modules/ui-design/styles/<name>/`, task 1003323): `STYLE=<name>` lays it over the instance's own pack through `GDS_BRANDING_FILE`, so any surface renders under any look with no page edit.
14
14
 
15
15
  A surface describes itself in two JSON files beside it — `<page>.states.json` and `<page>.probes.json` — in the vocabularies `lib.js` defines and validates. The full loop, the file shapes and the failure table: [`docs/recipes/ui-look-before-you-ship.md`](../../../docs/recipes/ui-look-before-you-ship.md). Exit codes everywhere: 0 clean · 1 problems · 2 a broken file or usage · 3 no browser.
16
+
17
+ ## The anti-pattern detector (`tells.js`, and the rendered twins in `lib.js`)
18
+
19
+ The [detector] group of impeccable's rebuild spec, landed as rules in the kit that already exists ([task 1003329](https://cloudbongos.com/builders#/task/1003329), [ADR 0223](../../../docs/adr/0223-the-detector-is-two-tiers-in-the-kit.md)): no second tool, no browser bundle, no hook, no model, no key. `check-mock.js` runs the static rules over a mock's source; `render.js` reports the computed-value twins per state, width and mode. Two tiers, kept apart on purpose, beside the platform rules and the instance's taste bans:
20
+
21
+ - **Floors** (`FAIL`, mechanical, every instance): **bounce easing** (a `cubic-bezier` whose y control points leave 0..1; nothing on the platform overshoots and the switch has no spring) · **a card nested in a card** (a `card` class inside another, from a tag-stack walk of the markup).
22
+ - **Tells** (`WARN`; a clean scan is a floor, never proof, and a reviewer verifies each in context): **identical icon-heading-text card grid** (three or more adjacent siblings with the same icon, heading, paragraph shape) · **the hero-metric template** (three or more statistics in the first section) · **decorative section numbering** (`01`, `02` as labels) · **a kicker or eyebrow label directly above the h1** (a `FAIL` where the instance's `DESIGN.md` bans the eyebrow, as the chrome world does) · **a coloured side stripe** (a `border-left` wider than a hairline) · **a hard zero-blur offset shadow** · **a zero-offset coloured halo** as elevation · **decorative glass and blur** (`backdrop-filter` on more than two surfaces) · **a default typeface as the voice** (Inter, Roboto, Arial, Helvetica, system-ui and their kin named in a `font-family`, where the pack's two stacks are the voice) · **a prose measure wider than 80ch** · **a headline over 90 characters** · **the same entrance animation on three or more rules** · **a geometric mask on a plate** (`clip-path: circle | ellipse | polygon` on an `img` or plate rule) · **unicode glyphs standing in for an icon set** (three or more arrows, stars, ticks in copy) · **a rainbow or mesh gradient** (a conic gradient, or four or more coloured stops; a black, white or transparent scrim is not one) · **the browser surfaces nobody themed** (`::selection`, `caret-color` beside a field, `text-underline-offset` beside an underline) · **proportional numerals in data** (four-digit figures with no `tabular-nums`) · **tiny type** (a `font-size` under 11px).
23
+ - **Rendered tells** (`render.js`, from computed values, printed as `~ tell:` lines and kept in `report.json` under each render, never counted as problems): **prose-measure** (a paragraph wider than about 95 characters per line) · **tiny-type** (visible text under 11px) · **proportional-numerals** (a cell with a figure and no `tabular-nums`) · **headline-lines** (an h1 wrapping past three lines) · **bounce-easing** (an overshooting curve in a computed timing function).
24
+
25
+ **Not ported, on purpose:** the purple-to-blue gradient tell (a literal colour is already a platform failure; under the fifteen-token contract a gradient's hue is unresolvable from source), a browser-injected overlay (the rendered twins run inside the kit's own page evaluation), a per-project ignore config (the waiver below is per file, which is where a false positive lives), and any run on native app code (every surface here is a served page).
26
+
27
+ **Waivers and switches.** A false positive is named, not silenced: `<!-- kit-ignore: tell: tiny type (the mono label is 10px by the world's own rule) -->` anywhere in the file waives that rule for the file and prints it as `WAIVE` with the reason (a key matches the rule by prefix, so `kit-ignore: tell: a kicker` reads like the rule). `--raw` runs with no waivers; `--no-tells` runs the platform rules and taste bans only; `--json` prints the rows as `{ status, rule, detail }` with `fails` and `warns` counts for a job that gates on them. Every rule is pinned in `tests/ui_design_kit.mjs` with a fixture that trips it and a clean fixture that passes all of them.
@@ -22,6 +22,7 @@
22
22
  const fs = require('node:fs');
23
23
  const path = require('node:path');
24
24
  const lib = require('./lib.js');
25
+ const tells = require('./tells.js');
25
26
 
26
27
  // The taste bans an instance's DESIGN.md may switch on, by the words its "Don't"
27
28
  // section uses. Pure: text in, a Set of ban keys out.
@@ -31,6 +32,7 @@ const TASTE_BANS = Object.freeze({
31
32
  'gradient-text': /gradient text/i,
32
33
  'text-glow': /\bglow\b/i,
33
34
  'box-shadow': /box[- ]shadow|\bshadow\b/i,
35
+ 'eyebrow': /\b(eyebrow|kicker)\b/i,
34
36
  });
35
37
  function readDesignBans(mdText) {
36
38
  const bans = new Set();
@@ -66,7 +68,7 @@ const BLACK_OR_WHITE = /^(000|000000|fff|ffffff|0{8}|f{8}|000000[0-9a-f]{2}|ffff
66
68
  // Pure: the rules over a mock's source. `packColors` = the instance's fifteen
67
69
  // (name → value) so a literal equal to a pack colour is caught; `bans` = the
68
70
  // instance's taste bans; `states`/`keys` = names the mock must carry.
69
- function checkMock(html, { bans = new Set(), packColors = {}, states = [], keys = [] } = {}) {
71
+ function checkMock(html, { bans = new Set(), packColors = {}, states = [], keys = [], waivers = new Map(), noTells = false } = {}) {
70
72
  const raw = String(html).replace(/\r\n/g, '\n');
71
73
  const results = [];
72
74
  const ok = (rule, pass, detail) => results.push([pass ? 'PASS' : 'FAIL', rule, detail || '']);
@@ -123,13 +125,17 @@ function checkMock(html, { bans = new Set(), packColors = {}, states = [], keys
123
125
  ok('no innerHTML', !/innerHTML/.test(scripts));
124
126
  const durs = [...css.matchAll(/transition\s*:[^;]*?(\d+)ms/g)].map((x) => +x[1]).filter((d) => d > 400);
125
127
  ok('state transitions at most 400ms', durs.length === 0, durs.join(','));
128
+
129
+ // ── the anti-pattern detector (task 1003329, ADR 0223): floors FAIL, tells WARN, a
130
+ // per-file `kit-ignore:` waiver WAIVEs with its reason; --raw runs with none ──
131
+ if (!noTells) results.push(...tells.scanTells(raw, { bans, waivers }));
126
132
  return results;
127
133
  }
128
134
 
129
135
  function main() {
130
136
  const args = process.argv.slice(2);
131
137
  const file = lib.argOf(args, '--file', null);
132
- if (!file || lib.hasFlag(args, '--help')) { process.stderr.write('usage: node modules/ui-design/kit/check-mock.js --file <mock.html> [--states a,b] [--keys A,B] [--design <DESIGN.md>] [--no-taste] [--pack <style>]\n'); process.exit(lib.EXIT.USAGE); }
138
+ if (!file || lib.hasFlag(args, '--help')) { process.stderr.write('usage: node modules/ui-design/kit/check-mock.js --file <mock.html> [--states a,b] [--keys A,B] [--design <DESIGN.md>] [--no-taste] [--no-tells] [--raw] [--json] [--pack <style>]\n'); process.exit(lib.EXIT.USAGE); }
133
139
  if (!fs.existsSync(file)) { process.stderr.write(`check-mock: not found: ${file}\n`); process.exit(lib.EXIT.USAGE); }
134
140
  const styleArg = lib.argOf(args, '--pack', null);
135
141
  const style = styleArg ? lib.resolveStyle(styleArg) : null;
@@ -143,10 +149,17 @@ function main() {
143
149
  let packColors = {};
144
150
  if (style) packColors = lib.packColors(style.pack);
145
151
  else { try { packColors = lib.packColors(lib.instancePack()); } catch { packColors = {}; } }
146
- const results = checkMock(fs.readFileSync(file, 'utf8'), { bans, packColors, states: lib.listArg(args, '--states', []), keys: lib.listArg(args, '--keys', []) });
147
- for (const [s, rule, detail] of results) process.stdout.write(`${s.padEnd(4)} ${rule}${detail ? ' (' + detail + ')' : ''}\n`);
152
+ const source = fs.readFileSync(file, 'utf8');
153
+ const waivers = lib.hasFlag(args, '--raw') ? new Map() : tells.parseWaivers(source);
154
+ const results = checkMock(source, { bans, packColors, states: lib.listArg(args, '--states', []), keys: lib.listArg(args, '--keys', []), waivers, noTells: lib.hasFlag(args, '--no-tells') });
148
155
  const fails = results.filter((r) => r[0] === 'FAIL').length;
149
- process.stdout.write(`\n${fails ? `${fails} RULE(S) FAIL` : 'ALL RULES PASS'} (taste bans: ${[...bans].join(', ') || 'none declared'})\n`);
156
+ const warns = results.filter((r) => r[0] === 'WARN').length;
157
+ if (lib.hasFlag(args, '--json')) {
158
+ process.stdout.write(JSON.stringify({ file, fails, warns, tasteBans: [...bans], results: results.map(([status, rule, detail]) => ({ status, rule, detail })) }, null, 1) + '\n');
159
+ } else {
160
+ for (const [s, rule, detail] of results) process.stdout.write(`${s.padEnd(4)} ${rule}${detail ? ' (' + detail + ')' : ''}\n`);
161
+ process.stdout.write(`\n${fails ? `${fails} RULE(S) FAIL` : 'ALL RULES PASS'}${warns ? ` · ${warns} tell(s) to verify in context` : ''} (taste bans: ${[...bans].join(', ') || 'none declared'})\n`);
162
+ }
150
163
  process.exit(fails ? lib.EXIT.PROBLEMS : lib.EXIT.CLEAN);
151
164
  }
152
165
 
@@ -188,6 +188,23 @@ function factsInPage(spec) {
188
188
  if (e.value) out.value = e.value.map(([s]) => { const el = q(s); return el ? el.value : null; });
189
189
  if (e.hasClass) out.hasClass = e.hasClass.map(([s, c]) => { const el = q(s); return el ? el.classList.contains(c) : null; });
190
190
  if (e.top) out.top = e.top.map(([s]) => { const el = q(s); return el ? Math.round(el.getBoundingClientRect().top) : null; });
191
+ // The craft tells, from COMPUTED values (task 1003329): the rendered twins of tells.js.
192
+ // Reported as warnings by render.js, never as floor problems — a reviewer verifies each.
193
+ const px = (el) => parseFloat(getComputedStyle(el).fontSize) || 16;
194
+ const wide = [], tiny = [], prop = [], tells = [];
195
+ qa('p, li, blockquote, dd').filter(shown).forEach((el) => { if ((el.textContent || '').trim().length < 140 || /grid|flex|table/.test(getComputedStyle(el).display)) return; const cpl = el.clientWidth / (px(el) * 0.5); if (cpl > 95) wide.push(`${describe(el)} ~${Math.round(cpl)}ch`); });
196
+ qa('p, li, a, button, label, td, th, small, span, dt, dd').filter(shown).forEach((el) => { if ((el.textContent || '').trim().length < 2) return; const s = px(el); if (s < 11) tiny.push(`${describe(el)} ${s}px`); });
197
+ qa('td, th, dd').filter(shown).forEach((el) => { const t = (el.textContent || '').trim(); if (el.children.length || !/\d{3,}/.test(t)) return; if (!/tabular-nums/.test(getComputedStyle(el).fontVariantNumeric)) prop.push(`${describe(el)} "${t.slice(0, 16)}"`); });
198
+ const h1 = qa('h1').find(shown);
199
+ const hLines = h1 ? Math.round(h1.getBoundingClientRect().height / (parseFloat(getComputedStyle(h1).lineHeight) || px(h1) * 1.2)) : 0;
200
+ const curves = qa('[class]').filter(shown).slice(0, 400).flatMap((el) => { const cs = getComputedStyle(el); return ((cs.transitionTimingFunction || '') + ' ' + (cs.animationTimingFunction || '')).match(/cubic-bezier\([^)]*\)/g) || []; });
201
+ const overshoot = [...new Set(curves)].filter((b) => { const n = (b.match(/-?[\d.]+/g) || []).map(Number); return n[1] < 0 || n[1] > 1 || n[3] < 0 || n[3] > 1; });
202
+ if (wide.length) tells.push(`prose-measure: ${wide.slice(0, 3).join(' | ')}`);
203
+ if (tiny.length) tells.push(`tiny-type: ${tiny.slice(0, 3).join(' | ')}`);
204
+ if (prop.length) tells.push(`proportional-numerals: ${prop.slice(0, 3).join(' | ')}`);
205
+ if (hLines > 3) tells.push(`headline-lines: the h1 wraps to ${hLines} lines`);
206
+ if (overshoot.length) tells.push(`bounce-easing: ${overshoot.join(' | ')}`);
207
+ out.tells = tells;
191
208
  return out;
192
209
  }
193
210
  // Pure: compare collected facts against an expectation. Returns problem strings.
@@ -234,6 +251,10 @@ function reducedMotionProblems({ running = [], frames = 0 } = {}) {
234
251
  if (frames > 2) p.push(`reduced-motion: a requestAnimationFrame loop (${frames} frames in 800ms)`);
235
252
  return p;
236
253
  }
254
+ // Pure: the craft tells a render surfaced (the computed-value twins of tells.js), as warning
255
+ // lines the renderer prints and records — never floor problems, because a clean scan is a
256
+ // floor and not proof, and a reviewer verifies each tell in context (task 1003329, ADR 0223).
257
+ function craftTells(facts) { return (facts && facts.tells ? facts.tells : []).map((t) => `tell: ${t}`); }
237
258
 
238
259
  // ── the states file ───────────────────────────────────────────────────────────
239
260
  // { "page": "/projects", "surface"?: "public-landing" (a module key), "stub"?: { prefix?, child? },
@@ -429,7 +450,7 @@ function packColors(pack) {
429
450
  module.exports = {
430
451
  REPO_ROOT, KIT_DIR, STYLES_DIR, EXIT, FLOORS, SIZES, MODES, CHANNELS, SETTLE_MS, ACTIONS, EXPECT_KEYS,
431
452
  argOf, hasFlag, listArg, pageName, normalizePageArg, resolveSurfaceDir, sidecarPath, readJson,
432
- validateActions, runActions, validateExpect, factsInPage, expectProblems, auditFloors, reducedMotionProblems,
453
+ validateActions, runActions, validateExpect, factsInPage, expectProblems, auditFloors, reducedMotionProblems, craftTells,
433
454
  validateStatesFile, validateProbesFile, NoBrowserError, launchBrowser, attachCollectors, withMode,
434
455
  freePort, startStub, parseColor, composite, luminance, contrastRatio, instancePack, packColors,
435
456
  listStyles, resolveStyle, styleBrandingFile,