@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.
- package/.bongos-core.json +82 -27
- package/.claude/skills/design/SKILL.md +5 -1
- package/docs/adr/0222-the-image-family-under-the-hero-only-rule.md +36 -0
- package/docs/adr/0223-the-detector-is-two-tiers-in-the-kit.md +33 -0
- package/docs/adr/README.md +2 -0
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/ui-look-before-you-ship.md +2 -0
- package/modules/ui-design/kit/README.md +12 -0
- package/modules/ui-design/kit/check-mock.js +18 -5
- package/modules/ui-design/kit/lib.js +22 -1
- package/modules/ui-design/kit/render.js +4 -2
- package/modules/ui-design/kit/tells.js +204 -0
- package/modules/ui-design/module.json +1 -1
- package/modules/ui-design/skills/README.md +6 -2
- package/modules/ui-design/skills/brandkit/SKILL.md +62 -0
- package/modules/ui-design/skills/image-to-code/SKILL.md +66 -0
- package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +62 -0
- package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +83 -0
- package/modules/ui-design/skills/impeccable/SKILL.md +1 -1
- package/modules/ui-design/styles/grove/DESIGN.md +1 -1
- package/modules/ui-design/styles/grove/assets/grove-pod-cut.webp +0 -0
- package/modules/ui-design/styles/grove/assets/grove-pod-cut.webp.json +12 -0
- package/modules/ui-design/styles/grove/assets/grove-pod.jpg +0 -0
- package/modules/ui-design/styles/grove/assets/grove-pod.jpg.json +12 -0
- package/modules/ui-design/styles/grove/mock.html +15 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/ui_design_kit.mjs +86 -0
- package/tests/ui_design_skills.mjs +52 -0
|
@@ -136,7 +136,7 @@ Verification is **bounded passes, never an open loop**, and the budget covers th
|
|
|
136
136
|
|
|
137
137
|
## Not here, and where it went
|
|
138
138
|
|
|
139
|
-
- **The anti-pattern detector**:
|
|
139
|
+
- **The anti-pattern detector**: landed as rules in the kit (task 1003329, ADR 0223): `kit/tells.js` runs inside `check-mock.js` (two floors that FAIL, eighteen tells that WARN, a kicker above the h1 a FAIL where the world bans the eyebrow), and the rendered twins ride `render.js` as `~ tell:` lines per state; `contrast.js` is the pair floor. Run it once per review round, treat a clean scan as a floor and never as proof, verify every finding in context, and name a false positive where it fires with `kit-ignore: <rule> (reason)` rather than silencing the rule.
|
|
140
140
|
- **The live loop**: `claude plugin install playwright@claude-plugins-official`, a prerequisite the module never installs, driving `kit/serve.js`'s stub (`SURFACE=<dir or module key>`, `STYLE=<look>`). Variants are states in the states file, rendered by the kit; accepting one is a commit and discarding one is a checkout; there is no in-page overlay, no injected script, no journal, and no leftover to sweep because nothing was injected.
|
|
141
141
|
- **Hooks, the bypass agent, telemetry, the remote bundle, the installer, `doctor`, `pin`, `unpin`**: gone. The ship gate is the sweep; the materialiser is the install; a reviewer runs with ordinary permissions.
|
|
142
142
|
- **The asset producer**: images are hero plates and hero objects by token (task 1003328 for the image family); nothing here generates, crops or cleans an image.
|
|
@@ -115,7 +115,7 @@ Everything in this look is cut from soft paper and set down in a garden. Contain
|
|
|
115
115
|
|
|
116
116
|
- **Tinted shadows.** `0 10px 26px -14px shade, 0 2px 6px shade-soft`, where `shade` is the ink at 16% and `shade-soft` the ink at 7%; in dark, black at 60% and 35%. Grey shadows are banned: a shadow is the ground's own colour, deepened.
|
|
117
117
|
- **The slab.** A deep-green panel (the sea mixed 62% into black, the same in both modes) with cream type and a 20%-white hairline inside it.
|
|
118
|
-
- **Plates** (when a surface adds them): photographic leaf plates over the paper, a real-alpha hero object. A surface's plates are its own
|
|
118
|
+
- **Plates** (when a surface adds them): photographic leaf plates over the paper, a real-alpha hero object. A surface's plates are its own. This look's specimen carries one, as the worked example of the pair convention (task 1003328, `imagegen-frontend-web`): `assets/grove-pod.jpg` (the black-ground plate, dark) and `assets/grove-pod-cut.webp` (the alpha cutout, light), a sculpted paper seed pod in sage and tan, each with a `<file>.json` provenance sidecar; the tokens are `--plate-pod` / `--plate-pod-cut` on the mock's `body`.
|
|
119
119
|
|
|
120
120
|
## Motion
|
|
121
121
|
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "cutout",
|
|
3
|
+
"model": "nano_banana_pro",
|
|
4
|
+
"aspect_ratio": "1:1",
|
|
5
|
+
"resolution": "2k",
|
|
6
|
+
"generated": "2026-09-02T00:57Z",
|
|
7
|
+
"round": 1,
|
|
8
|
+
"prompt": "Background removed (the art pipeline's image background remover) from the grove-pod generation: a single sculpted paper seed pod, hand-folded from thick pale sage and warm tan paper, soft rounded forms, matte fibrous paper texture, two curled paper leaves at its base, lit softly from the upper left with soft tinted shadows, studio still life, centred, no text, no letters, no logo, no watermark.",
|
|
9
|
+
"source": "the remover's PNG (2048px, real alpha verified with ffmpeg alphaextract, YAVG 118)",
|
|
10
|
+
"encoded": "ffmpeg libwebp quality 88, alpha kept (yuva420p), 1600px (task 1003328)",
|
|
11
|
+
"use": "light mode: the specimen's hero object as an alpha cutout on the pale sage paper"
|
|
12
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "plate",
|
|
3
|
+
"model": "nano_banana_pro",
|
|
4
|
+
"aspect_ratio": "1:1",
|
|
5
|
+
"resolution": "2k",
|
|
6
|
+
"generated": "2026-09-02T00:55Z",
|
|
7
|
+
"round": 1,
|
|
8
|
+
"prompt": "A single sculpted paper seed pod, hand-folded from thick pale sage and warm tan paper, soft rounded forms, matte fibrous paper texture, two curled paper leaves at its base, lit softly from the upper left with soft tinted shadows, studio still life, centred, pure black everywhere the object is not, no text, no letters, no logo, no watermark.",
|
|
9
|
+
"source": "the generation returned a white ground (the one-in-six case); this plate is the real-alpha cutout (grove-pod-cut.webp's source) composited on pure black with ffmpeg, then scaled to 1600px",
|
|
10
|
+
"encoded": "ffmpeg overlay on color=black, scale=1600, JPEG -q:v 3 (task 1003328)",
|
|
11
|
+
"use": "dark mode: the specimen's hero object as a masthead on the black ground"
|
|
12
|
+
}
|
|
@@ -39,6 +39,10 @@ body {
|
|
|
39
39
|
--radius: 22px;
|
|
40
40
|
--radius-sm: 12px;
|
|
41
41
|
--radius-pill: 999px;
|
|
42
|
+
--plate-pod: url(/assets/grove-pod.jpg);
|
|
43
|
+
--plate-pod-cut: url(/assets/grove-pod-cut.webp);
|
|
44
|
+
--plate-pod-hero: var(--plate-pod-cut);
|
|
45
|
+
--pod-shade: drop-shadow(0 18px 22px var(--shade));
|
|
42
46
|
--top-h: 64px;
|
|
43
47
|
--gutter: clamp(16px, 4vw, 26px);
|
|
44
48
|
--band: clamp(40px, 7vh, 72px);
|
|
@@ -73,6 +77,8 @@ body {
|
|
|
73
77
|
--label-face: var(--font-body);
|
|
74
78
|
--label-size: 11.5px;
|
|
75
79
|
--label-space: .06em;
|
|
80
|
+
--plate-pod-hero: var(--plate-pod);
|
|
81
|
+
--pod-shade: none;
|
|
76
82
|
}
|
|
77
83
|
@media (prefers-color-scheme: dark) {
|
|
78
84
|
:root:not([data-mode="light"]) body {
|
|
@@ -98,6 +104,8 @@ body {
|
|
|
98
104
|
--label-face: var(--font-body);
|
|
99
105
|
--label-size: 11.5px;
|
|
100
106
|
--label-space: .06em;
|
|
107
|
+
--plate-pod-hero: var(--plate-pod);
|
|
108
|
+
--pod-shade: none;
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
111
|
|
|
@@ -125,6 +133,11 @@ body[data-state="signed-in"] .who { display: inline-flex; }
|
|
|
125
133
|
|
|
126
134
|
/* the bands */
|
|
127
135
|
.band { padding: var(--band) 0; }
|
|
136
|
+
/* the hero object: the specimen's one plate, a sculpted paper seed pod, as a masthead above the
|
|
137
|
+
words, offset right and bleeding past the wrap by a gutter (the masthead anchor of
|
|
138
|
+
imagegen-frontend-web; never text-left image-right). Light takes the alpha cutout on the paper
|
|
139
|
+
under a tinted lift; dark takes the black-ground plate on the black. Decorative: aria-hidden. */
|
|
140
|
+
.pod { width: min(360px, 58vw); aspect-ratio: 1; margin: calc(-1 * var(--band) / 2) calc(-1 * var(--gutter)) 6px auto; background: var(--plate-pod-hero) center / contain no-repeat; filter: var(--pod-shade); }
|
|
128
141
|
h1 { font-family: var(--font-display); font-weight: 800; font-size: clamp(34px, 6.4vw, 84px); line-height: .96; letter-spacing: -.03em; margin: 0 0 18px; max-width: 12ch; }
|
|
129
142
|
h2 { font-family: var(--font-display); font-weight: 800; font-size: clamp(24px, 3vw, 38px); line-height: 1.08; letter-spacing: -.02em; margin: 0 0 14px; }
|
|
130
143
|
.lede { color: var(--ink-soft); max-width: 56ch; margin: 0; }
|
|
@@ -189,7 +202,7 @@ footer a { color: var(--accent-ink); display: inline-block; padding: 3px 0; font
|
|
|
189
202
|
</style>
|
|
190
203
|
</head>
|
|
191
204
|
<body>
|
|
192
|
-
<!-- THESIS: the grove as a specimen sheet. The thirteen colours and the two faces the pack carries, laid through this look's own derived recipes (its DESIGN.md frontmatter): a bar that floats as a soft card, a rounded heavy display voice, tiles under tinted shadows mixed from the ink, the sea green as a slab a whole band sits on, door pills with the primary in tan, a rail of soft tiles with one lit, ledger rows inside a sculpted panel. This page declares no :root and no colour of its own: the pack is spliced by the stub (STYLE=grove) and every value is a var() or a color-mix() of it. Dark restates the platform's black tier on body, since a mock links no sheet; the candidate's green dark ground cannot travel in a pack, so dark is the black under the tan and the sea. STATES: rest (signed out), signed-in. MODES: ?mode=dark|light. -->
|
|
205
|
+
<!-- THESIS: the grove as a specimen sheet. The thirteen colours and the two faces the pack carries, laid through this look's own derived recipes (its DESIGN.md frontmatter): a bar that floats as a soft card, a rounded heavy display voice, tiles under tinted shadows mixed from the ink, the sea green as a slab a whole band sits on, door pills with the primary in tan, a rail of soft tiles with one lit, ledger rows inside a sculpted panel, and one hero object (a sculpted paper seed pod, the pair convention: the alpha cutout on the paper in light, the black-ground plate in dark, each with a provenance sidecar under assets/). This page declares no :root and no colour of its own: the pack is spliced by the stub (STYLE=grove) and every value is a var() or a color-mix() of it. Dark restates the platform's black tier on body, since a mock links no sheet; the candidate's green dark ground cannot travel in a pack, so dark is the black under the tan and the sea. STATES: rest (signed out), signed-in. MODES: ?mode=dark|light. -->
|
|
193
206
|
<a class="skip" href="#main">Skip to content</a>
|
|
194
207
|
<header class="bar">
|
|
195
208
|
<div class="wrap">
|
|
@@ -207,6 +220,7 @@ footer a { color: var(--accent-ink); display: inline-block; padding: 3px 0; font
|
|
|
207
220
|
<main id="main" tabindex="-1">
|
|
208
221
|
<section class="band">
|
|
209
222
|
<div class="wrap">
|
|
223
|
+
<div class="pod" aria-hidden="true"></div>
|
|
210
224
|
<p class="label badge">A look from the style library</p>
|
|
211
225
|
<h1>The grove</h1>
|
|
212
226
|
<p class="lede">A sculpted paper garden. Pale sage paper, soft corners on everything that holds content, shadows tinted from the ink and never grey, the sea green as a second voice, and one lit tan on everything you can press.</p>
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.465",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.465",
|
|
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.
|
|
3
|
+
"version": "1.19.465",
|
|
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",
|
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.
|
|
36
|
+
const CORE_VERSION = '1.19.465'; // 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');
|
package/tests/ui_design_kit.mjs
CHANGED
|
@@ -185,6 +185,92 @@ test('checkMock: a taste ban applies only when the instance declared it', () =>
|
|
|
185
185
|
assert.ok(!withoutBan.some((r) => r[1].startsWith('taste:')), 'no taste rule is even evaluated without a ban');
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
+
// ── the anti-pattern detector (task 1003329, ADR 0223): every rule trips on a fixture and passes on one ──
|
|
189
|
+
const tellsMod = require(path.join(KIT, 'tells.js'));
|
|
190
|
+
const CLEAN = `<!doctype html><html><head><style>
|
|
191
|
+
::selection{background:var(--accent)} body{color:var(--ink);--font:var(--font-body)} p{max-width:64ch}
|
|
192
|
+
.x{transition:opacity 200ms cubic-bezier(.2,.7,.3,1)} .lift{box-shadow:0 10px 26px -14px var(--shade)}
|
|
193
|
+
</style></head><body><main><h1>Short title</h1><p>Plain copy.</p></main></body></html>`;
|
|
194
|
+
const withSnippet = (snippet) => (typeof snippet === 'function' ? snippet(CLEAN) : CLEAN.replace('</main>', snippet + '</main>'));
|
|
195
|
+
const tripped = (html, prefix, opts) => tellsMod.scanTells(html, opts).find((r) => r[1].includes(prefix) && r[0] !== 'PASS');
|
|
196
|
+
const DETECTOR_CASES = [
|
|
197
|
+
['bounce easing', 'FAIL', '<style>.b{transition:transform 200ms cubic-bezier(.34,1.56,.64,1)}</style>'],
|
|
198
|
+
['a card nested in a card', 'FAIL', '<div class="card"><div class="card">x</div></div>'],
|
|
199
|
+
['identical icon-heading-text', 'WARN', '<ul><li><svg></svg><h3>a</h3><p>b</p></li><li><svg></svg><h3>a</h3><p>b</p></li><li><svg></svg><h3>a</h3><p>b</p></li></ul>'],
|
|
200
|
+
['the hero-metric', 'WARN', '<section><b>99%</b> <b>10k</b> <b>24+</b> uptime</section>'],
|
|
201
|
+
['decorative section numbering', 'WARN', '<p>01 Features</p><p>02 Pricing</p>'],
|
|
202
|
+
['a kicker', 'WARN', (c) => c.replace('<h1>Short title</h1>', '<p class="label">Eyebrow</p><h1>Short title</h1>')],
|
|
203
|
+
['a coloured side stripe', 'WARN', '<style>.q{border-left:4px solid var(--accent)}</style>'],
|
|
204
|
+
['a hard zero-blur', 'WARN', '<style>.h{box-shadow:6px 6px 0 var(--ink)}</style>'],
|
|
205
|
+
['a zero-offset coloured halo', 'WARN', '<style>.g{box-shadow:0 0 24px var(--accent)}</style>'],
|
|
206
|
+
['decorative glass', 'WARN', '<style>.a{backdrop-filter:blur(8px)}.b{backdrop-filter:blur(8px)}.c{backdrop-filter:blur(8px)}</style>'],
|
|
207
|
+
['a default typeface', 'WARN', '<style>h2{font-family:Inter,sans-serif}</style>'],
|
|
208
|
+
['a prose measure', 'WARN', '<style>.wide{max-width:96ch}</style>'],
|
|
209
|
+
['a headline longer', 'WARN', (c) => c.replace('<h1>Short title</h1>', `<h1>${'word '.repeat(22)}</h1>`)],
|
|
210
|
+
['the same entrance', 'WARN', '<style>.a{animation:rise 1s}.b{animation:rise 1s}.c{animation:rise 1s}</style>'],
|
|
211
|
+
['a geometric mask', 'WARN', '<style>img.plate{clip-path:circle(50%)}</style>'],
|
|
212
|
+
['unicode glyphs', 'WARN', '<p>→ one ★ two ✓ three</p>'],
|
|
213
|
+
['a rainbow', 'WARN', '<style>.m{background:conic-gradient(var(--a),var(--b))}</style>'],
|
|
214
|
+
['the browser surfaces', 'WARN', '<style>a{text-decoration:underline}</style><a href="#">x</a>'],
|
|
215
|
+
['proportional numerals', 'WARN', '<table><tr><td>1,200</td><td>34,000</td><td>12345</td></tr></table>'],
|
|
216
|
+
['tiny type', 'WARN', '<style>.s{font-size:9px}</style>'],
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
test('the detector: a clean page trips nothing, and every rule class trips on its own fixture at its own tier', () => {
|
|
220
|
+
const clean = tellsMod.scanTells(CLEAN);
|
|
221
|
+
assert.equal(clean.length, tellsMod.FLOOR_RULES.length + tellsMod.TELL_RULES.length, 'every rule reports a row');
|
|
222
|
+
assert.deepEqual(clean.filter((r) => r[0] !== 'PASS'), [], 'the clean fixture passes every floor and tell');
|
|
223
|
+
for (const [prefix, tier, snippet] of DETECTOR_CASES) {
|
|
224
|
+
const row = tripped(withSnippet(snippet), prefix);
|
|
225
|
+
assert.ok(row, `${prefix}: trips on its fixture`);
|
|
226
|
+
assert.equal(row[0], tier, `${prefix}: reported as ${tier}`);
|
|
227
|
+
assert.ok(row[2], `${prefix}: names what it saw`);
|
|
228
|
+
}
|
|
229
|
+
assert.equal(DETECTOR_CASES.length, tellsMod.FLOOR_RULES.length + tellsMod.TELL_RULES.length, 'one tripping fixture per rule, no rule unpinned');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('the detector: a kicker above the h1 is a tell by default and a FAIL only where the instance bans the eyebrow', () => {
|
|
233
|
+
const page = withSnippet((c) => c.replace('<h1>Short title</h1>', '<p class="eyebrow">Eyebrow</p><h1>Short title</h1>'));
|
|
234
|
+
assert.equal(tripped(page, 'a kicker')[0], 'WARN');
|
|
235
|
+
assert.equal(tripped(page, 'a kicker', { bans: new Set(['eyebrow']) })[0], 'FAIL');
|
|
236
|
+
assert.ok(mock.readDesignBans(fs.readFileSync(path.join(ROOT, 'DESIGN.md'), 'utf8')).has('eyebrow'), 'the chrome world bans it by name');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('the detector: a per-file kit-ignore waiver turns a hit into WAIVE with its reason, and --raw ignores waivers', () => {
|
|
240
|
+
const page = withSnippet('<style>.s{font-size:9px}</style><!-- kit-ignore: tell: tiny type (the mono label is 9px by the world\'s own rule) -->');
|
|
241
|
+
const waivers = tellsMod.parseWaivers(page);
|
|
242
|
+
assert.equal(waivers.get('tell: tiny type'), 'the mono label is 9px by the world\'s own rule');
|
|
243
|
+
const row = tellsMod.scanTells(page, { waivers }).find((r) => r[1].includes('tiny type'));
|
|
244
|
+
assert.equal(row[0], 'WAIVE'); assert.match(row[2], /9px; the mono label/);
|
|
245
|
+
assert.equal(tellsMod.scanTells(page).find((r) => r[1].includes('tiny type'))[0], 'WARN', 'with no waiver map (--raw) the tell stands');
|
|
246
|
+
const file = tmp('m.html', page);
|
|
247
|
+
const raw = run('check-mock.js', ['--file', file, '--no-taste', '--raw']);
|
|
248
|
+
assert.match(raw.stdout, /WARN\s+tell: tiny type/);
|
|
249
|
+
const json = run('check-mock.js', ['--file', file, '--no-taste', '--json']);
|
|
250
|
+
const parsed = JSON.parse(json.stdout);
|
|
251
|
+
assert.ok(parsed.results.some((r) => r.status === 'WAIVE' && /tiny type/.test(r.rule)), '--json carries the WAIVE row');
|
|
252
|
+
assert.ok(!parsed.results.some((r) => r.status === 'FAIL' && /^(floor|tell):/.test(r.rule)), 'a waived tell is not a failure (the fixture fails only the structural platform rules a bare fragment cannot meet)');
|
|
253
|
+
assert.equal(parsed.fails, parsed.results.filter((r) => r.status === 'FAIL').length, '--json counts its own FAIL rows');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('the detector runs inside checkMock (floors fail the mock; a tell never does) and --no-tells switches it off', () => {
|
|
257
|
+
const f = fails(mock.checkMock(GOOD_MOCK.replace('<style>', '<style>.c{transition:transform 200ms cubic-bezier(.34,1.56,.64,1)}')));
|
|
258
|
+
assert.ok(f.some((r) => r.startsWith('floor: bounce easing')), f.join(' | '));
|
|
259
|
+
const rows = mock.checkMock(GOOD_MOCK.replace('<style>', '<style>.s{font-size:9px}'));
|
|
260
|
+
assert.ok(rows.some((r) => r[0] === 'WARN' && /tiny type/.test(r[1])));
|
|
261
|
+
assert.deepEqual(fails(rows), []);
|
|
262
|
+
assert.ok(!mock.checkMock(GOOD_MOCK, { noTells: true }).some((r) => /^(floor|tell):/.test(r[1])), '--no-tells evaluates no detector rule');
|
|
263
|
+
assert.ok(mock.TASTE_BANS.eyebrow.test('an eyebrow above the h1'));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('the rendered tells: craftTells reads the in-page facts as warning lines and never as problems', () => {
|
|
267
|
+
assert.deepEqual(lib.craftTells(clean()), []);
|
|
268
|
+
const t = lib.craftTells({ ...clean(), tells: ['prose-measure: p.lede ~110ch', 'tiny-type: small 9px'] });
|
|
269
|
+
assert.deepEqual(t, ['tell: prose-measure: p.lede ~110ch', 'tell: tiny-type: small 9px']);
|
|
270
|
+
assert.deepEqual(lib.auditFloors({ ...clean(), tells: ['tiny-type: small 9px'] }, { width: 1440, mode: 'dark' }), [], 'a tell is not a floor');
|
|
271
|
+
assert.match(String(lib.factsInPage), /out\.tells = tells/, 'factsInPage collects the tells in the page');
|
|
272
|
+
});
|
|
273
|
+
|
|
188
274
|
test('readDesignBans reads the Don\'t section — the live DESIGN.md declares the five, an empty file none', () => {
|
|
189
275
|
const live = mock.readDesignBans(fs.readFileSync(path.join(ROOT, 'DESIGN.md'), 'utf8'));
|
|
190
276
|
for (const k of ['em-dash', 'emoji', 'gradient-text', 'text-glow', 'box-shadow']) assert.ok(live.has(k), `live DESIGN.md bans ${k}`);
|
|
@@ -148,6 +148,58 @@ test('impeccable carries none of the behaviour that made its origin dangerous: n
|
|
|
148
148
|
assert.match(skill, /1003329/, 'the detector is named as its own task, not shipped here');
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
// ── the image family (task 1003328, ADR 0222): plates and hero objects only, never screens ──
|
|
152
|
+
const IMAGE_FAMILY = ['image-to-code', 'imagegen-frontend-web', 'imagegen-frontend-mobile', 'brandkit'];
|
|
153
|
+
|
|
154
|
+
test('the image family of task 1003328 exists as four in-house skills, declared, each naming the sidecar, "text-free" and the art pipeline key in its own rules', () => {
|
|
155
|
+
const pre = read(path.join(SKILLS, 'PREAMBLE.md')).trimEnd();
|
|
156
|
+
for (const name of IMAGE_FAMILY) {
|
|
157
|
+
assert.ok(ownDirs.includes(name), `${name}: skills/${name}/SKILL.md exists`);
|
|
158
|
+
assert.ok(manifest.contributes.skills.includes(name), `${name}: declared in contributes.skills`);
|
|
159
|
+
const text = read(path.join(SKILLS, name, 'SKILL.md'));
|
|
160
|
+
const own = text.slice(text.indexOf(pre) + pre.length);
|
|
161
|
+
assert.match(own, /sidecar/i, `${name}: names the provenance sidecar beside every plate (DESIGN.md's materials contract)`);
|
|
162
|
+
assert.match(own, /text-free/i, `${name}: restates that a plate carries no text`);
|
|
163
|
+
assert.match(own, /art[- ]key|art pipeline/i, `${name}: generation goes through the instance's art pipeline key, never a credential of its own`);
|
|
164
|
+
assert.match(own, /1003328/, `${name}: names the port task`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('no image-family skill carries the directives the panel flagged: no eagerness, no image-count mandate, no one-image-per-section or per-screen rule, no must-invoke-any-image-tool', () => {
|
|
169
|
+
const pre = read(path.join(SKILLS, 'PREAMBLE.md')).trimEnd();
|
|
170
|
+
for (const name of IMAGE_FAMILY) {
|
|
171
|
+
const text = read(path.join(SKILLS, name, 'SKILL.md'));
|
|
172
|
+
const own = text.slice(text.indexOf(pre) + pre.length);
|
|
173
|
+
assert.doesNotMatch(own, /IMAGE_GENERATION_EAGERNESS|eagerness\s*[:=]/i, `${name}: no eagerness setting`);
|
|
174
|
+
assert.doesNotMatch(own, /do not be lazy|don't be lazy/i, `${name}: no image-count goad`);
|
|
175
|
+
assert.doesNotMatch(own, /\b(exactly|generate|produce|render)\s+one\s+(image|frame|render)\s+per\s+(section|screen)\b/i, `${name}: no one-image-per-section or per-screen rule as a directive (naming the upstream's rule in the Rebuilt-from is fine)`);
|
|
176
|
+
assert.doesNotMatch(own, /\b(must|always)\s+(invoke|call|use)\s+(any|every|an?|the)\s+(available\s+)?image[- ]generat/i, `${name}: no must-invoke-any-image-tool directive`);
|
|
177
|
+
assert.doesNotMatch(own, /scale[s]?\s+(the\s+)?image\s+count\s+with/i, `${name}: no count scaled with sections (naming it as the upstream's is fine; directing it is not)`);
|
|
178
|
+
}
|
|
179
|
+
for (const name of ['imagegen-frontend-mobile', 'brandkit']) {
|
|
180
|
+
const own = read(path.join(SKILLS, name, 'SKILL.md'));
|
|
181
|
+
assert.match(own, /never (a|an) (screen|phone mockup|generated logo)|no phone mockups?/i, `${name}: says what it will not generate`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('the worked example of the image family is the grove specimen: the pair with a sidecar beside every raster, referenced as tokens on the mock body', () => {
|
|
186
|
+
const grove = path.join(MODULE, 'styles', 'grove');
|
|
187
|
+
for (const f of ['grove-pod.jpg', 'grove-pod-cut.webp']) {
|
|
188
|
+
assert.ok(fs.existsSync(path.join(grove, 'assets', f)), `styles/grove/assets/${f} exists`);
|
|
189
|
+
const side = path.join(grove, 'assets', `${f}.json`);
|
|
190
|
+
assert.ok(fs.existsSync(side), `${f}.json sidecar beside it (named WITH the extension)`);
|
|
191
|
+
const meta = readJson(side);
|
|
192
|
+
for (const k of ['kind', 'model', 'prompt', 'encoded', 'use']) assert.ok(meta[k], `${f}.json carries ${k}`);
|
|
193
|
+
assert.match(meta.prompt, /no text/i, 'the prompt carries the text-free refusal');
|
|
194
|
+
}
|
|
195
|
+
const mock = read(path.join(grove, 'mock.html'));
|
|
196
|
+
assert.match(mock, /--plate-pod:\s*url\(\/assets\/grove-pod\.jpg\)/, 'the dark plate is a token on body, root-absolute');
|
|
197
|
+
assert.match(mock, /--plate-pod-cut:\s*url\(\/assets\/grove-pod-cut\.webp\)/, 'the light cutout is a token on body, root-absolute');
|
|
198
|
+
assert.ok(!/<img\b/i.test(mock), 'the plate is a token, not an <img>');
|
|
199
|
+
const design = read(path.join(grove, 'DESIGN.md'));
|
|
200
|
+
assert.match(design, /grove-pod\.jpg/, 'the look\'s DESIGN.md names its specimen\'s plate instead of claiming it brings none');
|
|
201
|
+
});
|
|
202
|
+
|
|
151
203
|
test('every in-house SKILL.md opens with the PLATFORM PREAMBLE, byte-identical to PREAMBLE.md, exactly once, right after its frontmatter', () => {
|
|
152
204
|
const pre = read(path.join(SKILLS, 'PREAMBLE.md')).trimEnd();
|
|
153
205
|
for (const name of ownDirs) {
|