1818 * Markers look like: <!-- br:models.chatVisible -->66<!-- /br:models.chatVisible -->
1919 * and wrap the WHOLE token, so a badge URL, its alt text and the prose number
2020 * can all regenerate from one key.
21+ *
22+ * THIS COPY IS AHEAD OF THE SOURCE. blockrun's `brand-script-sync` CI job
23+ * diffs every consumer against brand/sync-brand-numbers.mjs and its printed
24+ * remediation is "copy the source over the consumer" — twice that overwrote a
25+ * fix made here (#84, #128). What this copy carries that the source does not,
26+ * as of 2026-09-13: assertRenderable + escAttr (a value from the mirror is
27+ * refused, and attribute-escaped, before it is written into a README that the
28+ * brand-sync bot then pushes unattended with contents:write), keyOf() on the
29+ * keys-in-use count, and the --check summary that does not say "up to date"
30+ * under a list of stale fenced markers. Resync source <- consumer: land THIS
31+ * file in blockrun/brand and fan it out; do not copy the source over it.
32+ * test/brand-sync-script.test.ts fails on a copy without the guard, so a
33+ * consumer <- source resync cannot pass this repo's required `test` check.
2134 */
2235import { execFileSync } from "node:child_process" ;
2336import { existsSync , lstatSync , readFileSync , writeFileSync , readdirSync } from "node:fs" ;
@@ -101,15 +114,58 @@ function flatten(obj, prefix = "") {
101114 * Renderers are registered under the FULL marker name so a badge's label is
102115 * written out rather than guessed from the key.
103116 */
117+ /**
118+ * What a brand value is allowed to be, checked at the moment it is USED.
119+ *
120+ * These values arrive over the network from blockrun.ai (or the
121+ * awesome-blockrun mirror) and are written verbatim into README.md,
122+ * CONTRIBUTING.md and skills/*\/SKILL.md, which `.github/workflows/brand-sync.yml`
123+ * then commits and pushes to the default branch weekly, unattended, with
124+ * `contents: write`. Rendering was `String(value)` and the badge renderer
125+ * interpolated straight into `src="..."` and `alt="..."`, so a value carrying
126+ * a quote or an angle bracket closed the attribute and injected markup into
127+ * every consuming repo's README. Write access to one mirror repo was enough.
128+ *
129+ * Checked here rather than over the whole artifact on purpose: the payload
130+ * legitimately carries prose fields we never render (`savings.baselineModel`
131+ * is a string), and refusing those would break the sync on an unrelated
132+ * addition upstream.
133+ */
134+ const SAFE_TEXT = / ^ [ \p{ L} \p{ N} . , % + / · — – - ] { 1 , 64 } $ / u;
135+
136+ function assertRenderable ( marker , value ) {
137+ const what = ( ) => `${ marker } = ${ JSON . stringify ( value ) } ` ;
138+ if ( typeof value === "number" ) {
139+ if ( ! Number . isFinite ( value ) ) fail ( `brand-numbers: refusing to render ${ what ( ) } — not a finite number` ) ;
140+ return value ;
141+ }
142+ if ( typeof value === "string" ) {
143+ if ( ! SAFE_TEXT . test ( value ) ) {
144+ fail (
145+ `brand-numbers: refusing to render ${ what ( ) } — a rendered value must be ` +
146+ `a number or a short plain label. This value would be written verbatim ` +
147+ `into README/CONTRIBUTING/SKILL.md and pushed by the brand-sync bot.` ,
148+ ) ;
149+ }
150+ return value ;
151+ }
152+ fail ( `brand-numbers: refusing to render ${ what ( ) } — expected a number or a string, got ${ Array . isArray ( value ) ? "an array" : typeof value } ` ) ;
153+ }
154+
155+ /** Escape for an HTML attribute. Belt to assertRenderable's braces. */
156+ const escAttr = ( v ) =>
157+ String ( v ) . replace ( / & / g, "&" ) . replace ( / < / g, "<" ) . replace ( / > / g, ">" )
158+ . replace ( / " / g, """ ) . replace ( / ' / g, "'" ) ;
159+
104160const badge = ( label ) => ( n ) =>
105- `<img src="https://img.shields.io/badge/${ label } -${ n } -5B9BF6?style=flat-square&labelColor=0B0A0F" alt="${ n } ${ label } ">` ;
161+ `<img src="https://img.shields.io/badge/${ label } -${ escAttr ( n ) } -5B9BF6?style=flat-square&labelColor=0B0A0F" alt="${ escAttr ( n ) } ${ label } ">` ;
106162
107163const RENDER = {
108164 "mcp.tools@badge" : badge ( "tools" ) ,
109165 "models.totalVisible@badge" : badge ( "models" ) ,
110166 "models.chatVisible@badge" : badge ( "models" ) ,
111167} ;
112- const render = ( marker , value ) => ( RENDER [ marker ] ?? String ) ( value ) ;
168+ const render = ( marker , value ) => ( RENDER [ marker ] ?? String ) ( assertRenderable ( marker , value ) ) ;
113169
114170/** `mcp.tools@badge` looks up `mcp.tools`. Unmodified markers are unaffected. */
115171const keyOf = ( marker ) => marker . split ( "@" ) [ 0 ] ;
@@ -308,7 +364,8 @@ const everUsed = new Set();
308364
309365for ( const file of walk ( ROOT ) ) {
310366 const { before, after, changed, used } = syncFile ( file , numbers , problems , skipped ) ;
311- used . forEach ( ( k ) => everUsed . add ( k ) ) ;
367+ // keyOf: mcp.tools and mcp.tools@badge are ONE key in use, not two.
368+ used . forEach ( ( k ) => everUsed . add ( keyOf ( k ) ) ) ;
312369 if ( ! changed ) continue ;
313370 drifted . push ( { file : relative ( ROOT , file ) , before, after } ) ;
314371 if ( ! check ) writeFileSync ( file , after ) ;
@@ -332,7 +389,16 @@ if (problems.length) {
332389
333390if ( check ) {
334391 if ( drifted . length === 0 ) {
335- console . log ( `brand-numbers: up to date (${ everUsed . size } keys in use)` ) ;
392+ // Do not say "up to date" straight after listing markers known to be
393+ // stale. The skip stays non-fatal for the reason above, but a CI log that
394+ // prints the stale ones and then declares everything current is a log
395+ // nobody reads twice.
396+ console . log (
397+ skipped . length
398+ ? `brand-numbers: no drift outside code fences (${ everUsed . size } keys in use), ` +
399+ `but ${ skipped . length } fenced marker(s) listed above are stale — add @live to sync them`
400+ : `brand-numbers: up to date (${ everUsed . size } keys in use)` ,
401+ ) ;
336402 process . exit ( 0 ) ;
337403 }
338404 console . error ( "brand-numbers: these files disagree with brand-numbers.json\n" ) ;
0 commit comments