fix(brand): de-cake the actual source, not the file it is compiled over

The About screen has been showing Cake Wallet's app icon, and the receive QR
page — the screen a user screenshots to get paid — has been showing Cake's
wordmark. In CI builds only, which is why nobody caught it.

compile_graphics.sh compiles res/pictures/*.svg into assets/new-ui/*.svg.vec,
and every platform workflow runs it. That makes res/pictures the source and
assets/new-ui the output. The August de-cake pass replaced three files under
assets/new-ui and left res/pictures untouched, so each CI build faithfully
recompiled Cake's artwork straight over the top of ours. Locally it looked
right, because locally nobody re-runs the compiler.

Three files, all of them branding: hash_squircle_icon.svg, hash-wordmark.svg,
hash-wordmark-light.svg. The sources are now the Hash Bags artwork, so the
compiler produces what the filenames have claimed since August.

This is the third time somebody else's artwork has reached users through this
repo, and the second time an audit missed it, so it gets a test rather than
another careful look: any SVG present in both res/pictures and assets/new-ui
must be byte-identical. Drift means a hand-edit that the next CI build will
silently revert. The test also asserts it compared something, because a silent
zero is how the previous two got through.

AGENTS.md has had the rule the whole time: generated files are never
hand-edited, fix the source and regenerate. The trap here is that the generated
copies sit beside the sources under identical names, and the ones the Dart code
references are the generated ones.
This commit is contained in:
jwinterm
2026-09-24 19:50:00 -04:00
parent c1f618176c
commit fff5259860
4 changed files with 77 additions and 99 deletions
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 674 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 722 B

+6 -35
View File
@@ -1,36 +1,7 @@
<svg width="125" height="125" viewBox="0 0 125 125" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="125" height="125" rx="25" fill="url(#paint0_linear_15143_228548)"/>
<g clip-path="url(#clip0_15143_228548)">
<path d="M90.3236 57.7646C89.3631 58.7221 88.5635 59.7069 88.0232 61.15C87.4829 62.5929 87.2155 64.1643 86.8307 66.6494C86.2795 70.1874 85.2043 71.5786 84.3529 72.457C82.6146 74.2492 80.145 74.1483 77.1351 73.5536C75.244 73.1799 74.7527 73.63 72.5042 75.5723L60.65 87.3076L60.6445 105.123C60.6445 105.443 61.0321 105.604 61.2586 105.377L96.2699 70.4357C96.742 69.9638 97.0093 69.3254 97.0093 68.6571L97.053 50.9995L90.3236 57.7619V57.7646Z" fill="url(#paint1_linear_15143_228548)"/>
<path d="M76.1982 68.9107C78.4795 68.9762 80.0704 70.6265 81.948 69.4426C83.8254 68.2587 83.7736 66.9412 84.1365 64.7644C84.5021 62.5847 85.2253 60.0505 85.8693 58.5229C86.516 56.9926 87.9596 55.0367 89.4277 53.6101L96.9949 45.7401L97.044 37.8101C97.044 37.3246 96.46 37.0791 96.1162 37.4228L61.7025 71.8241C61.0257 72.5006 60.6465 73.4172 60.6465 74.372V82.1791L72.2714 70.5147C72.2714 70.5147 73.9142 68.848 76.1955 68.9134L76.1982 68.9107Z" fill="url(#paint2_linear_15143_228548)"/>
<path d="M97.0549 31.3077L60.6465 67.7985V60.0077C60.6465 59.053 61.0257 58.1364 61.7025 57.4599L96.1244 23.0504C96.4655 22.7094 97.0522 22.9495 97.0522 23.435V31.3077H97.0549Z" fill="url(#paint3_linear_15143_228548)"/>
<path d="M94.253 17.4413L32.6162 30.8652C30.4085 31.3453 28.834 33.2985 28.834 35.5572V80.5482C28.834 81.2164 29.0987 81.8575 29.5735 82.3321L55.5086 108.258C56.038 108.787 56.944 108.413 56.944 107.663V59.4452C56.9467 57.8549 57.5798 56.3273 58.7041 55.2034L95.028 18.8571C95.6202 18.2652 95.0744 17.264 94.2557 17.4413H94.253Z" fill="url(#paint4_linear_15143_228548)"/>
</g>
<defs>
<linearGradient id="paint0_linear_15143_228548" x1="62.5" y1="0" x2="62.5" y2="125" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#EFEFF8"/>
</linearGradient>
<linearGradient id="paint1_linear_15143_228548" x1="96.1007" y1="61.2018" x2="60.775" y2="97.6317" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF7D38"/>
<stop offset="0.612828" stop-color="#FE6412"/>
<stop offset="1" stop-color="#EB4600"/>
</linearGradient>
<linearGradient id="paint2_linear_15143_228548" x1="88.1009" y1="51.0077" x2="59.81" y2="80.7664" gradientUnits="userSpaceOnUse">
<stop stop-color="#61C5FF"/>
<stop offset="1" stop-color="#2A92FA"/>
</linearGradient>
<linearGradient id="paint3_linear_15143_228548" x1="97.0549" y1="28.6044" x2="63.1921" y2="65.2236" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFC414"/>
<stop offset="1" stop-color="#FF9447"/>
</linearGradient>
<linearGradient id="paint4_linear_15143_228548" x1="107.46" y1="-33.3011" x2="27.0857" y2="100.988" gradientUnits="userSpaceOnUse">
<stop offset="0.105" stop-color="#776A83"/>
<stop offset="0.568693" stop-color="#312938"/>
<stop offset="0.960073" stop-color="#19151D"/>
</linearGradient>
<clipPath id="clip0_15143_228548">
<rect width="68.2373" height="91.1865" fill="white" transform="translate(28.834 17.334)"/>
</clipPath>
</defs>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<title>Hash Bags app icon</title>
<desc>Hash Bags app icon: brand-green rounded square with a centered white hash.</desc>
<rect width="512" height="512" rx="112" fill="#1a5c38"/>
<text x="256" y="390" font-family="ui-monospace, 'JetBrains Mono', 'Courier New', monospace" font-size="380" font-weight="700" fill="#ffffff" text-anchor="middle">#</text>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 485 B

+57
View File
@@ -0,0 +1,57 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
// compile_graphics.sh compiles res/pictures/*.svg into assets/new-ui/*.svg.vec,
// and CI runs it on every platform. That makes res/pictures the source and
// assets/new-ui the output, which is easy to get backwards: the files sit side
// by side, have identical names, and the ones under assets/ are the ones the
// Dart code names.
//
// Getting it backwards shipped Cake Wallet's artwork twice. The August de-cake
// pass replaced the copies under assets/new-ui and left res/pictures untouched,
// so every CI build recompiled Cake's icon over the top: their app icon on the
// About screen, and their wordmark on the receive QR page, which is the screen
// a user screenshots to get paid. A local build looked right, because locally
// nobody re-runs the compiler.
//
// So: any SVG present in both places must be byte-identical. If they drift, the
// one under assets/ is a hand-edit that the next CI build will silently revert.
void main() {
test('brand SVGs under assets/new-ui match their res/pictures source', () {
final assetsDir = Directory('assets/new-ui');
final sourceDir = Directory('res/pictures');
expect(assetsDir.existsSync(), isTrue, reason: 'no assets/new-ui — wrong CWD?');
expect(sourceDir.existsSync(), isTrue, reason: 'no res/pictures — wrong CWD?');
final drifted = <String>[];
var compared = 0;
for (final f in assetsDir.listSync().whereType<File>()) {
if (!f.path.endsWith('.svg')) continue;
final name = f.uri.pathSegments.last;
final source = File('${sourceDir.path}/$name');
if (!source.existsSync()) continue; // not compiled from res/pictures
compared++;
final a = f.readAsBytesSync();
final b = source.readAsBytesSync();
if (a.length != b.length || !_same(a, b)) {
drifted.add(
'$name: assets/new-ui is ${a.length}b, res/pictures is ${b.length}b. '
'Edit res/pictures and re-run ./compile_graphics.sh; the assets copy '
'is output and CI overwrites it.',
);
}
}
// A silent zero would make this test vacuous, which is how the last two
// branding regressions reached users.
expect(compared, greaterThan(0), reason: 'compared no SVG pairs at all');
expect(drifted, isEmpty, reason: 'generated brand assets drifted from source:\n${drifted.join('\n')}');
});
}
bool _same(List<int> a, List<int> b) {
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}