@bongos/core 1.24.56 → 1.24.57

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/bongos/package-core.js",
5
- "core_version": "1.24.56",
6
- "core_contract": "1.24.56",
7
- "source_commit": "b301b90b209d27181527aad7387781baf8948fb5",
5
+ "core_version": "1.24.57",
6
+ "core_contract": "1.24.57",
7
+ "source_commit": "44d3648a8e5f49130f5b03eae054a58469a673b6",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-11-07T13:32:48.529Z",
9
+ "built_at": "2026-11-07T14:00:36.333Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 637,
13
13
  "agent_docs_stubbed": 32,
14
- "functional_verbatim": 3894,
14
+ "functional_verbatim": 3899,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 4564,
20
- "tree_sha256": "0a4b526c24767189ede807ca92148bfab49737f776fa48f3e1567a4ab76ea900",
19
+ "file_count": 4569,
20
+ "tree_sha256": "adb31b4e84d4eec610bda1bf4acabaea7bec731c264105eea81c5fbeb9a20728",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -3002,7 +3002,7 @@
3002
3002
  {
3003
3003
  "path": "docs/module-api-changelog.md",
3004
3004
  "mode": "0000644",
3005
- "sha256": "6c9886c818c06b89e72464ca0c30c74f8f228c5dbbdd73f9b21b8d12df73aaaa"
3005
+ "sha256": "780027478174931510aba4ef73db541e20a10227b036f5edff87c43b3d2f47f6"
3006
3006
  },
3007
3007
  {
3008
3008
  "path": "docs/modules-contract.md",
@@ -11892,12 +11892,12 @@
11892
11892
  {
11893
11893
  "path": "package-lock.json",
11894
11894
  "mode": "0000644",
11895
- "sha256": "211020df9c415bec7b3202e2e2adf83e1156421747acb7d50d5146481b82f7bb"
11895
+ "sha256": "37727eb36eab08c102913443e700669b003d0df27b2f9fcc092812ad49f63d65"
11896
11896
  },
11897
11897
  {
11898
11898
  "path": "package.json",
11899
11899
  "mode": "0000644",
11900
- "sha256": "a6d5ee484135d66f225d0b135d82ea3369743fa50d208601ef0851ecb8cb96f3"
11900
+ "sha256": "62583c855b89326986ebc92dbc8c8747539c95544cdc1520156db7be2b42674a"
11901
11901
  },
11902
11902
  {
11903
11903
  "path": "public-docs/index.html",
@@ -11917,7 +11917,7 @@
11917
11917
  {
11918
11918
  "path": "release-notes.json",
11919
11919
  "mode": "0000644",
11920
- "sha256": "432bafc0cee051c7358d2a75e2f806549fb2cb3d24b9144e1f55aa301029fadf"
11920
+ "sha256": "32c9fa02b48190ec780fcb6b24fca04cbb96a1d31c48256dface2f8d9c57ab22"
11921
11921
  },
11922
11922
  {
11923
11923
  "path": "scripts/bongos/CLAUDE.md",
@@ -12254,6 +12254,11 @@
12254
12254
  "mode": "0000644",
12255
12255
  "sha256": "d9d2e908d8c254ca62e497ebc403ddc7afb27558dfa3dcc317af1c2e29ca87a1"
12256
12256
  },
12257
+ {
12258
+ "path": "scripts/bongos/css-literal-scan.js",
12259
+ "mode": "0000644",
12260
+ "sha256": "8da46806402a840192f29f93d98afd6b5018715f6fd4ce6fbe060e23ffc977de"
12261
+ },
12257
12262
  {
12258
12263
  "path": "scripts/bongos/cutover-codemod.js",
12259
12264
  "mode": "0000644",
@@ -12284,6 +12289,11 @@
12284
12289
  "mode": "0000755",
12285
12290
  "sha256": "3de9084b1c059adf6a28734dc65a1d2823a6b6efcfd5ef18faa9e38679137893"
12286
12291
  },
12292
+ {
12293
+ "path": "scripts/bongos/design-generators-fresh.js",
12294
+ "mode": "0000644",
12295
+ "sha256": "f417e0ffcc884b018748a7f17062f52a6f247b8364ea4471efc97dd476143db3"
12296
+ },
12287
12297
  {
12288
12298
  "path": "scripts/bongos/design-sync.js",
12289
12299
  "mode": "0000644",
@@ -12442,12 +12452,12 @@
12442
12452
  {
12443
12453
  "path": "scripts/bongos/fitness-ratchets.js",
12444
12454
  "mode": "0000644",
12445
- "sha256": "29c8c99a1380a0df741338feef8cc0465e22b12c45ba7655584a6a24e48987ac"
12455
+ "sha256": "be9e886a0fc9b2473c01667930cb6854b84d2b493a974ccb1682ae1c7f74c150"
12446
12456
  },
12447
12457
  {
12448
12458
  "path": "scripts/bongos/fitness.js",
12449
12459
  "mode": "0000644",
12450
- "sha256": "a29dbf55477243b0de3112c66802e05a8558b5a01a10f6012dce6e6f5e0877a4"
12460
+ "sha256": "a23aac093727185157bb3b6fafae5f33258a2b73a0f63969754dc6326b8952e2"
12451
12461
  },
12452
12462
  {
12453
12463
  "path": "scripts/bongos/forwarder.js",
@@ -13934,6 +13944,11 @@
13934
13944
  "mode": "0000644",
13935
13945
  "sha256": "a56ec4614506eb7601aa097b04c74ed4ac1593f422c41936a2bce0f890c501e4"
13936
13946
  },
13947
+ {
13948
+ "path": "scripts/gds/css-literal-scan.js",
13949
+ "mode": "0000644",
13950
+ "sha256": "8ec8b350129f2f5084722331d92e15ceec1902868681d88c639fc1991c388749"
13951
+ },
13937
13952
  {
13938
13953
  "path": "scripts/gds/cutover-codemod.js",
13939
13954
  "mode": "0000644",
@@ -13964,6 +13979,11 @@
13964
13979
  "mode": "0000755",
13965
13980
  "sha256": "d72b0c536684c020934095172288b44341a22af833073c0ff53b7205dc8e6c67"
13966
13981
  },
13982
+ {
13983
+ "path": "scripts/gds/design-generators-fresh.js",
13984
+ "mode": "0000644",
13985
+ "sha256": "c2a29ede8cc98a51321b1bf5ff0867cf6f9d83184631cbd15edbac05cc88d5a1"
13986
+ },
13967
13987
  {
13968
13988
  "path": "scripts/gds/design-sync.js",
13969
13989
  "mode": "0000644",
@@ -15507,7 +15527,7 @@
15507
15527
  {
15508
15528
  "path": "src/bongos/module-scope-map.js",
15509
15529
  "mode": "0000644",
15510
- "sha256": "d6105d4513257ed6895937bae4e3d8d3ca22f37763ad0361871de575c5a44ba6"
15530
+ "sha256": "d9116cce5cc12918469c44c99a721da789c54b011c2d461e2b2a662aa53e42fc"
15511
15531
  },
15512
15532
  {
15513
15533
  "path": "src/bongos/module-store.js",
@@ -15852,7 +15872,7 @@
15852
15872
  {
15853
15873
  "path": "src/module-api.js",
15854
15874
  "mode": "0000644",
15855
- "sha256": "e0299fc4bd1f20a6e57b3e7447211ce655a8e645578e792bd816ddb39124a366"
15875
+ "sha256": "8995b2b62fbd8e2b5104180a8deb25dd69963c37d66c1b27ceb41adf3c4a3d31"
15856
15876
  },
15857
15877
  {
15858
15878
  "path": "src/module-loader/catalog.js",
@@ -17379,6 +17399,11 @@
17379
17399
  "mode": "0000644",
17380
17400
  "sha256": "7a492f806c867e71e52a9d90b3c0b30d47327c83d95e989ad09377c958457dd5"
17381
17401
  },
17402
+ {
17403
+ "path": "tests/design_counts_ratchet.mjs",
17404
+ "mode": "0000644",
17405
+ "sha256": "99d6c3b98b6ec839fe4b4856ab081bc6b19e96a39b4db4be0c155eb2e09e4ed0"
17406
+ },
17382
17407
  {
17383
17408
  "path": "tests/design_reference.mjs",
17384
17409
  "mode": "0000644",
@@ -3366,5 +3366,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3366
3366
  landed since 1.24.54 with no explicit bump. run 42375689581. (task 1002620)
3367
3367
  1.24.56 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3368
3368
  landed since 1.24.55 with no explicit bump. run 42377334765. (task 1002620)
3369
+ 1.24.57 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3370
+ landed since 1.24.56 with no explicit bump. run 42379442367. (task 1002620)
3369
3371
  ---------------------------------------------------------------------------
3370
3372
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.56",
3
+ "version": "1.24.57",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.56",
9
+ "version": "1.24.57",
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.24.56",
3
+ "version": "1.24.57",
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",
@@ -11307,5 +11307,11 @@
11307
11307
  "id": "1005087",
11308
11308
  "text": "The Studio, the thinking sky, the tweak editor, the city, approvals, the atlas and the welcome screen now take their colours and fonts from the project's own branding, so a re-branded project sees them in its own colours; the"
11309
11309
  }
11310
+ ],
11311
+ "1.24.57": [
11312
+ {
11313
+ "id": "1005093",
11314
+ "text": "Stylesheets can no longer gain new typed-in colours, text sizes, corners, shadows or breakpoints without the build failing, and the public design reference must be regenerated whenever its sources change."
11315
+ }
11310
11316
  ]
11311
11317
  }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // css-literal-scan.js — the detector behind the per-stylesheet DESIGN COUNTS
4
+ // ratchet in fitness-ratchets.js (task 1005093, goal 1000138 "one Cloud Bongos
5
+ // design system"; DESIGN.md, the Fifteen Rule and the --ds-* scales).
6
+ //
7
+ // WHAT IT COUNTS, per tracked stylesheet, in five kinds:
8
+ // colours — every typed-in colour: a hex (#abc, #aabbcc, #aabbccdd) or a
9
+ // colour function (rgb/rgba/hsl/hsla/hwb/lab/lch/oklab/oklch).
10
+ // Counted per OCCURRENCE. The one literal the Fifteen Rule allows
11
+ // — black or white with alpha as a scrim/shadow/halo
12
+ // (rgba(0,0,0,.4), rgb(255 255 255 / .2)) — is not counted.
13
+ // Named colours (red, white…) are not detected.
14
+ // font_sizes — font-size / font declarations whose value carries a typed-in
15
+ // non-zero length (px, rem, em, pt, %, vw…) instead of a token.
16
+ // radii — *-radius declarations with a typed-in non-zero length.
17
+ // shadows — box-shadow / text-shadow declarations that are not none/inherit
18
+ // and carry a typed-in length or colour.
19
+ // breakpoints — @media / @container rules with a typed-in length (a custom
20
+ // property cannot be used in a media query, so these never become
21
+ // tokens; the count stops the set of breakpoints sprawling).
22
+ // Sizes, radii, shadows and breakpoints are counted per DECLARATION / RULE.
23
+ //
24
+ // WHAT IT DOES NOT COUNT. A custom-property declaration (`--x: #123`) is a token
25
+ // DEFINITION — tokens.css and the hall's token block are exactly where literals
26
+ // belong — so its value is skipped. Page-private palettes are guarded separately
27
+ // (tests/hall_private_palettes.mjs). Custom-property NAMES are removed from a
28
+ // value before it is read (so --ds-space-12 is not a "12"), and url(...) bodies
29
+ // are emptied (a data URI is not a colour). A literal inside a var() fallback
30
+ // DOES count: a fallback is a typed-in value.
31
+ //
32
+ // Scope: every tracked *.css outside docs/ (the design mocks there are not
33
+ // served), node_modules and vendor/.
34
+ //
35
+ // Print the counts: node scripts/bongos/css-literal-scan.js [--all]
36
+
37
+ const path = require('path');
38
+ const fs = require('fs');
39
+ const { execFileSync } = require('child_process');
40
+
41
+ const ROOT = path.resolve(__dirname, '..', '..');
42
+ const KINDS = ['colours', 'font_sizes', 'radii', 'shadows', 'breakpoints'];
43
+
44
+ const EXCLUDE_RE = /^docs\/|(^|\/)(node_modules|vendor)\//;
45
+ const COMMENT_RE = /\/\*[\s\S]*?\*\//g;
46
+ const URL_RE = /url\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\)/gi;
47
+ const VAR_NAME_RE = /--[\w-]+/g;
48
+ // A declaration: a property, a colon, and a value that ends at ; or } (a selector
49
+ // such as `a:hover {` ends at `{`, so it never matches).
50
+ const DECL_RE = /(?:^|[{;\s])(-?-?[a-zA-Z][\w-]*)\s*:\s*([^;{}]+)(?=[;}])/g;
51
+ const AT_RULE_RE = /@(media|container)\b([^{;]*)\{/g;
52
+ const HEX_RE = /#[0-9a-fA-F]{3,8}(?![\w-])/g;
53
+ const COLOUR_FN_RE = /(?<![\w-])(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch)\(([^()]*)\)/gi;
54
+ const LENGTH_RE = /(?<![\w.#-])(\d*\.?\d+)(px|rem|em|pt|vw|vh|vmin|vmax|ch|ex|%)(?![\w%])/g;
55
+ const MEDIA_LENGTH_RE = /(?<![\w.-])\d*\.?\d+(px|em|rem)(?![\w])/;
56
+ const NO_SHADOW_RE = /^\s*(none|inherit|initial|unset|revert)\s*(!important)?\s*$/i;
57
+
58
+ /** True for black or white with alpha — the Fifteen Rule's one allowed literal. */
59
+ function isScrim(args) {
60
+ const parts = args.split(/[\s,/]+/).filter(Boolean);
61
+ if (parts.length !== 4) return false;
62
+ const rgb = parts.slice(0, 3).map((p) => p.toLowerCase());
63
+ const black = rgb.every((p) => p === '0' || p === '0%');
64
+ const white = rgb.every((p) => p === '255' || p === '100%');
65
+ return black || white;
66
+ }
67
+
68
+ function colourCount(value) {
69
+ let n = (value.match(HEX_RE) || []).length;
70
+ COLOUR_FN_RE.lastIndex = 0;
71
+ let m;
72
+ while ((m = COLOUR_FN_RE.exec(value))) {
73
+ const fn = m[0].slice(0, m[0].indexOf('(')).toLowerCase();
74
+ if ((fn === 'rgb' || fn === 'rgba') && isScrim(m[1])) continue;
75
+ n += 1;
76
+ }
77
+ return n;
78
+ }
79
+
80
+ function hasLength(value) {
81
+ LENGTH_RE.lastIndex = 0;
82
+ let m;
83
+ while ((m = LENGTH_RE.exec(value))) if (Number(m[1]) !== 0) return true;
84
+ return false;
85
+ }
86
+
87
+ /** The five counts for one stylesheet's text. */
88
+ function countSheet(text) {
89
+ const counts = Object.fromEntries(KINDS.map((k) => [k, 0]));
90
+ const css = String(text).replace(COMMENT_RE, ' ').replace(URL_RE, 'url()');
91
+ AT_RULE_RE.lastIndex = 0;
92
+ let m;
93
+ while ((m = AT_RULE_RE.exec(css))) if (MEDIA_LENGTH_RE.test(m[2])) counts.breakpoints += 1;
94
+ DECL_RE.lastIndex = 0;
95
+ while ((m = DECL_RE.exec(css))) {
96
+ const prop = m[1].toLowerCase();
97
+ if (prop.startsWith('--')) continue; // a token definition
98
+ const value = m[2].replace(VAR_NAME_RE, ' ');
99
+ counts.colours += colourCount(value);
100
+ if ((prop === 'font-size' || prop === 'font') && hasLength(value)) counts.font_sizes += 1;
101
+ else if (prop.endsWith('radius') && hasLength(value)) counts.radii += 1;
102
+ else if ((prop === 'box-shadow' || prop === 'text-shadow') && !NO_SHADOW_RE.test(value)
103
+ && (hasLength(value) || colourCount(value) > 0)) counts.shadows += 1;
104
+ }
105
+ return counts;
106
+ }
107
+
108
+ function trackedStylesheets(root = ROOT) {
109
+ const out = execFileSync('git', ['ls-files', '-z', '--', '*.css'], { cwd: root, encoding: 'utf8' });
110
+ return out.split('\0').filter((f) => f && !EXCLUDE_RE.test(f)).sort();
111
+ }
112
+
113
+ /**
114
+ * Counts for every tracked stylesheet: { sheets: { [path]: counts }, scanned }.
115
+ * Sheets whose five counts are all zero are omitted (a missing sheet reads as zero).
116
+ */
117
+ function scanCssLiterals({ root = ROOT, files = null, read = null } = {}) {
118
+ const list = files || trackedStylesheets(root);
119
+ const reader = read || ((f) => fs.readFileSync(path.join(root, f), 'utf8'));
120
+ const sheets = {};
121
+ let scanned = 0;
122
+ for (const f of list) {
123
+ let text;
124
+ try { text = reader(f); } catch { continue; }
125
+ scanned += 1;
126
+ const c = countSheet(text);
127
+ if (KINDS.some((k) => c[k] > 0)) sheets[f] = c;
128
+ }
129
+ return { sheets, scanned };
130
+ }
131
+
132
+ if (require.main === module) {
133
+ const { sheets, scanned } = scanCssLiterals();
134
+ const totals = Object.fromEntries(KINDS.map((k) => [k, 0]));
135
+ for (const [f, c] of Object.entries(sheets)) {
136
+ for (const k of KINDS) totals[k] += c[k];
137
+ if (process.argv.includes('--all') || KINDS.some((k) => c[k] > 0)) {
138
+ process.stdout.write(`${f} ${KINDS.map((k) => `${k}=${c[k]}`).join(' ')}\n`);
139
+ }
140
+ }
141
+ process.stdout.write(`\n${scanned} stylesheet(s) scanned · totals ${KINDS.map((k) => `${k}=${totals[k]}`).join(' ')}\n`);
142
+ }
143
+
144
+ module.exports = { scanCssLiterals, countSheet, KINDS };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ // design-generators-fresh.js — the design system's generated files are fresh
3
+ // (task 1005093, goal 1000138). Part of fitness.js's "generated artifacts fresh"
4
+ // check; kept in its own file because fitness.js sits at the 1,500-line budget.
5
+ //
6
+ // Both generated files read the shared token sheet (modules/ui-design/public/tokens.css) and the brand pack; the public
7
+ // reference also reads the hall's sheets and DESIGN.md. A stylesheet edit without a
8
+ // regeneration used to surface only as a failing unit test; this names it in fitness.
9
+ // Skipped when the ui-design module is not in the checkout. `root` is injectable for
10
+ // tests/design_counts_ratchet.mjs, which runs it against a copied tree.
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execFileSync } = require('child_process');
15
+
16
+ const { REPO_ROOT } = require('./fitness-lib.js');
17
+
18
+ function checkDesignGeneratorsFresh(root = REPO_ROOT) {
19
+ const violations = [];
20
+ for (const [script, out, task] of [
21
+ ['gen-design-tokens.js', 'config/design-tokens.neutral.json', '1005082'],
22
+ ['gen-design-reference.js', 'modules/ui-design/public/brand.html (the public /brand page)', '1005083'],
23
+ ]) {
24
+ const gen = path.join(root, 'modules', 'ui-design', 'scripts', script);
25
+ if (!fs.existsSync(gen)) continue;
26
+ try { execFileSync(process.execPath, [gen, '--check'], { cwd: root, stdio: 'pipe' }); }
27
+ catch { violations.push(`${out} is STALE — a design source (the token sheet, the brand pack, a hall sheet or DESIGN.md) changed without a regeneration. Run \`node modules/ui-design/scripts/${script}\` and commit (task ${task}).`); }
28
+ }
29
+ return violations;
30
+ }
31
+
32
+ module.exports = { checkDesignGeneratorsFresh };
@@ -27,6 +27,13 @@
27
27
  // docs-entropy scanner's three counts, reused as a library
28
28
  // source_text_only_tests — test files that only read source text and match
29
29
  // its wording, never running the code (task 1004868)
30
+ // design counts — PER STYLESHEET, the typed-in colours, font sizes, radii,
31
+ // shadows and breakpoints (task 1005093, goal 1000138; detector:
32
+ // css-literal-scan.js). Kept in their own `design_counts` object rather than
33
+ // `metrics` — ~60 sheets × 5 kinds would drown the metric line — but with the
34
+ // same verdict: higher than baseline fails, lower nudges a ratchet-down. A
35
+ // sheet with no baseline row has a baseline of zero, so a NEW sheet starts on
36
+ // the --ds-* tokens.
30
37
  //
31
38
  // The knip dead-code ratchet needs node_modules + ~seconds, so it is NOT part
32
39
  // of the in-suite check: CI runs `node scripts/bongos/fitness-ratchets.js --knip`
@@ -535,6 +542,46 @@ function skillListingMetrics() {
535
542
  }
536
543
  }
537
544
 
545
+ // ---------- design counts (task 1005093) ----------
546
+
547
+ const DESIGN_HINT = ' — use a --ds-* scale token (modules/ui-design/public/tokens.css) or one of the fifteen colour '
548
+ + 'tokens instead of a typed-in value (DESIGN.md, the Fifteen Rule). Run '
549
+ + '`node scripts/bongos/css-literal-scan.js` to see every sheet\'s counts and the rule.';
550
+
551
+ // Current per-sheet counts, or null when the scan found no stylesheet at all —
552
+ // a collapsed enumeration is a scan defect, never a flattering all-zero tree.
553
+ function designCounts() {
554
+ const { scanCssLiterals } = require('./css-literal-scan.js');
555
+ const { sheets, scanned } = scanCssLiterals();
556
+ if (!scanned) {
557
+ process.stderr.write('fitness-ratchets: design counts NOT MEASURED — the stylesheet scan enumerated ZERO files, which is a scan defect, not a clean tree\n');
558
+ return null;
559
+ }
560
+ return sheets;
561
+ }
562
+
563
+ // Pure verdict over { sheet: { kind: n } } maps. Keys starting with `_` are notes.
564
+ function compareDesignCounts(current, baseline) {
565
+ const { KINDS } = require('./css-literal-scan.js');
566
+ const violations = [];
567
+ const warnings = [];
568
+ const sheets = new Set([...Object.keys(current || {}), ...Object.keys(baseline || {})].filter((k) => !k.startsWith('_')));
569
+ for (const sheet of [...sheets].sort()) {
570
+ const cur = (current || {})[sheet] || {};
571
+ const base = (baseline || {})[sheet] || {};
572
+ for (const kind of KINDS) {
573
+ const value = cur[kind] || 0;
574
+ const floor = base[kind] || 0;
575
+ if (value > floor) {
576
+ violations.push(`design count ${kind} in ${sheet} regressed: ${value} > baseline ${floor} — fix the regression; never raise the baseline to pass${DESIGN_HINT}`);
577
+ } else if (value < floor) {
578
+ warnings.push(`ratchet-down available: design count ${kind} in ${sheet} improved to ${value} (baseline ${floor}) — lower it in config/fitness-baselines.json design_counts`);
579
+ }
580
+ }
581
+ }
582
+ return { violations, warnings };
583
+ }
584
+
538
585
  function computeMetrics({ withDocs = true } = {}) {
539
586
  const files = trackedJsFiles();
540
587
  const contents = new Map(files.map((f) => [f, readRepoFile(f)]));
@@ -560,15 +607,28 @@ function computeMetrics({ withDocs = true } = {}) {
560
607
  // computeMetrics() sweep — git ls-files plus every tracked .js plus the
561
608
  // docs-entropy scan, measured at 23.7s + 2.5s + 2.8s, i.e. ~29s of this file's
562
609
  // 29s runtime, which is what pushed it into the 90s timeout in the first place.
563
- function checkQualityRatchets({ baselinePath = BASELINE_PATH, metrics: injectedMetrics = null } = {}) {
610
+ // `designCounts` (task 1005093) is injectable the same way; when a test injects
611
+ // `metrics` alone, the stylesheet scan is skipped rather than run for real.
612
+ function checkQualityRatchets({ baselinePath = BASELINE_PATH, metrics: injectedMetrics = null, designCounts: injectedDesign = null } = {}) {
564
613
  const metrics = injectedMetrics === null ? computeMetrics() : injectedMetrics;
565
- const { metrics: baselines, error: baselineError } = readBaselines(baselinePath);
614
+ const { metrics: baselines, error: baselineError, parsed } = readBaselines(baselinePath);
566
615
  const { violations, warnings } = compareToBaselines(metrics, baselines);
616
+ const design = injectedDesign !== null ? injectedDesign : (injectedMetrics === null ? designCounts() : null);
617
+ if (design) {
618
+ const designBaseline = parsed && parsed.design_counts;
619
+ if (!baselineError && (!designBaseline || typeof designBaseline !== 'object')) {
620
+ violations.push('config/fitness-baselines.json has no "design_counts" object — the per-stylesheet design ratchet cannot verify anything, so it fails CLOSED. Bootstrap deliberately: node scripts/bongos/fitness-ratchets.js --update-baselines');
621
+ } else if (designBaseline) {
622
+ const d = compareDesignCounts(design, designBaseline);
623
+ violations.push(...d.violations);
624
+ warnings.push(...d.warnings);
625
+ }
626
+ }
567
627
  // task 1003227: an unreadable baseline file is a hard failure, not silence. Put it
568
628
  // FIRST so it is the headline rather than buried under per-metric noise.
569
629
  if (baselineError) violations.unshift(baselineError);
570
630
  return {
571
- name: 'quality ratchets — size, duplication, cycles, console, doc entropy, legacy GDS literals, source-text-only tests only tighten (task 1003129)',
631
+ name: 'quality ratchets — size, duplication, cycles, console, doc entropy, legacy GDS literals, source-text-only tests, per-stylesheet design counts only tighten (task 1003129)',
572
632
  hardFail: violations.length > 0,
573
633
  violations,
574
634
  warnings,
@@ -598,6 +658,12 @@ function main() {
598
658
  // this file cannot be eaten by a refresh either.
599
659
  const preserved = existingFile && typeof existingFile === 'object' ? { ...existingFile } : {};
600
660
  delete preserved.metrics;
661
+ // design_counts is recomputed too (task 1005093), keeping its `_` notes.
662
+ const design = designCounts();
663
+ if (design) {
664
+ const notes = Object.fromEntries(Object.entries(preserved.design_counts || {}).filter(([k]) => k.startsWith('_')));
665
+ preserved.design_counts = { ...notes, ...design };
666
+ }
601
667
  if (!preserved._comment) {
602
668
  preserved._comment = 'fitness-ratchets baselines (task 1003129): quality debt recorded at gate-adoption. Shrink-only — lower a value when the metric improves; raising one to pass CI is out of policy.';
603
669
  }
@@ -647,6 +713,7 @@ module.exports = {
647
713
  readBaselines,
648
714
  computeMetrics,
649
715
  compareToBaselines,
716
+ compareDesignCounts,
650
717
  sourceTextTestMetrics,
651
718
  duplicateWindowCount,
652
719
  normalizeLines,
@@ -200,7 +200,7 @@ function checkNestedDocsPresent() {
200
200
  // here would false-fail every PR (the gitignored whole file is absent from a clean checkout, so it
201
201
  // always reads as "would change"); `--check-block` is the exact, non-noisy gate. (docs/file-map.md +
202
202
  // the OpenAPI docs are still fully committed and unchanged by ADR 0110 — they keep the plain --check.)
203
- function checkGeneratedArtifactsFresh() {
203
+ function checkGeneratedArtifactsFresh({ designRoot = REPO_ROOT } = {}) { // designRoot: tests only (task 1005093)
204
204
  const violations = [];
205
205
  try {
206
206
  execFileSync(process.execPath, [path.join(__dirname, 'gen-repo-map.js'), '--check-block'], { cwd: REPO_ROOT, stdio: 'pipe' });
@@ -254,9 +254,10 @@ function checkGeneratedArtifactsFresh() {
254
254
  } catch {
255
255
  violations.push('docs/page-inventory.json + docs/page-inventory.md are STALE (or two pages claim one id) — a page was added, renamed, retitled or changed its scripts/states. Run `node scripts/bongos/page-inventory.js` and commit (task 1004314).');
256
256
  }
257
+ violations.push(...require('./design-generators-fresh.js').checkDesignGeneratorsFresh(designRoot)); // task 1005093
257
258
  // NB the role packs' responsibility blocks are generated too, but their freshness
258
259
  // is asserted by role-pack-guard.js — one check owns "the packs are correct".
259
- return { name: 'generated artifacts fresh', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings: [], note: 'Tracked generated content matches source: nested CLAUDE.md symbol blocks (code) + the CLAUDE.md §13 snippet (sessions) via --check-block, plus the CLAUDE.md §2 interaction-posture block (the knob defaults), docs/file-map.md skills/scheduled-tasks sections, the API spec + typed client GENERATE from the routes (both gitignored, task 1004866), docs/copy-registry.json + copy-inventory.md (the reader-facing copy, task 1003556) and docs/page-inventory.json + page-inventory.md (every page and its stable id, task 1004314). The gitignored docs/repo-map.md + docs/session-log-index.md are deploy-regenerated (ADR 0110), not gated here.' };
260
+ return { name: 'generated artifacts fresh', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings: [], note: 'Tracked generated content matches source: nested CLAUDE.md symbol blocks (code) + the CLAUDE.md §13 snippet (sessions) via --check-block, plus the CLAUDE.md §2 interaction-posture block (the knob defaults), docs/file-map.md skills/scheduled-tasks sections, the API spec + typed client GENERATE from the routes (both gitignored, task 1004866), docs/copy-registry.json + copy-inventory.md (the reader-facing copy, task 1003556) and docs/page-inventory.json + page-inventory.md (every page and its stable id, task 1004314), and the design system config/design-tokens.neutral.json + public /brand reference (task 1005093). The gitignored docs/repo-map.md + docs/session-log-index.md are deploy-regenerated (ADR 0110), not gated here.' };
260
261
  }
261
262
 
262
263
  // ---- deterministic cron routine boundary (System 4 / ADR 0078) -------------
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/css-literal-scan.js — @bongos-forwarder (task 1004848). Generated by
3
+ // scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
4
+ // The real script is scripts/bongos/css-literal-scan.js. This forwarder keeps callers that still name
5
+ // the old path working, and warns once so they get repointed. Task 1003706 removes it.
6
+ 'use strict';
7
+ require('../bongos/forwarder.js').forward(module, 'css-literal-scan.js');
@@ -0,0 +1,6 @@
1
+ // scripts/gds/design-generators-fresh.js — @bongos-forwarder (task 1004848). Generated by
2
+ // scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
3
+ // The real script is scripts/bongos/design-generators-fresh.js. This forwarder keeps callers that still name
4
+ // the old path working, and warns once so they get repointed. Task 1003706 removes it.
5
+ 'use strict';
6
+ require('../bongos/forwarder.js').forward(module, 'design-generators-fresh.js');
@@ -473,7 +473,7 @@ const MODULE_GLOBS = {
473
473
  'scripts/bongos/adr-namespace.js', 'scripts/bongos/baseline-staleness.js',
474
474
  'scripts/bongos/category-advisory-guard.js', 'scripts/bongos/client-baseurl-guard.js',
475
475
  'scripts/bongos/doc-cli-guard.js', 'scripts/bongos/doc-control-claims.js', 'scripts/bongos/doc-comment-coverage.js',
476
- 'scripts/bongos/exec-path-guard.js', 'scripts/bongos/fitness-lib.js',
476
+ 'scripts/bongos/exec-path-guard.js', 'scripts/bongos/fitness-lib.js', 'scripts/bongos/design-generators-fresh.js',
477
477
  'scripts/bongos/fitness-checks-error-envelope.js', 'scripts/bongos/fitness-checks-identity.js',
478
478
  'scripts/bongos/fitness-checks-packaging.js', 'scripts/bongos/fitness-checks-route-pins.js',
479
479
  'scripts/bongos/fitness-checks-vocabulary.js', 'scripts/bongos/fitness-checks-write-validation.js',
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.56'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.57'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,134 @@
1
+ // tests/design_counts_ratchet.mjs — design counts that can only go down, and the
2
+ // design generators wired into the fitness freshness gate (task 1005093, goal 1000138).
3
+ //
4
+ // Done when: adding a new hard-coded font-size to a hall sheet fails fitness, and
5
+ // editing the shared token sheet without regenerating fails the freshness gate.
6
+ // Both are proved here on the real tree: the first by scanning the real sheets with
7
+ // one extra declaration, the second by running the gate on a copied tree.
8
+ //
9
+ // Run: node --test tests/design_counts_ratchet.mjs
10
+
11
+ import { strict as assert } from 'node:assert';
12
+ import { test } from 'node:test';
13
+ import fs from 'node:fs';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+ import { createRequire } from 'node:module';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
+ const scan = require(path.join(ROOT, 'scripts', 'bongos', 'css-literal-scan.js'));
22
+ const ratchets = require(path.join(ROOT, 'scripts', 'bongos', 'fitness-ratchets.js'));
23
+ const fitness = require(path.join(ROOT, 'scripts', 'bongos', 'fitness.js'));
24
+ const { checkDesignGeneratorsFresh } = require(path.join(ROOT, 'scripts', 'bongos', 'design-generators-fresh.js'));
25
+
26
+ const HALL_SHEET = 'modules/hall-ui/public/style.css';
27
+ const readReal = (f) => fs.readFileSync(path.join(ROOT, f), 'utf8');
28
+
29
+ test('the detector counts typed-in values and leaves tokens alone', () => {
30
+ const c = scan.countSheet(`
31
+ :root { --ds-x: 14px; --tone: #123456; } /* token definitions: not counted */
32
+ .a { font-size: 13px; border-radius: 6px; color: #abc; }
33
+ .b { font-size: var(--ds-text-sm); border-radius: var(--ds-radius-tile); color: var(--ink); }
34
+ .c { box-shadow: 0 2px 4px rgba(0, 0, 0, .2); } /* a shadow; the black scrim is allowed */
35
+ .d { box-shadow: none; background: url("data:image/svg+xml;fill='#fff'"); }
36
+ .e { background: color-mix(in srgb, var(--ink) 40%, rgb(12 34 56)); padding: 0; }
37
+ @media (max-width: 720px) { .f { border-radius: 0; } }
38
+ @media (prefers-reduced-motion: reduce) { .g { transition: none; } }
39
+ `);
40
+ assert.deepEqual(c, { colours: 2, font_sizes: 1, radii: 1, shadows: 1, breakpoints: 1 });
41
+ });
42
+
43
+ test('a selector with a colon is not read as a declaration', () => {
44
+ assert.equal(scan.countSheet('a:hover, b:focus-visible { color: var(--ink); }').colours, 0);
45
+ });
46
+
47
+ test('the real tree matches its committed design-count baselines', () => {
48
+ const { sheets, scanned } = scan.scanCssLiterals();
49
+ assert.ok(scanned > 40, `the scan should see the live stylesheets (saw ${scanned})`);
50
+ const r = ratchets.checkQualityRatchets({ metrics: {}, designCounts: sheets });
51
+ assert.deepEqual(r.violations, [], 'design counts regressed against config/fitness-baselines.json');
52
+ });
53
+
54
+ test('a NEW hard-coded font-size in a hall sheet fails fitness', () => {
55
+ const { sheets } = scan.scanCssLiterals({
56
+ read: (f) => readReal(f) + (f === HALL_SHEET ? '\n.planted { font-size: 13px; }\n' : ''),
57
+ });
58
+ const r = ratchets.checkQualityRatchets({ metrics: {}, designCounts: sheets });
59
+ assert.equal(r.hardFail, true);
60
+ assert.ok(r.violations.some((v) => v.includes(`font_sizes in ${HALL_SHEET} regressed`)), r.violations.join('\n'));
61
+ });
62
+
63
+ test('a new sheet starts at zero: any typed-in colour in it fails', () => {
64
+ const d = ratchets.compareDesignCounts({ 'modules/x/public/new.css': { colours: 1 } }, { _comment: 'note' });
65
+ assert.equal(d.violations.length, 1);
66
+ assert.match(d.violations[0], /colours in modules\/x\/public\/new\.css regressed: 1 > baseline 0/);
67
+ });
68
+
69
+ test('a lower count nudges a ratchet-down and does not fail', () => {
70
+ const d = ratchets.compareDesignCounts({ 'a.css': { radii: 2 } }, { 'a.css': { radii: 3 } });
71
+ assert.deepEqual(d.violations, []);
72
+ assert.match(d.warnings[0], /ratchet-down available: design count radii in a\.css improved to 2/);
73
+ });
74
+
75
+ test('a baseline file with no design_counts fails closed', () => {
76
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-counts-'));
77
+ const file = path.join(dir, 'baselines.json');
78
+ fs.writeFileSync(file, JSON.stringify({ metrics: {} }));
79
+ try {
80
+ const r = ratchets.checkQualityRatchets({ baselinePath: file, metrics: {}, designCounts: {} });
81
+ assert.equal(r.hardFail, true);
82
+ assert.match(r.violations[0], /no "design_counts" object/);
83
+ } finally {
84
+ fs.rmSync(dir, { recursive: true, force: true });
85
+ }
86
+ });
87
+
88
+ // The generators resolve their sources from their own location, so a copy of the
89
+ // files they read is a complete, isolated tree the gate can run against.
90
+ const GENERATOR_SOURCES = [
91
+ 'config/branding.neutral.json',
92
+ 'config/design-tokens.neutral.json',
93
+ 'modules/ui-design/public/tokens.css',
94
+ 'modules/ui-design/public/brand.html',
95
+ 'modules/ui-design/public/brand',
96
+ 'modules/ui-design/scripts/gen-design-tokens.js',
97
+ 'modules/ui-design/scripts/gen-design-reference.js',
98
+ 'modules/hall-ui/public/style.css',
99
+ 'modules/hall-ui/public/controls.css',
100
+ 'modules/hall-ui/public/dom-utils.js',
101
+ 'scripts/bongos/gen-brand.js',
102
+ 'DESIGN.md',
103
+ ];
104
+
105
+ function copiedTree() {
106
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-fresh-'));
107
+ for (const rel of GENERATOR_SOURCES) {
108
+ fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
109
+ fs.cpSync(path.join(ROOT, rel), path.join(dir, rel), { recursive: true });
110
+ }
111
+ return dir;
112
+ }
113
+
114
+ test('the freshness gate passes on a fresh tree and fails when the token sheet changes without a regeneration', () => {
115
+ const dir = copiedTree();
116
+ try {
117
+ assert.deepEqual(checkDesignGeneratorsFresh(dir), [], 'the copied tree should start fresh');
118
+ const sheet = path.join(dir, 'modules/ui-design/public/tokens.css');
119
+ const before = fs.readFileSync(sheet, 'utf8');
120
+ const after = before.replace(/--ds-space-xs:\s*[^;]+;/, '--ds-space-xs: 11px;');
121
+ assert.notEqual(after, before, 'the token sheet should declare --ds-space-xs');
122
+ fs.writeFileSync(sheet, after);
123
+ const v = checkDesignGeneratorsFresh(dir);
124
+ assert.ok(v.some((x) => x.startsWith('config/design-tokens.neutral.json is STALE')), v.join('\n'));
125
+ assert.ok(v.some((x) => x.startsWith('modules/ui-design/public/brand.html')), v.join('\n'));
126
+ // …and the registered fitness check carries that verdict, so the build goes red.
127
+ assert.ok(fitness.CHECKS.includes(fitness.checkGeneratedArtifactsFresh));
128
+ const gate = fitness.checkGeneratedArtifactsFresh({ designRoot: dir });
129
+ assert.equal(gate.hardFail, true);
130
+ assert.ok(gate.violations.some((x) => x.startsWith('config/design-tokens.neutral.json is STALE')), gate.violations.join('\n'));
131
+ } finally {
132
+ fs.rmSync(dir, { recursive: true, force: true });
133
+ }
134
+ });