@bongos/core 1.19.459 → 1.19.461

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 (34) hide show
  1. package/.bongos-core.json +65 -35
  2. package/.claude/skills/design/SKILL.md +10 -3
  3. package/docs/adr/0220-an-in-house-rebuilt-skill-is-a-first-party-skill.md +46 -0
  4. package/docs/adr/README.md +1 -0
  5. package/docs/copy-inventory.md +52 -61
  6. package/docs/copy-registry.json +92 -203
  7. package/docs/file-map.md +1 -1
  8. package/docs/module-api-changelog.md +4 -0
  9. package/modules/hall-ui/public/collab-lib.js +40 -4
  10. package/modules/hall-ui/public/collab.css +18 -34
  11. package/modules/hall-ui/public/collab.html +17 -17
  12. package/modules/hall-ui/public/collab.js +75 -39
  13. package/modules/hall-ui/public/collab.states.json +2 -2
  14. package/modules/hall-ui/public/copy-desk.css +64 -109
  15. package/modules/hall-ui/public/copy-desk.html +76 -69
  16. package/modules/hall-ui/public/copy-desk.js +175 -80
  17. package/modules/hall-ui/public/copy-desk.states.json +59 -2
  18. package/modules/hall-ui/public/oversight.css +11 -2
  19. package/modules/lifecycle/help-requests.js +18 -8
  20. package/modules/ui-design/module.json +1 -1
  21. package/modules/ui-design/skills/PREAMBLE.md +3 -3
  22. package/modules/ui-design/skills/README.md +13 -0
  23. package/modules/ui-design/skills/design-taste-frontend/SKILL.md +136 -0
  24. package/modules/ui-design/skills/design-taste-frontend-v1/SKILL.md +125 -0
  25. package/modules/ui-design/skills/high-end-visual-design/SKILL.md +128 -0
  26. package/modules/ui-design/skills/redesign-existing-projects/SKILL.md +120 -0
  27. package/package-lock.json +2 -2
  28. package/package.json +1 -1
  29. package/scripts/gds/docs-entropy.js +5 -1
  30. package/src/module-api.js +1 -1
  31. package/tests/collab_page.mjs +13 -7
  32. package/tests/docs_entropy.mjs +29 -1
  33. package/tests/hall_collab_world.mjs +214 -0
  34. package/tests/ui_design_skills.mjs +99 -9
@@ -0,0 +1,128 @@
1
+ ---
2
+ name: high-end-visual-design
3
+ description: Makes an interface read as an expensive agency build, inside the instance's world. One texture archetype and one layout archetype per project (never the same pair twice), the container anatomy (a shell and a core with a hairline ring and concentric radii, where the world allows a container at all), the pill button with its nested arrow disc, the spatial rhythm, the motion choreography inside the Kill Switch, the hover physics, the scroll entry, the mobile collapse per archetype and the performance rules, then a pre-output checklist. Triggers on "make it feel premium", "agency-grade", "this looks cheap", "expensive", "high-end", "the polish pass". Rebuilt in-house from the taste-skill rebuild spec (task 1003325, ADR 0198); the fonts are the pack's, the palette is the world's, images are hero plates only.
4
+ ---
5
+
6
+ <!-- BEGIN PLATFORM PREAMBLE · ui-design module · one block, byte-identical in every skill the module ships (vendored or rebuilt in-house) so an upstream refresh is a three-way merge; the source is modules/ui-design/skills/PREAMBLE.md and tests/ui_design_skills.mjs fails on drift -->
7
+ > **Platform preamble — read before any rule below.** This skill ships with the `ui-design` module (vendored from a scanned origin, or rebuilt in-house from a spec) and runs INSIDE an instance's world, which it reads first and never overrides.
8
+ >
9
+ > 1. **Load the world before any taste rule.** `config/branding.json` over `config/branding.neutral.json` (`theme.ui` is the fifteen tokens: <redacted> colours and two font stacks), `config/design-tokens.json` over `config/design-tokens.neutral.json`, and `DESIGN.md` at the repo root: the instance's palette and derived tiers, faces, materials, motion grammar, components, and its Do's and Don'ts. Where an instance has no `DESIGN.md`, the neutral pack is the world and the platform floors in item 6 are the whole rulebook; say so and build to them. Never invent a palette.
10
+ > 2. **The fifteen-token contract is the only colour source.** Every colour is a `var()` or a `color-mix()` of the fifteen; the only legal literal is pure black or white with alpha as a scrim, mask or halo. No page `:root` (page tokens live on `body`), and every dark rule is written twice: `:root[data-mode="dark"]` and its `prefers-color-scheme` twin.
11
+ > 3. **A style skill produces variants INSIDE that world, never a second world.** The look this skill carries (its palette, faces, materials, mood boards) is a reference for composition and craft; the instance's tokens and faces replace it. Changing the world is the owner's decision and an ADR, not a session's taste.
12
+ > 4. **Images are for hero plates and hero objects only.** UI is built from templates and the instance's world, not painted; any image-generation step below applies to text-free plates and hero objects, never to controls, cards, text, or a screenshot of a screen.
13
+ > 5. **The instance's taste bans apply and win.** Read them from its `DESIGN.md`. Cloud Bongos's own (no em dash or en dash in visible copy, no emoji, no gradient text, no three-equal-card row, no fake screenshot built from divs) are that instance's, not the module's; where this skill's rules conflict with an instance's bans, the instance wins.
14
+ > 6. **The platform floors hold on every instance** and a `DESIGN.md` may tighten them, never loosen them: every control clears 24px on both sides (primary pills 46–48px); AA in both modes (4.5:1 body text, 3:1 large text and non-text) measured from the real stylesheet; and the Kill Switch, `@media (prefers-reduced-motion: reduce)` at the foot of the token layer killing every animation and transition, no motion authored in JavaScript, and a rest frame that is a complete composition on its own.
15
+ > 7. **Look before you ship.** Render the real surface through the module's kit before and after (`/design` Step 2 and `docs/recipes/ui-look-before-you-ship.md`: every state at 1440 / 390 / 320 in dark and light, audited against the floors); a page with no states file gets one first, and new interactive code ships with a pin in the page's own test file.
16
+ >
17
+ > The rules below are the skill's own. A vendored copy carries a `PROVENANCE.md` beside this file naming the origin, the pinned commit, the licence and the scan verdict it was vendored under; an in-house rebuild names its rebuild spec and its ADR in the section right after this block.
18
+ <!-- END PLATFORM PREAMBLE -->
19
+
20
+ ## Rebuilt from
21
+
22
+ This skill is an **in-house rebuild**, not a vendored copy. Its documented functionality comes from `Leonxlnx/taste-skill` @ `<redacted>` (MIT; the `soft-skill` entry, "high-end-visual-design"), which `/scan-before-install` reduced to `dangerous` at the default tier on 2026-08-28, so under [ADR 0198](../../../../docs/adr/<redacted>.md) it could land only as a REBUILD-SPEC. The spec is the body of [task 1003325](https://cloudbongos.com/builders#/task/1003325); this file implements its requirements from scratch, **no upstream bytes consulted or copied**; the landing shape is [ADR 0220](../../../../docs/adr/<redacted>.md).
23
+
24
+ What changed in translation, on purpose: the upstream skill specified **the** fonts and shadows that make a site feel expensive; here the fonts are the pack's two stacks and a shadow exists only where the instance's world has one (the chrome world has exactly one, and it belongs to the sculpture). Its texture archetypes become the looks in the style library; its layout archetypes stay free. Its motion (a hamburger morph, a blurred full-screen menu, an 800ms blurred fade-up on scroll) is kept as CSS inside the Kill Switch, and the scroll entry is honest about what a static page can do. Imagery follows the owner's rule: hero plates and hero objects by token, never a step.
25
+
26
+ ## One texture archetype, one layout archetype, per project
27
+
28
+ Pick **one of each** before anything is drawn, write the pair in the sheet's header comment, and record it in the ship notes. The same pair is never used twice across sibling surfaces of one instance: the second surface that needs "premium" takes a different layout archetype under the same texture, because the texture is the instance's and the layout is yours.
29
+
30
+ **Texture archetypes** map to the style library, `modules/ui-design/styles/`, and to the world; the pack in force fixes which one you are in:
31
+
32
+ | Archetype | In this library | What carries it |
33
+ |---|---|---|
34
+ | Dark glass tech | `chrome-world` (the neutral pack) | the void, the glass tokens, one rendered chrome plate, a black type halo |
35
+ | Warm editorial luxury | `expedition` | warm paper, a halftone screen, hairline frames, heavy uppercase display, one lamp yellow as a fill |
36
+ | Soft light structuralism | `grove` | pale sage, soft corners on what holds content, tinted shadows, a sea slab under a band |
37
+
38
+ A page never switches texture. To see a surface under another texture, render it with `--style <name>` on the kit; adopting one is the owner's `config/branding.json`, not a page.
39
+
40
+ **Layout archetypes** (the free choice):
41
+
42
+ - **The asymmetric tile grid**: interlocking spans, zero empty cells, one tile that breaks the container by a gutter.
43
+ - **The depth cascade**: bands that overlap by a step (a negative top margin of `spacing.lg`), the object (a plate, by token) crossing two of them.
44
+ - **The editorial split**: a hard vertical split at the golden-ish 5:7, type on the narrow side, the plate or the air on the wide side, a hairline down the seam.
45
+
46
+ ## The bans
47
+
48
+ Defaults an instance's `DESIGN.md` may only tighten:
49
+
50
+ - **The default sans stack** as a display face, unless it is the pack's face (then it is the face; the pack wins).
51
+ - **Thick default icon sets.** One hairline stroke width, the world's inline mark, or no icon.
52
+ - **Plain grey hairline borders** as decoration. A hairline is structure (`rule` / `rule-soft`, tokenised); it separates, it does not box.
53
+ - **Harsh dark drop shadows.** Where the world has a shadow at all, it is tinted with the ink (`color-mix(in srgb, var(--ink) 20%, transparent)`) and it belongs to an object; the chrome world's UI has none.
54
+ - **Edge-glued navbars.** The bar keeps the world's gutter on both sides and one hairline under it.
55
+ - **Symmetric three-column grids** (the three-equal-card row the chrome world bans by name).
56
+ - **Default easing** (`ease`, `linear`, `ease-in-out` on an entrance). The world's curves, or none.
57
+
58
+ ## The container anatomy, where the world allows a container
59
+
60
+ Where a surface carries a panel (a dialog, the poster card, a settings sheet), a major container is **two concentric shapes**: an outer shell and an inner core.
61
+
62
+ - The shell: the ground one step above the page (`bg-card`), the world's panel radius (`rounded.panel`), a **hairline ring** in `glass-edge` (dark) / `rule-soft` (light).
63
+ - The core: inset by one spacing step (`spacing.sm`), an **inner highlight** as a 1px inset pure-white line at low alpha in dark (the one legal literal), and an **inner radius mathematically concentric**: `calc(var(--radius-panel) - var(--space-sm))`, never the same radius as the shell.
64
+ - The chrome world has no card vocabulary below the fold (hairlines only); there the anatomy applies to the one thing that is allowed to be a panel, and the sheet says so in a comment. A world that says "no containers" is not overruled by this section.
65
+
66
+ ## The button anatomy
67
+
68
+ A primary call to action is a **pill** (999px) at 46 to 48px, the accent as the fill and the world's `accent-on` as the ink, and its trailing arrow sits **inside its own circular wrapper**, flush with the pill's inner padding, never a bare glyph beside the label:
69
+
70
+ ```
71
+ [ Open the hall (→) ] the disc: accent-on at 12% over the fill (color-mix), 28px, the glyph centred
72
+ ```
73
+
74
+ The secondary action is the same pill, transparent at rest, glass on hover, no disc. Both clear the 24px floor by construction. A world that joins the two into one bar (the chrome world's door pill) is that world's signature; keep it.
75
+
76
+ ## Spatial rhythm
77
+
78
+ - **Section padding roughly doubles** the body's rhythm: the world's band clamp is the floor and twice it is the ceiling (`clamp(58px, 8.4vh, 104px)` doubled to `clamp(116px, 16.8vh, 208px)` on a hero-adjacent band in the chrome world); heavy breathing room between chapters, tight rhythm inside one.
79
+ - **Tiny uppercase eyebrows** above major headings, in the world's label face, as a hairline pill or bare: at most one per three sections, and never above the h1 where the world bans it (the chrome world does).
80
+ - The container is `spacing.container`; the gutter is the world's; nothing scrolls sideways at any width.
81
+
82
+ ## Motion choreography, inside the Kill Switch
83
+
84
+ All CSS, on the world's curves; nothing moves in script; everything dead under `prefers-reduced-motion`, where the rest frame is the complete composition.
85
+
86
+ - **Easing that simulates mass**: the world's entrance curve (the chrome world's `cubic-bezier(.16, 1, .3, 1)`) for arrivals, the state curve for hover and press. Where a world names none, an exponential ease-out.
87
+ - **The hamburger that morphs into a close mark**: three bars as three pseudo-elements; the `aria-expanded` toggle (state, in script) flips a class; the bars rotate and collapse in CSS over 300ms. Under the switch they swap instantly.
88
+ - **The full-screen blurred menu**: a fixed layer with `backdrop-filter` (blur is allowed here because the layer is fixed), links revealed in a **staggered slide-up** via `animation-delay` from `--i` in the markup. Under the switch the menu is simply open and complete. On the chrome world's apex the menu is a hairline band, not a full-screen layer; the world's component wins.
89
+
90
+ ## Hover physics
91
+
92
+ Internal kinetic tension instead of a colour swap: on `:hover` the button holds; on `:active` the button **scales down** (`scale(.98)`) while the nested arrow disc **translates diagonally and scales up** (`translate(2px, -2px) scale(1.1)`), both on `transform` only, over 200 to 400ms on the state curve. Focus restates the world's ring. Nothing here needs the pointer's position, so nothing here needs script.
93
+
94
+ ## Scroll entry
95
+
96
+ Elements arrive with a **slow blurred fade-up over roughly 800ms**: `opacity: 0; translate: 0 16px; filter: blur(6px)` to rest, on the entrance curve.
97
+
98
+ - Prefer **CSS scroll-driven animation** (`animation-timeline: view()`) behind `@supports`, which needs no script.
99
+ - Where that is unsupported, an `IntersectionObserver` may add **one class**; the motion stays in CSS. The entry state is applied only once the observer is live (a `.js` class on `body`), so a page without script, or with the observer not yet run, shows the composed rest frame and never a hidden element.
100
+ - Never a raw scroll listener, never a continuous value in script.
101
+ - Under the switch there is no entry: every element is at rest.
102
+
103
+ ## Mobile collapse, per archetype
104
+
105
+ Below the world's breakpoint (900px in the chrome world; the tablet breakpoint elsewhere):
106
+
107
+ - **Asymmetric tile grid**: spans reset, one column, full width, tiles in reading order.
108
+ - **Depth cascade**: overlaps and negative margins removed; bands stack with the hairline between them; the plate becomes a masthead above the words.
109
+ - **Editorial split**: the seam becomes a hairline between two stacked bands; the narrow side first.
110
+ - Rotations, offsets and overlaps go; a full-height section is `100dvh` or the world's floored stage, never `100vh`; the bar holds one row down to 320px.
111
+
112
+ ## Performance rules
113
+
114
+ - Animate **`transform` and `opacity` only** (and `filter` on a plate where the world does).
115
+ - **Backdrop blur only on fixed or sticky layers**; light glass over paper is a tint at blur 0, as the world's glass token says.
116
+ - **Noise and grain** only where the world has that material, on a fixed, `pointer-events: none` pseudo-element.
117
+ - **`z-index` reserved for systemic layers** on a named scale on `body`; no arbitrary values.
118
+ - The LCP element is the plate or the headline; fonts have a sized fallback; no layout property animates.
119
+
120
+ ## Pre-output checklist
121
+
122
+ - [ ] The **archetype pair** is named in the sheet header and the ship notes, and it is not the pair a sibling surface already used.
123
+ - [ ] **Nested shells** on every major container the world allows one on; concentric inner radius; hairline ring; inner highlight in dark.
124
+ - [ ] **Section padding** at or above the world's band clamp; eyebrows rationed; the h1 has none where banned.
125
+ - [ ] **The world's easing** on every transition; no default curve.
126
+ - [ ] **Scroll entry** honest: CSS timeline or one observer class; rest frame complete without script and under the switch.
127
+ - [ ] **Mobile collapse** per archetype, looked at in the 390 and 320 shots.
128
+ - [ ] **The premium impression** judged on the kit's shots (`node modules/ui-design/kit/render.js --page <path>`, ALL CLEAN in every mode the page serves), not on the source; the button's disc and the ring measured, not eyeballed.
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: redesign-existing-projects
3
+ description: Upgrades an existing site or app in place, in whatever framework and styling method it already uses, without breaking it. A fixed three-step sequence (scan the stack, diagnose against a written audit, apply targeted fixes), the seven-group audit (typography, colour and surface, layout, interaction, content, code and omissions) with the instance's world as the answer key, a catalogue of upgrade techniques marked CSS-only or not available, and a fixed impact-per-risk fix order. Triggers on "redesign this", "upgrade the existing page", "make the current X premium", "audit this UI", "this page looks generic, fix it". Rebuilt in-house from the taste-skill rebuild spec (task 1003325, ADR 0198); keeps the stack, never migrates frameworks, images are hero plates only.
4
+ ---
5
+
6
+ <!-- BEGIN PLATFORM PREAMBLE · ui-design module · one block, byte-identical in every skill the module ships (vendored or rebuilt in-house) so an upstream refresh is a three-way merge; the source is modules/ui-design/skills/PREAMBLE.md and tests/ui_design_skills.mjs fails on drift -->
7
+ > **Platform preamble — read before any rule below.** This skill ships with the `ui-design` module (vendored from a scanned origin, or rebuilt in-house from a spec) and runs INSIDE an instance's world, which it reads first and never overrides.
8
+ >
9
+ > 1. **Load the world before any taste rule.** `config/branding.json` over `config/branding.neutral.json` (`theme.ui` is the fifteen tokens: <redacted> colours and two font stacks), `config/design-tokens.json` over `config/design-tokens.neutral.json`, and `DESIGN.md` at the repo root: the instance's palette and derived tiers, faces, materials, motion grammar, components, and its Do's and Don'ts. Where an instance has no `DESIGN.md`, the neutral pack is the world and the platform floors in item 6 are the whole rulebook; say so and build to them. Never invent a palette.
10
+ > 2. **The fifteen-token contract is the only colour source.** Every colour is a `var()` or a `color-mix()` of the fifteen; the only legal literal is pure black or white with alpha as a scrim, mask or halo. No page `:root` (page tokens live on `body`), and every dark rule is written twice: `:root[data-mode="dark"]` and its `prefers-color-scheme` twin.
11
+ > 3. **A style skill produces variants INSIDE that world, never a second world.** The look this skill carries (its palette, faces, materials, mood boards) is a reference for composition and craft; the instance's tokens and faces replace it. Changing the world is the owner's decision and an ADR, not a session's taste.
12
+ > 4. **Images are for hero plates and hero objects only.** UI is built from templates and the instance's world, not painted; any image-generation step below applies to text-free plates and hero objects, never to controls, cards, text, or a screenshot of a screen.
13
+ > 5. **The instance's taste bans apply and win.** Read them from its `DESIGN.md`. Cloud Bongos's own (no em dash or en dash in visible copy, no emoji, no gradient text, no three-equal-card row, no fake screenshot built from divs) are that instance's, not the module's; where this skill's rules conflict with an instance's bans, the instance wins.
14
+ > 6. **The platform floors hold on every instance** and a `DESIGN.md` may tighten them, never loosen them: every control clears 24px on both sides (primary pills 46–48px); AA in both modes (4.5:1 body text, 3:1 large text and non-text) measured from the real stylesheet; and the Kill Switch, `@media (prefers-reduced-motion: reduce)` at the foot of the token layer killing every animation and transition, no motion authored in JavaScript, and a rest frame that is a complete composition on its own.
15
+ > 7. **Look before you ship.** Render the real surface through the module's kit before and after (`/design` Step 2 and `docs/recipes/ui-look-before-you-ship.md`: every state at 1440 / 390 / 320 in dark and light, audited against the floors); a page with no states file gets one first, and new interactive code ships with a pin in the page's own test file.
16
+ >
17
+ > The rules below are the skill's own. A vendored copy carries a `PROVENANCE.md` beside this file naming the origin, the pinned commit, the licence and the scan verdict it was vendored under; an in-house rebuild names its rebuild spec and its ADR in the section right after this block.
18
+ <!-- END PLATFORM PREAMBLE -->
19
+
20
+ ## Rebuilt from
21
+
22
+ This skill is an **in-house rebuild**, not a vendored copy. Its documented functionality comes from `Leonxlnx/taste-skill` @ `<redacted>` (MIT; the `redesign-skill` entry), which `/scan-before-install` reduced to `dangerous` at the default tier on 2026-08-28, so under [ADR 0198](../../../../docs/adr/<redacted>.md) it could land only as a REBUILD-SPEC. The spec is the body of [task 1003325](https://cloudbongos.com/builders#/task/1003325); this file implements its requirements from scratch, **no upstream bytes consulted or copied**; the landing shape is [ADR 0220](../../../../docs/adr/<redacted>.md).
23
+
24
+ What changed in translation, on purpose: the upstream audit carried its own opinions (pure black backgrounds are a defect, flat sections are unfinished); here **the instance's world is the answer key**, so an audit row is a defect only where the world does not call for it (the chrome world's dark ground is pure black by the owner's decision, and its bands are flat by design). The upgrade catalogue marks each technique **CSS-only or not available** on a platform where nothing moves in script. Imagery follows the owner's rule.
25
+
26
+ ## The three steps, in order
27
+
28
+ 1. **Scan.** Identify the framework and the styling method from the tree, not from memory: the instance's `package.json`, the directory's nested `CLAUDE.md`, the sheet the page loads, `config/design-sources.json` (which tool authored the surface). On a platform surface that is static HTML, one sheet and the instance's transforms. Then load the world (the preamble's order) and the page's states file. Render **before** with the kit and keep the shots: `node modules/ui-design/kit/render.js --page <path> --out <dir>/before`.
29
+ 2. **Diagnose** against the written audit below. Every row is a yes or a no with a line reference; the rows that are "yes" and not called for by the world are the work. Write the list in the ship notes before touching anything.
30
+ 3. **Fix**, in the impact-per-risk order below, one group per commit, and render **after** with the same states file. The diff names which audit rows it closed.
31
+
32
+ ## The audit (the world is the answer key)
33
+
34
+ ### Typography
35
+
36
+ - Default faces where the pack names others: the two stacks are the faces.
37
+ - Weak headline presence: display size, weight and tracking are the world's (the chrome world: 300, `-0.042em`, two lines).
38
+ - Over-wide measure: prose above 65ch.
39
+ - Only two weights in use: the world's hierarchy has display, body and a term weight; a page that uses two is flat.
40
+ - Proportional numerals in data: every real number is `tabular-nums`.
41
+ - Missing tracking adjustments on display sizes; orphaned last words on headlines (`text-wrap: balance` on headings, `pretty` on prose).
42
+
43
+ ### Colour and surface
44
+
45
+ - **Pure black backgrounds** where the world does not call for one (the chrome world does; a page inside it is not in violation).
46
+ - Oversaturated or multiple accents: the accent is the pack's `accent`, once; a second voice is `sea`.
47
+ - Mixed warm and cool greys: only the pack's neutral ramp.
48
+ - The purple-blue generated gradient signature; gradient text; a neon glow: banned on every pack.
49
+ - Untinted shadows: a shadow, where the world has one, is tinted with `ink`.
50
+ - Zero texture: a defect only where the world has a material (a plate, a screen); the chrome world's texture is its plate, and nothing else is painted.
51
+ - Inconsistent light direction across plates, shadows and highlights.
52
+ - **A lone inverted section** inside an otherwise light or dark page: the copy-paste accident; the chrome world bans a black panel on a light page.
53
+ - **Flat text-on-plain-ground sections** read as unfinished **where the world builds depth**; where the world is hairlines by design, a flat band is the design.
54
+
55
+ ### Layout
56
+
57
+ - Total symmetry; three equal feature columns; every card the same size.
58
+ - Fixed `100vh` sections: the world's floored stage or `100dvh`.
59
+ - Flexbox percentage maths for a grid: `grid` with named tracks.
60
+ - Missing `max-width`: the container is `spacing.container`.
61
+ - Uniform radii everywhere: the world's three-step radius tier (pill, panel, tile), and nothing between.
62
+ - No overlap or depth where the world has a cascade; misaligned baselines across side-by-side cards.
63
+
64
+ ### Interaction
65
+
66
+ - Missing hover, press, focus, loading, empty or error states: every one is a state the page's states file names and the kit renders.
67
+ - Dead placeholder links (`href="#"`): a link goes somewhere or is not a link.
68
+ - No active-page indicator in navigation (the world's lit hairline, `aria-current`).
69
+ - Animation of layout-triggering properties; motion in script; anything that escapes the Kill Switch.
70
+
71
+ ### Content
72
+
73
+ Text is `copy-desk`'s; this audit **flags** and the ship notes carry the flags. Placeholder person names, fake round numbers, invented startup brand names, cliche marketing verbs, exclamation marks in success copy, passive voice, identical dates, repeated avatars. The fixtures under `modules/ui-design/kit/fixtures/` are the neutral builder and the real feed shapes; a page rendered with them shows whether the layout survives real data.
74
+
75
+ ### Code and omissions
76
+
77
+ - Div soup where the world requires structure: one `<main>`, one `<header>`, one `<footer>`, one `<h1>`, a skip link that targets `<main>`.
78
+ - Inline styles; arbitrary `z-index` values (a named scale on `body`); missing `alt` text; missing social meta tags; imports that do not exist.
79
+ - Missing legal links (the footer row: privacy, terms, contact), a 404 that renders the world (the platform's game-less 404 tail), field validation, the skip link (a floor, not a nicety), a consent banner where the instance needs one.
80
+
81
+ ## The upgrade catalogue
82
+
83
+ A replacement for each generic pattern, each marked for this platform:
84
+
85
+ | Generic pattern | Upgrade | On this platform |
86
+ |---|---|---|
87
+ | A static headline | **Variable-font animation** on `font-variation-settings`, on the state curve | CSS-only, where the pack's face is variable |
88
+ | A fade-in title | **Text-mask reveal**: a clip-path wipe on the entrance curve | CSS-only; a gradient mask is banned where the world bans gradient text |
89
+ | A symmetric grid | **The broken grid**: asymmetric spans, one tile past the container by a gutter, zero empty cells | CSS-only |
90
+ | A card row | **The parallax card stack**: sticky panels in a tall band, each one step lower | CSS-only (`position: sticky`) |
91
+ | Linear easing | **Spring-like motion**: the world's entrance curve, a little overshoot where the world names none | CSS-only; no spring library |
92
+ | A flat overlay | **True layered glass**: the world's glass tokens, blur only on a fixed layer | CSS-only |
93
+ | A hover border | **Spotlight border** following the pointer | **Not available** (needs pointer tracking in script); use a hairline that lights to the accent on hover |
94
+ | A grey drop shadow | **Tinted shadow**: `color-mix(in srgb, var(--ink) 20%, transparent)` | CSS-only, where the world has a shadow |
95
+ | A flat ground | **Grain** on a fixed pointer-inert pseudo-element | Only where the world has that material |
96
+ | An empty hero | **The plate**: the world's hero object by token, or the air | Hero plates and hero objects only; never a generated image as a step |
97
+
98
+ ## The fix order (impact per risk)
99
+
100
+ Apply in this order, one group per commit, and stop when the audit list is empty:
101
+
102
+ 1. **Fonts**: the pack's two stacks through the world's roles; `font-display: swap` with a sized fallback.
103
+ 2. **Palette cleanup**: every literal becomes a `var()` or a `color-mix()` of the fifteen; the dark twin written twice; no page `:root`; the accent spent only on the pressable and the live. A page that "needs a different palette" needs a look: render it under one from `modules/ui-design/styles/` with `--style <name>` on the kit to compare, and leave adopting it to the owner's `config/branding.json`; a redesign never carries a private palette.
104
+ 3. **Hover, press and focus states**: the state curve, the world's ring, the 24px floor on every control.
105
+ 4. **Layout and spacing**: the world's rhythm, the container, the breakpoint, the radius tier.
106
+ 5. **Component replacement**: the world's components for the page's generic ones (its bar, its door, its ledger rows, its card).
107
+ 6. **Loading, empty and error states**: designed, named in the states file, rendered.
108
+ 7. **Typographic polish**: tracking, measure, tabular figures, balanced headings.
109
+
110
+ ## Hard rules
111
+
112
+ - **Keep the existing stack. Never migrate frameworks.** A redesign that changes the build tool is a different task and an ADR.
113
+ - **Do not break functionality.** `node modules/ui-design/kit/probe.js --page <path>` is PASS before the first commit and after the last; a probe that was green and goes red reverts the group that broke it.
114
+ - **Check the dependency manifest and the styling-library version before adding anything**, and adding is a task decision: print the command in the ship notes, do not run it inside a taste pass.
115
+ - **Small, reviewable diffs**: one audit group per commit, each naming its rows. The grader reads the real diff.
116
+ - **The world wins.** An audit row the instance's `DESIGN.md` calls for by name is not a defect; say so in the notes and move on.
117
+
118
+ ## Proof
119
+
120
+ The before and after shots from the same states file, side by side, in the modes the page serves and at 1440 / 390 / 320; `render.js` ALL CLEAN after; `probe.js` PASS before and after; the contrast pairs measured; the audit list in the ship notes with every closed row struck and every world-sanctioned row marked as such.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.459",
3
+ "version": "1.19.461",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.459",
9
+ "version": "1.19.461",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.459",
3
+ "version": "1.19.461",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -259,8 +259,12 @@ function scanDuplicateProse(relFiles) {
259
259
  for (const { text, startLine } of paragraphs) {
260
260
  const trimmed = text.trim();
261
261
  if (trimmed.length < MIN_DUPE_CHARS) continue;
262
- // Skip content that is intentionally generated/duplicated across files
262
+ // Skip content that is intentionally generated/duplicated across files:
263
+ // generated blocks, and the ui-design module's PLATFORM PREAMBLE, which
264
+ // its policy requires byte-identical in every skill it ships (ADR 0198 §4;
265
+ // tests/ui_design_skills.mjs pins the copies against PREAMBLE.md).
263
266
  if (trimmed.startsWith('<!-- BEGIN GENERATED')) continue;
267
+ if (trimmed.startsWith('<!-- BEGIN PLATFORM PREAMBLE')) continue;
264
268
  // Normalize internal whitespace for comparison
265
269
  const key = trimmed.replace(/\s+/g, ' ');
266
270
  if (!byText.has(key)) byText.set(key, []);
package/src/module-api.js CHANGED
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
33
33
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
34
34
  // the entry to that file. Look for a version's history there, not here.
35
35
  // ---------------------------------------------------------------------------
36
- const CORE_VERSION = '1.19.459'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.461'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
37
 
38
38
  // A namespaced logger so a module's log lines are attributable + consistent.
39
39
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -53,22 +53,26 @@ test('an unknown age reads as fresh, never as an accusation', () => {
53
53
 
54
54
  test('ageing changes how a row LOOKS, not only where it sits', () => {
55
55
  // The whole point of the clause. Each band must have its own visual treatment
56
- // in CSS, or "visibly aging" is just a sort order with extra steps.
56
+ // in CSS, or "visibly aging" is just a sort order with extra steps. Since the
57
+ // redesign (task 1003471) the treatment is the age PILL's colour in the row's
58
+ // right column — a glance down the list reads the column — and never a
59
+ // coloured band down the row's edge, which this world does not draw.
57
60
  const css = read('modules', 'hall-ui', 'public', 'collab.css');
58
61
  const L = lib();
59
62
  for (const b of L.BANDS) {
60
- assert.match(css, new RegExp(`\\.cb-item--${b.id}\\b`), `${b.id} has its own styling`);
63
+ assert.match(css, new RegExp(`^\\.cb-age--${b.id}\\s*\\{[^}]*color: var\\(--`, 'm'), `${b.id} has its own colour`);
61
64
  }
62
- assert.match(css, /\.cb-item--stale\s*\{[^}]*border-left-color/, 'the band reads down a list at a glance');
65
+ assert.match(css, /\.cb-age--stale\s*\{[^}]*var\(--danger\)/, 'stale reads down a list at a glance, in the danger ink');
63
66
  // Colour alone is not a message and not everyone sees it.
64
- assert.match(css, /\.cb-item--stale \.cb-age/, 'and the words are emphasised too');
67
+ assert.match(css, /\.cb-age--stale\s*\{[^}]*font-weight/, 'and the words are emphasised too');
68
+ assert.ok(!/border-left/.test(css), 'no coloured band');
65
69
  });
66
70
 
67
71
  test('the row markup carries the band class and the label', () => {
68
72
  const js = read('modules', 'hall-ui', 'public', 'collab.js');
69
- assert.match(js, /cb-item--\$\{L\.ageBand\(/, 'the class comes from the band');
70
- assert.match(js, /cb-age cb-age--\$\{band\.id\}/, 'and so does the chip');
73
+ assert.match(js, /cb-age cb-age--\$\{band\.id\}/, 'the pill\'s class comes from the band');
71
74
  assert.match(js, /escapeHtml\(band\.label\)/, 'the label is rendered, escaped');
75
+ assert.match(js, /L\.ageBand\(req\.created_at, nowMs\)/, 'through the lib, so the boundaries are the tested ones');
72
76
  });
73
77
 
74
78
  test('oldest first WITHIN the page, in addition to the band', () => {
@@ -143,7 +147,9 @@ test('every CSS token the page uses exists in style.css', () => {
143
147
  const css = read('modules', 'hall-ui', 'public', 'collab.css');
144
148
  const style = read('modules', 'hall-ui', 'public', 'style.css');
145
149
  const used = new Set([...css.matchAll(/var\((--[a-z-]+)\)/g)].map((x) => x[1]));
146
- assert.ok(used.size >= 5);
150
+ // Four since the redesign (task 1003471): the sheet holds only the age
151
+ // bands' inks, the row and the panel being the archetype's and the shell's.
152
+ assert.ok(used.size >= 4);
147
153
  for (const t of used) {
148
154
  assert.ok(new RegExp(`^\\s*${t}:`, 'm').test(style), `${t} must be defined (idea 1000821)`);
149
155
  }
@@ -19,7 +19,7 @@ import path from 'node:path';
19
19
  const require = createRequire(import.meta.url);
20
20
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
21
  const SCANNER = path.join(ROOT, 'scripts', 'gds', 'docs-entropy.js');
22
- const { collectMarkdownFiles, parseFile } = require(SCANNER); // require.main-guarded → no scan on import
22
+ const { collectMarkdownFiles, parseFile, scanDuplicateProse } = require(SCANNER); // require.main-guarded → no scan on import
23
23
 
24
24
  test('collectMarkdownFiles passes the glob as an argv array, never a shell string', () => {
25
25
  let seen = null;
@@ -138,3 +138,31 @@ test('the real scan on THIS checkout matches the CI baselines (the platform-pari
138
138
  + 'on a CRLF checkout this read 21, because every heading in every CRLF file went unparsed',
139
139
  );
140
140
  });
141
+
142
+ // The duplicate-prose scan skips content that is DUPLICATED BY POLICY: generated blocks,
143
+ // and the ui-design module's PLATFORM PREAMBLE, which ADR 0198 §4 requires byte-identical
144
+ // in every skill the module ships (task 1003325 / ADR 0220 — four in-house copies would
145
+ // have tripped the doc_duplicate_prose ratchet on the policy working as designed). The
146
+ // skip is a marker prefix, so a plain paragraph beside it in the same two files is still
147
+ // flagged: the calibration that proves the skip is narrow.
148
+ test('scanDuplicateProse skips a paragraph opening with the PLATFORM PREAMBLE marker, and still flags a plain duplicate beside it', () => {
149
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'docs-entropy-preamble-'));
150
+ const plain = 'A plain paragraph long enough to clear the scanner minimum of one hundred and twenty characters, written into two files so the scan has a real duplicate to report.';
151
+ const preamble = [
152
+ '<!-- BEGIN PLATFORM PREAMBLE · a fixture block, long enough to clear the minimum on its own, duplicated on purpose -->',
153
+ '> **Platform preamble.** Read the world first; this block is byte-identical in every skill the module ships.',
154
+ '<!-- END PLATFORM PREAMBLE -->',
155
+ ].join('\n');
156
+ const generated = '<!-- BEGIN GENERATED fixture --> a generated block long enough to clear the minimum, also duplicated on purpose, also skipped <!-- END GENERATED -->';
157
+ for (const name of ['a.md', 'b.md']) {
158
+ fs.writeFileSync(path.join(tmp, name), `# ${name}\n\n${plain}\n\n${preamble}\n\n${generated}\n`);
159
+ }
160
+ // scanDuplicateProse resolves each rel against the repo root, so a temp file is
161
+ // addressed by its relative path from there (path.join normalises the `..` hops).
162
+ const rel = (f) => path.relative(ROOT, path.join(tmp, f)).split(path.sep).join('/');
163
+ const dupes = scanDuplicateProse([rel('a.md'), rel('b.md')]);
164
+ assert.equal(dupes.length, 1, `exactly the plain paragraph is flagged, got ${JSON.stringify(dupes.map((d) => d.snippet.slice(0, 40)))}`);
165
+ assert.ok(dupes[0].snippet.startsWith('A plain paragraph'), 'the flagged block is the plain paragraph');
166
+ assert.equal(dupes[0].locations.length, 2, 'seen in both files');
167
+ fs.rmSync(tmp, { recursive: true, force: true });
168
+ });
@@ -0,0 +1,214 @@
1
+ // tests/hall_collab_world.mjs — the collaboration pages in the chrome world
2
+ // (task 1003471, goal 1000081): collab and the copy desk, redesigned on the
3
+ // hall's row archetype (oversight.css) rather than re-skinned. Static pins over
4
+ // the shipped source: the family sheet is linked and the page sheets spend only
5
+ // the fifteen, every list is the family's row and the card-in-a-card is gone,
6
+ // state is colour and never opacity, the facts the audit found thrown away (who
7
+ // is asking; the duplicate total; the surface count) are rendered, the server
8
+ // names the asker, the dialogs are the family modal, the lists that grow are
9
+ // paged through the kit, and the harness carries what the pages read.
10
+ import test from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
18
+ const FIX = path.join(ROOT, 'scripts', 'hall-preview', 'fixtures');
19
+ const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
20
+ const readRoot = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8').replace(/\r\n/g, '\n');
21
+ const PAGES = ['collab', 'copy-desk'];
22
+
23
+ // collab-lib.js is pure, so RUN it rather than grep it.
24
+ function lib() {
25
+ const win = {};
26
+ new Function('window', read('collab-lib.js'))(win);
27
+ return win.OTBCollabLib;
28
+ }
29
+
30
+ test('both pages link the family sheet, before their own', () => {
31
+ for (const p of PAGES) {
32
+ const html = read(`${p}.html`);
33
+ const fam = html.indexOf('/builders/oversight.css');
34
+ const own = html.indexOf(`/builders/${p}.css`);
35
+ assert.ok(fam > 0, `${p}.html links oversight.css`);
36
+ assert.ok(own > fam, `${p}.html links its own sheet after the family's`);
37
+ }
38
+ });
39
+
40
+ test('the page sheets spend only the fifteen, in px, and never opacity', () => {
41
+ for (const p of PAGES) {
42
+ const css = read(`${p}.css`);
43
+ const hexes = css.match(/#[0-9a-f]{3,8}\b/gi) || [];
44
+ assert.deepEqual(hexes, [], `${p}.css carries literal colours: ${hexes.join(', ')}`);
45
+ assert.ok(!/\d(\.\d+)?rem\b/.test(css), `${p}.css sizes in px plus tokens, not rem`);
46
+ assert.ok(!/opacity\s*:/.test(css), `${p}.css carries state as colour, never opacity`);
47
+ assert.ok(!/border-left/.test(css), `${p}.css paints no coloured band`);
48
+ }
49
+ // The one literal the copy desk keeps: the native dialog's scrim.
50
+ const cd = read('copy-desk.css');
51
+ const literals = cd.match(/rgba?\([^)]*\)/g) || [];
52
+ assert.deepEqual(literals, ['rgba(0, 0, 0, 0.55)'], 'black with alpha as a scrim, and nothing else');
53
+ assert.match(cd, /\.cd-dialog::backdrop \{ background: rgba\(0, 0, 0, 0\.55\); \}/, 'and it is spent on ::backdrop');
54
+ });
55
+
56
+ test('every list is the family\'s row, and the card-in-a-card is gone', () => {
57
+ for (const p of PAGES) assert.match(read(`${p}.js`), /class="ov-row/, `${p}.js renders .ov-row`);
58
+ // A class name, whole: `cd-stat` must not match the `cd-status` filter id.
59
+ const gone = (src, cls) => !new RegExp(`${cls}(?![a-z])`).test(src);
60
+ for (const old of ['cb-item', 'cb-list', 'cb-ctx']) {
61
+ assert.ok(gone(read('collab.js'), old) && gone(read('collab.css'), `\\.${old}`), `collab no longer emits ${old}`);
62
+ }
63
+ for (const old of ['cd-card', 'cd-stat', 'cd-tag', 'cd-filters__select', 'cd-browse', 'cd-empty']) {
64
+ assert.ok(gone(read('copy-desk.js'), old) && gone(read('copy-desk.css'), `\\.${old}`), `copy desk no longer emits ${old}`);
65
+ }
66
+ for (const p of PAGES) assert.match(read(`${p}.html`), /class="empty-state"/, `${p}.html empties with the kit's own state`);
67
+ });
68
+
69
+ // ---- collab: the facts the audit found thrown away ---------------------------
70
+ test('collab renders WHO IS ASKING, as a door to their profile', () => {
71
+ const L = lib();
72
+ assert.equal(L.askerOf({ requested_by: 92, requested_by_name: 'Preview Gamma', requested_by_login: 'preview-gamma' }), 'Preview Gamma');
73
+ assert.equal(L.askerOf({ requested_by: 92, requested_by_login: 'preview-gamma' }), 'preview-gamma', 'the login when there is no display name');
74
+ assert.equal(L.askerOf({ requested_by: 92 }), 'builder 92', 'an older server\'s bare id is still a name, never nothing');
75
+ const js = read('collab.js');
76
+ assert.match(js, /L\.askerOf\(req\)/, 'the row asks the lib who is asking');
77
+ assert.match(js, /\/builders\/profile\?id=\$\{escapeHtml\(String\(req\.requested_by\)\)\}/, 'and links the hall\'s per-builder profile');
78
+ assert.match(js, /asked by \$\{who\}/, 'as a fact on the row');
79
+ });
80
+
81
+ test('the server names the people on a row through a join, and the writes stay bare', () => {
82
+ const src = readRoot('modules', 'lifecycle', 'help-requests.js');
83
+ assert.match(src, /requested_by_login, a\.display_name AS requested_by_name/, 'the asker is joined');
84
+ assert.match(src, /needs_builder_login, n\.display_name AS needs_builder_name/, 'and the by-name addressee');
85
+ assert.match(src, /LEFT JOIN builders a ON a\.id = h\.requested_by/, 'a LEFT join, so a row never vanishes with its builder');
86
+ for (const fn of ['listOpenForBuilder', 'listMine']) {
87
+ const body = src.slice(src.indexOf(`async function ${fn}`));
88
+ assert.match(body.slice(0, 700), /SELECT \$\{LIST_COLS\} \$\{LIST_FROM\}/, `${fn} reads the joined columns`);
89
+ }
90
+ const upd = src.slice(src.indexOf('async function setHelpRequestStatus'));
91
+ assert.match(upd.slice(0, 600), /RETURNING \$\{COLS\}/, 'the UPDATE cannot reach a joined alias, so it returns the bare columns');
92
+ });
93
+
94
+ test('the first line is decided: the ask\'s first sentence is the title, the rest the sub, no word lost', () => {
95
+ const L = lib();
96
+ const two = L.titleOf('The record exits to the board. I cannot tell which link does it.');
97
+ assert.deepEqual(two, { title: 'The record exits to the board.', rest: 'I cannot tell which link does it.' });
98
+ const one = L.titleOf('Is there a safe order of operations?');
99
+ assert.deepEqual(one, { title: 'Is there a safe order of operations?', rest: '' }, 'one sentence is the whole title');
100
+ const long = 'A'.repeat(150) + '. Then more.';
101
+ assert.equal(L.titleOf(long).title, long, 'a first sentence too long to be a line keeps the whole ask together');
102
+ assert.equal(L.titleOf('docs/x.md is stale and y.js too').title, 'docs/x.md is stale and y.js too', 'a dot inside a path is not a sentence break');
103
+ const t = L.titleOf(' padded. and trailing ');
104
+ assert.equal(`${t.title} ${t.rest}`, 'padded. and trailing', 'nothing is clipped');
105
+ const js = read('collab.js');
106
+ assert.match(js, /L\.titleOf\(req\.what_is_stuck\)/);
107
+ assert.match(js, /class="ov-row__title">\$\{escapeHtml\(t\.title\)\}/);
108
+ assert.match(js, /class="ov-row__sub">\$\{escapeHtml\(t\.rest\)\}/);
109
+ });
110
+
111
+ test('"asked of you" reads in runs — by name first, then each craft', () => {
112
+ const L = lib();
113
+ assert.equal(L.groupOf({ needs_builder_id: 95 }).id, 'you');
114
+ assert.equal(L.groupOf({ needs_craft: 'artist' }).label, 'asked of artists');
115
+ assert.match(L.addressedTo({ needs_builder_id: 7, needs_builder_name: 'Preview Alpha' }, 9), /Preview Alpha by name/, 'a named addressee is named');
116
+ const js = read('collab.js');
117
+ assert.match(js, /ov-group__head/, 'each run has the family\'s group head');
118
+ assert.match(js, /Number\(b\.id === 'you'\) - Number\(a\.id === 'you'\)/, 'and the personal run sorts first');
119
+ });
120
+
121
+ test('the age is a state pill in the row\'s right column, and every band has its own colour', () => {
122
+ const css = read('collab.css');
123
+ const L = lib();
124
+ for (const b of L.BANDS) assert.match(css, new RegExp(`^\\.cb-age--${b.id}\\s*\\{[^}]*color: var\\(--`, 'm'), `${b.id} has its own colour rule`);
125
+ assert.match(css, /\.cb-age--stale\s*\{[^}]*var\(--danger\)/, 'stale is the danger ink');
126
+ assert.match(css, /\.cb-age--aging\s*\{[^}]*var\(--warn\)/, 'aging is the warn ink');
127
+ const js = read('collab.js');
128
+ assert.match(js, /class="fact-pill cb-age cb-age--\$\{band\.id\}"/, 'the pill is the shell\'s fact-pill');
129
+ assert.match(js, /<div class="ov-row__right">\$\{ageChip\(req, nowMs\)\}/, 'in the right column, so a glance down the list reads the ages');
130
+ });
131
+
132
+ // ---- the copy desk: quieter, and no fact lost ----------------------------------
133
+ test('the copy desk\'s counts are the shell\'s glass strip, carrying two facts the boxes dropped', () => {
134
+ assert.match(read('copy-desk.html'), /class="stat-row ov-summary cd-summary" id="cd-summary-scroll"/);
135
+ const js = read('copy-desk.js');
136
+ assert.match(js, /stat-tile__num/, 'the figure is the shell\'s tile');
137
+ assert.match(js, /of \$\{int\(c\.duplicates_total\)\} said more than once/, 'the duplicate total was never shown before');
138
+ assert.match(js, /across \$\{int\(surfaces\)\} surface/, 'nor the surface count');
139
+ assert.ok(!/cd-stat__n/.test(js), 'the bordered box is gone');
140
+ });
141
+
142
+ test('state is colour from the verification vocabulary, never opacity', () => {
143
+ const js = read('copy-desk.js');
144
+ assert.match(js, /pill\('fact-pill--open', 'already flagged'\)/);
145
+ assert.match(js, /pill\('fact-pill--ok', 'looked at'\)/);
146
+ assert.match(js, /pill\('', 'untouched'\)/, 'the untouched row is the plain pill: colour marks the exception in a list that is mostly work');
147
+ assert.match(js, /pill\('fact-pill--warn', 'target gone'/);
148
+ assert.match(js, /f\.status === 'resolved' \? 'fact-pill--ok' : 'fact-pill--quiet'/);
149
+ });
150
+
151
+ test('the filters and the search are the kit\'s bar; the duplicates are paged through the kit', () => {
152
+ const js = read('copy-desk.js');
153
+ assert.equal((js.match(/kit\.makeFilterBar\(\{/g) || []).length, 2, 'two bars: the queue\'s filters and the search');
154
+ for (const id of ['cd-status', 'cd-surface', 'cd-search']) assert.match(js, new RegExp(`id: '${id}'`), `${id} is a kit control`);
155
+ assert.match(js, /kit\.makePager\('cd-dupes'/);
156
+ assert.match(js, /kit\.paginate\(ordered, dupePager\)/);
157
+ assert.match(js, /kit\.wirePager\(dupePager, rerender\)/);
158
+ assert.match(read('copy-desk.html'), /src="\/builders\/hall-kit\.js/, 'the page loads the kit');
159
+ });
160
+
161
+ test('the two dialogs are the family modal on a native <dialog>, with the number input in a field', () => {
162
+ const html = read('copy-desk.html');
163
+ assert.equal((html.match(/<dialog id="cd-(close-)?dialog" class="ov-modal cd-dialog">/g) || []).length, 2);
164
+ assert.match(html, /<div class="ov-field ov-field--wide">\s*<label class="ov-field__l" for="cd-close-task">[^<]*<\/label>\s*<input type="number" id="cd-close-task"/, 'the number input sits in the family field, which restates the 30px pill');
165
+ assert.ok(!/<label[^>]*>[^<]*<span class="cd-dialog__hint"/.test(html), 'no hint lives inside a label (the label voice would uppercase it)');
166
+ assert.equal((html.match(/class="ov-form__why"/g) || []).length, 3, 'the three hints are the form\'s own explainer line');
167
+ assert.match(html, /class="btn-accent" id="cd-submit"/, 'the accent door files the flag');
168
+ });
169
+
170
+ test('the copy desk renders MORE facts per row than any other page, and lost none', () => {
171
+ const js = read('copy-desk.js');
172
+ for (const fact of ['flagged_by_name', 'flagged_by_login', 'flagged_at', 'resolution_note', 'resolved_task_id',
173
+ 'text_at_flag', 'file_at_flag', 'line_at_flag', 'f.current.file', 'f.current.line', 'f.moved', 'target_state',
174
+ 'd.at', 'd.occurrences', 'd.surface', 'e.origin', 'e.file', 'truncated', 'blind_spots', 'freshness_check', 'r.at']) {
175
+ assert.ok(js.includes(fact), `${fact} is still rendered`);
176
+ }
177
+ assert.match(js, /said, when flagged:/, 'the snapshot is still labelled as the snapshot');
178
+ assert.match(js, /whole surface/, 'a surface-scope flag still says so');
179
+ });
180
+
181
+ test('the closing floor is the pack\'s rank label, not a Greek name in the markup', () => {
182
+ assert.ok(!/Metic\+/.test(read('copy-desk.html')), 'no "Metic+" in the lede');
183
+ assert.match(read('copy-desk.html'), /<span id="cd-close-rank">/);
184
+ assert.match(read('copy-desk.js'), /rankLabel\('metic'\)/);
185
+ });
186
+
187
+ // ---- the harness carries what the pages read ------------------------------------
188
+ test('the harness fixtures exist and carry the shapes the pages read', () => {
189
+ const fx = (n) => JSON.parse(fs.readFileSync(path.join(FIX, `${n}.json`), 'utf8'));
190
+ const forMe = fx('help-requests__for-me');
191
+ assert.ok(Array.isArray(forMe.help_requests) && forMe.help_requests.length >= 4, 'four asks, one per band');
192
+ assert.ok(forMe.help_requests.every((r) => r.requested_by_name && r.what_is_stuck && r.created_at), 'each names its asker');
193
+ assert.ok(forMe.help_requests.some((r) => r.needs_builder_id != null), 'one is by name');
194
+ assert.deepEqual(forMe.crafts, ['builder']);
195
+ const mine = fx('help-requests__mine');
196
+ assert.ok(mine.help_requests.length >= 2 && mine.help_requests.every((r) => String(r.requested_by) === '95'), 'the viewer\'s own (me.json is builder 95)');
197
+ const q = fx('copy-desk__queue');
198
+ assert.ok(q.flags.some((f) => f.target_state === 'gone') && q.flags.some((f) => f.moved) && q.flags.some((f) => f.scope === 'surface'), 'a gone target, a moved one, a whole-surface one');
199
+ assert.ok(q.flags.some((f) => f.status === 'resolved') && q.flags.some((f) => f.status === 'dismissed'), 'and both closed outcomes');
200
+ assert.ok(q.duplicates.length >= 8 && q.duplicates.every((d) => Array.isArray(d.at) && d.at.length === d.occurrences), 'real duplicates with their places');
201
+ assert.ok(q.registry.at && q.registry.freshness_check && q.registry.counts && q.registry.surfaces, 'the registry block the note reads');
202
+ const s = fx('copy-desk__strings');
203
+ assert.ok(Array.isArray(s.entries) && s.entries.length > 0 && typeof s.matched === 'number' && 'truncated' in s && 'limit' in s);
204
+ });
205
+
206
+ test('the states files walk the new states', () => {
207
+ const cd = JSON.parse(read('copy-desk.states.json'));
208
+ assert.deepEqual(Object.keys(cd.states), ['queue', 'out', 'search', 'composer', 'closer']);
209
+ assert.deepEqual(cd.states.search.actions[1], ['fill', '#cd-search', 'save']);
210
+ assert.deepEqual(cd.states.composer.actions[1], ['clickIfVisible', '.cd-flag-btn'], 'clickIfVisible: against the kit\'s stub the button does not exist');
211
+ assert.deepEqual(cd.states.closer.actions[1], ['clickIfVisible', '.cd-close-btn']);
212
+ const cb = JSON.parse(read('collab.states.json'));
213
+ assert.deepEqual(cb.states.board.expect.visible, ['#cb-for-me']);
214
+ });