A flat-YAML task tracker that is just files. No daemon, no database, no git hooks. diarie.dev
issue-tracker cli nodejs agent-friendly
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(diarie): brand stamp generator → brand-dist/ build output (opentype.js@^2.0.0)

Turn the designer's rough update-stamp.mjs sketch into a hardened, runnable
build step, and give it a build output to write into instead of the pristine
source.

- Rename update-stamp.mjs → .js (inherits the cliFiles eslint override; shebang
dropped). Harden the .dnr splice: assert EXACTLY one match (zero OR many both
throw, naming the count) and replace via a function replacer so `$`/`$&` in
the date/version can't be treated as replacement patterns. CWD-safe paths via
import.meta.url; Buffer→ArrayBuffer slice avoids the Node pool-slice trap.
- The generator now stamps brand-dist/index.html, NOT source brand/index.html —
the committed source stamp is the designer's bespoke-pipeline artifact and
stays untouched. Verified via Playwright that the opentype.js reproduction is
pixel-identical to the bespoke stamp, so it is safe as the deploy-time source
of truth.
- opentype.js@^2.0.0 (not ^1.3.5): proven byte-identical to 1.3.5 for our
getPath().toPathData(1) usage, even under issue #724's own repro conditions —
and 2.x is the maintained line. @types/opentype.js@^1.3.10 (only version
published; covers our usage on both runtimes).
- brand/dist-copy.js (brand:copy): reset brand-dist/ to a clean snapshot of the
deployable source allowlist (index.html, og.png, fonts/). brand:build =
run-s brand:copy brand:stamp.
- brand-dist/ gitignored (a deliberate lint-scope exclusion — it is generated).
- tsc now typechecks brand/ (was outside the include by scaffolding default);
knip traces the new entrypoints. Malformed-input splice test (5 cases).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

+240 -71
+6
.gitignore
··· 24 24 .impeccable/ 25 25 .cache/ 26 26 27 + # The brand deploy build output. `brand:build` regenerates it (copy of the deployable source + 28 + # freshly generated stamp + favicons); CI builds it and deploys it to diarie.dev via pages.yml. 29 + # A DELIBERATE lint-scope exclusion per the warning above: it is generated, so linting the copies 30 + # would be noise — but that means a lint rule cannot see it, by design. 31 + brand-dist/ 32 + 27 33 # Local upstream-friction notes against the templates diarie was scaffolded from 28 34 # (contribute-back opportunities). Personal working notes — never committed. 29 35 UPSTREAM-*.md
+29
brand/dist-copy.js
··· 1 + /** 2 + * dist-copy.js — reset `brand-dist/` to a clean snapshot of the deployable 3 + * source, ready for the stamp + favicon generators to write into. 4 + * 5 + * A BUILD step (`brand:copy`), the first stage of `brand:build`. It copies 6 + * only the ALLOWLIST below — the files diarie.dev actually serves — so the 7 + * design docs, the brand book, and the build tooling never leak into the 8 + * deploy. `cp` throws if a listed source is missing (loud, per the founding 9 + * thesis — a missing asset must never silently produce an empty deploy). 10 + */ 11 + import { cp, mkdir, rm } from 'node:fs/promises'; 12 + import { fileURLToPath } from 'node:url'; 13 + 14 + const SRC = new URL('./', import.meta.url); // brand/ 15 + const DIST = new URL('../brand-dist/', import.meta.url); // repo-root sibling 16 + 17 + // The deployable set (diarie.dev root). CNAME/.nojekyll join this once the 18 + // deploy workflow lands; generated favicons are written by brand:favicon. 19 + const ASSETS = ['index.html', 'og.png', 'fonts']; 20 + 21 + await rm(DIST, { recursive: true, force: true }); 22 + await mkdir(DIST, { recursive: true }); 23 + 24 + for (const name of ASSETS) { 25 + await cp(new URL(name, SRC), new URL(name, DIST), { recursive: true }); 26 + console.log(`copied ${name} → brand-dist/`); 27 + } 28 + 29 + console.log(`brand-dist/ ready at ${fileURLToPath(DIST)}`);
+125
brand/update-stamp.js
··· 1 + /** 2 + * update-stamp.js — regenerate the INKOM footer stamp with the current 3 + * date and package version, as outlined SVG paths (no fonts at runtime, 4 + * no rasterizer stair-stepping — see HANDOFF.md, "rotated text"). 5 + * 6 + * A BUILD step: it stamps the deploy output `brand-dist/index.html`, NOT the 7 + * pristine source `brand/index.html` (whose stamp is the designer's bespoke 8 + * artifact). Run after `brand:copy` has populated brand-dist/. Part of 9 + * `brand:build`; never part of the local gate. Usage: 10 + * npm run brand:build # copy + stamp + favicons 11 + * STAMP_DATE=2026-08-01 STAMP_VERSION=1.0.0 npm run brand:stamp # stamp only 12 + * 13 + * Fragment Mono is monospace, so plain glyph outlining + letter-spacing is 14 + * shaping-complete — no HarfBuzz needed on this side of the pipeline. 15 + */ 16 + import { readFile, writeFile } from 'node:fs/promises'; 17 + import { fileURLToPath } from 'node:url'; 18 + import opentype from 'opentype.js'; 19 + 20 + // ---- stamp geometry: keep in sync with the values in HANDOFF.md ---------- 21 + const SIZE = 12; // px 22 + const TRACK = 0.08; // em, letter-spacing 23 + const PAD = 14; // px, inner padding 24 + const H = 46; // px, rect height 25 + const ROT = -4; // deg, baked into the geometry 26 + 27 + // Paths are resolved relative to THIS file, not the caller's cwd — so the 28 + // script works from `npm run`, a bare `node brand/update-stamp.js`, or any 29 + // other cwd. The font is read from pristine source (brand/fonts/); the stamp 30 + // is written into the BUILD output (brand-dist/, a sibling of brand/). 31 + const FONT_URL = new URL('fonts/FragmentMono-Regular.ttf', import.meta.url); 32 + const PACKAGE_URL = new URL('../package.json', import.meta.url); 33 + const TARGET_URLS = [ 34 + new URL('../brand-dist/index.html', import.meta.url), 35 + ]; 36 + 37 + // The stamp is the only `.dnr` svg on a page. Global flag so a stray second 38 + // stamp is COUNTED (and rejected) rather than silently ignored. 39 + const DNR_RE = /<svg class="dnr"[\s\S]*?<\/svg>/g; 40 + 41 + /** 42 + * Replace the single `.dnr` stamp SVG in a built page. Throws unless EXACTLY 43 + * one stamp is present: zero means the page structure drifted and the stamp 44 + * would be dropped silently; more than one means an ambiguous replace that 45 + * would clobber more than intended. Uses a function replacer so `$`/`$&` in 46 + * the interpolated date/version can never be treated as replacement patterns. 47 + * 48 + * @param {string} html - the page source 49 + * @param {string} svg - the freshly outlined stamp 50 + * @returns {string} the page with its stamp replaced 51 + */ 52 + export function spliceStamp (html, svg) { 53 + const count = (html.match(DNR_RE) ?? []).length; 54 + if (count !== 1) { 55 + throw new Error(`expected exactly one .dnr stamp, found ${count}`); 56 + } 57 + return html.replaceAll(DNR_RE, () => svg); 58 + } 59 + 60 + /** 61 + * Outline the two stamp lines and assemble the rotated SVG. 62 + * 63 + * @param {import('opentype.js').Font} font - a parsed Fragment Mono 64 + * @param {string} date - ISO date for the INKOM line 65 + * @param {string} version - package version for the dnr line 66 + * @returns {string} the stamp SVG markup 67 + */ 68 + function buildStampSvg (font, date, version) { 69 + const shape = (/** @type {string} */ text) => ({ 70 + d: font 71 + .getPath(text, 0, 0, SIZE, { kerning: true, letterSpacing: TRACK }) 72 + .toPathData(1), 73 + w: font.getAdvanceWidth(text, SIZE, { letterSpacing: TRACK }), 74 + }); 75 + 76 + const l1 = shape(`INKOM ${date}`); 77 + const l2 = shape(`dnr ${version}`); 78 + const W = Math.round(Math.max(l1.w, l2.w) + 2 * PAD); 79 + 80 + return ( 81 + `<svg class="dnr" role="img" aria-label="Received ${date}, diarienummer ${version}" ` + 82 + `width="${W + 18}" height="${H + 16}" viewBox="-9 -8 ${W + 18} ${H + 16}" overflow="visible">` + 83 + `<g transform="rotate(${ROT} ${Math.round(W / 2)} ${Math.round(H / 2)})">` + 84 + `<rect x="0" y="0" width="${W}" height="${H}" fill="none" stroke="var(--st-line)" stroke-width="1.5"/>` + 85 + `<line x1="6" y1="${H / 2}" x2="${W - 6}" y2="${H / 2}" stroke="var(--st-line)" stroke-width="1"/>` + 86 + `<path fill="var(--st-text)" transform="translate(${PAD},18)" d="${l1.d}"/>` + 87 + `<path fill="var(--st-text)" transform="translate(${PAD},${H - 8})" d="${l2.d}"/>` + 88 + '</g></svg>' 89 + ); 90 + } 91 + 92 + async function main () { 93 + const date = process.env['STAMP_DATE'] ?? new Date().toISOString().slice(0, 10); 94 + const version = 95 + process.env['STAMP_VERSION'] ?? 96 + JSON.parse(await readFile(PACKAGE_URL, 'utf8')).version; 97 + 98 + // Slice to a correctly-bounded ArrayBuffer: a Node Buffer's `.buffer` can be 99 + // a shared pool larger than the file, so pass only this file's bytes. 100 + const fontBuf = await readFile(FONT_URL); 101 + const font = opentype.parse( 102 + fontBuf.buffer.slice(fontBuf.byteOffset, fontBuf.byteOffset + fontBuf.byteLength) 103 + ); 104 + 105 + const svg = buildStampSvg(font, date, version); 106 + 107 + for (const url of TARGET_URLS) { 108 + const file = fileURLToPath(url); 109 + const html = await readFile(url, 'utf8'); 110 + let stamped; 111 + try { 112 + stamped = spliceStamp(html, svg); 113 + } catch (err) { 114 + throw new Error(`${file}: ${/** @type {Error} */ (err).message}`, { cause: err }); 115 + } 116 + await writeFile(url, stamped); 117 + console.log(`${file}: stamped INKOM ${date} · dnr ${version}`); 118 + } 119 + } 120 + 121 + // Run only when executed directly — importing the module (e.g. from a test) 122 + // must not read fonts or write HTML. 123 + if (process.argv[1] === fileURLToPath(import.meta.url)) { 124 + await main(); 125 + }
-66
brand/update-stamp.mjs
··· 1 - #!/usr/bin/env node 2 - /** 3 - * update-stamp.mjs — regenerate the INKOM footer stamp with the current 4 - * date and package version, as outlined SVG paths (no fonts at runtime, 5 - * no rasterizer stair-stepping — see HANDOFF.md, "rotated text"). 6 - * 7 - * Usage: 8 - * node update-stamp.mjs # today + version from package.json 9 - * STAMP_DATE=2026-08-01 STAMP_VERSION=1.0.0 node update-stamp.mjs 10 - * 11 - * CI: run before deploy / in the release workflow, e.g. as an npm 12 - * "version" lifecycle hook so the stamp always matches the tagged release: 13 - * "scripts": { "version": "node update-stamp.mjs && git add index.html brand-book.html" } 14 - * 15 - * Dependency: npm i -D opentype.js 16 - * (Fragment Mono is monospace, so plain glyph outlining + letter-spacing is 17 - * shaping-complete — no HarfBuzz needed on this side of the pipeline.) 18 - */ 19 - import { readFile, writeFile } from 'node:fs/promises'; 20 - import opentype from 'opentype.js'; 21 - 22 - // ---- stamp geometry: keep in sync with the values in HANDOFF.md ---------- 23 - const SIZE = 12; // px 24 - const TRACK = 0.08; // em, letter-spacing 25 - const PAD = 14; // px, inner padding 26 - const H = 46; // px, rect height 27 - const ROT = -4; // deg, baked into the geometry 28 - const FONT = 'fonts/FragmentMono-Regular.ttf'; 29 - const TARGETS = ['index.html', 'brand-book.html']; 30 - 31 - const date = 32 - process.env.STAMP_DATE ?? new Date().toISOString().slice(0, 10); 33 - const version = 34 - process.env.STAMP_VERSION ?? 35 - JSON.parse(await readFile('package.json', 'utf8')).version; 36 - 37 - const font = opentype.parse((await readFile(FONT)).buffer); 38 - const shape = (text) => ({ 39 - d: font 40 - .getPath(text, 0, 0, SIZE, { kerning: true, letterSpacing: TRACK }) 41 - .toPathData(1), 42 - w: font.getAdvanceWidth(text, SIZE, { letterSpacing: TRACK }), 43 - }); 44 - 45 - const l1 = shape(`INKOM ${date}`); 46 - const l2 = shape(`dnr ${version}`); 47 - const W = Math.round(Math.max(l1.w, l2.w) + 2 * PAD); 48 - 49 - const svg = 50 - `<svg class="dnr" role="img" aria-label="Received ${date}, diarienummer ${version}" ` + 51 - `width="${W + 18}" height="${H + 16}" viewBox="-9 -8 ${W + 18} ${H + 16}" overflow="visible">` + 52 - `<g transform="rotate(${ROT} ${Math.round(W / 2)} ${Math.round(H / 2)})">` + 53 - `<rect x="0" y="0" width="${W}" height="${H}" fill="none" stroke="var(--st-line)" stroke-width="1.5"/>` + 54 - `<line x1="6" y1="${H / 2}" x2="${W - 6}" y2="${H / 2}" stroke="var(--st-line)" stroke-width="1"/>` + 55 - `<path fill="var(--st-text)" transform="translate(${PAD},18)" d="${l1.d}"/>` + 56 - `<path fill="var(--st-text)" transform="translate(${PAD},${H - 8})" d="${l2.d}"/>` + 57 - `</g></svg>`; 58 - 59 - // ---- splice into the built pages (the stamp is the only .dnr svg) -------- 60 - const RE = /<svg class="dnr"[\s\S]*?<\/svg>/; 61 - for (const file of TARGETS) { 62 - const html = await readFile(file, 'utf8'); 63 - if (!RE.test(html)) throw new Error(`${file}: no .dnr stamp found`); 64 - await writeFile(file, html.replace(RE, svg)); 65 - console.log(`${file}: stamped INKOM ${date} · dnr ${version} (${W}×${H})`); 66 - }
+2 -2
knip.jsonc
··· 1 1 { 2 2 "$schema": "https://unpkg.com/knip@5/schema.json", 3 3 4 - "entry": ["lib/index.js", "lib/schema.js"], 5 - "project": ["cli.js", "lib/**/*.js"], 4 + "entry": ["lib/index.js", "lib/schema.js", "brand/update-stamp.js", "brand/dist-copy.js"], 5 + "project": ["cli.js", "lib/**/*.js", "brand/*.js"], 6 6 7 7 // `@planned` marks an export that is deliberately kept without a caller. It must SAY WHY in the 8 8 // JSDoc — a silenced knip warning that implies somebody still needs the export is worse than no
+7 -2
package.json
··· 47 47 "lib/**/*.d.ts.map" 48 48 ], 49 49 "scripts": { 50 + "brand:build": "run-s brand:copy brand:stamp", 51 + "brand:copy": "node brand/dist-copy.js", 52 + "brand:stamp": "node brand/update-stamp.js", 50 53 "build": "npm run clean && tsc -p declaration.tsconfig.json", 51 54 "check": "run-p check:*", 52 55 "check:ast-grep": "node scripts/check-ast-grep-floor.js", ··· 63 66 "prepack": "npm run build", 64 67 "serve": "domstack-sync --server brand --watch --port ${PORT:-3334}", 65 68 "test": "run-s check test:*", 66 - "test-ci": "run-s test:*", 67 - "test:node": "node --test" 69 + "test:node": "node --test", 70 + "test-ci": "run-s test:*" 68 71 }, 69 72 "dependencies": { 70 73 "@voxpelli/typed-utils": "^5.0.0", ··· 78 81 "@domstack/sync": "^0.0.5", 79 82 "@types/js-yaml": "^4.0.9", 80 83 "@types/node": "^22.20.1", 84 + "@types/opentype.js": "^1.3.10", 81 85 "@voxpelli/eslint-config": "^25.1.0", 82 86 "@voxpelli/tsconfig": "^16.2.1", 83 87 "eslint": "^9.39.4", 84 88 "installed-check": "^10.0.1", 85 89 "knip": "^5.88.1", 86 90 "npm-run-all2": "^7.0.2", 91 + "opentype.js": "^2.0.0", 87 92 "remark-cli": "^12.0.0", 88 93 "remark-frontmatter": "^5.0.0", 89 94 "remark-gfm": "^4.0.1",
+69
test/update-stamp.spec.js
··· 1 + /** 2 + * Unit tests for the brand stamp splice. 3 + * 4 + * `spliceStamp` is the one risky part of the stamp generator: string surgery 5 + * on a built page. It is lifted out of the file's fs side-effects (importing 6 + * `brand/update-stamp.js` does not read fonts or write HTML — an entry guard 7 + * gates `main()`), so the loud-failure contract is assertable directly. 8 + * 9 + * The point of these tests is the MALFORMED input, per the repo's rule that a 10 + * refactor/splice proof needs a broken fixture — a happy-path-only test is 11 + * blind to exactly the drift this guard exists to catch. 12 + */ 13 + 14 + import assert from 'node:assert/strict'; 15 + import { describe, it } from 'node:test'; 16 + 17 + import { spliceStamp } from '../brand/update-stamp.js'; 18 + 19 + const STAMP = '<svg class="dnr" role="img" aria-label="x">…</svg>'; 20 + const page = (/** @type {string} */ body) => `<footer>\n ${body}\n</footer>\n`; 21 + 22 + describe('spliceStamp', () => { 23 + it('replaces the single .dnr stamp with the new svg', () => { 24 + const out = spliceStamp(page(STAMP), '<svg class="dnr">NEW</svg>'); 25 + assert.equal(out, page('<svg class="dnr">NEW</svg>')); 26 + }); 27 + 28 + it('inserts the replacement literally — `$&`/`$1` in the svg are not special', () => { 29 + // A function replacer means these dollar sequences land as-is instead of 30 + // being interpreted as replacement patterns (which would splice the match 31 + // back in, or an empty group). 32 + const svg = '<svg class="dnr" aria-label="dnr $& $1 $$">x</svg>'; 33 + const out = spliceStamp(page(STAMP), svg); 34 + assert.ok(out.includes('dnr $& $1 $$'), 'dollar sequences survived verbatim'); 35 + }); 36 + 37 + it('throws naming the count when NO stamp is present — never a silent no-op', () => { 38 + assert.throws( 39 + () => spliceStamp(page('<p>no stamp here</p>'), STAMP), 40 + (/** @type {unknown} */ err) => { 41 + assert.ok(err instanceof Error); 42 + assert.match(err.message, /found 0/); 43 + return true; 44 + } 45 + ); 46 + }); 47 + 48 + it('throws when MORE than one stamp is present — an ambiguous replace is rejected', () => { 49 + assert.throws( 50 + () => spliceStamp(page(`${STAMP}\n ${STAMP}`), STAMP), 51 + (/** @type {unknown} */ err) => { 52 + assert.ok(err instanceof Error); 53 + assert.match(err.message, /found 2/); 54 + return true; 55 + } 56 + ); 57 + }); 58 + 59 + it('rejects a stamp missing its closing tag — the lazy match cannot span to </svg>', () => { 60 + assert.throws( 61 + () => spliceStamp(page('<svg class="dnr">unterminated'), STAMP), 62 + (/** @type {unknown} */ err) => { 63 + assert.ok(err instanceof Error); 64 + assert.match(err.message, /found 0/); 65 + return true; 66 + } 67 + ); 68 + }); 69 + });
+2 -1
tsconfig.json
··· 5 5 ], 6 6 "include": [ 7 7 "lib/**/*", 8 - "test/**/*" 8 + "test/**/*", 9 + "brand/**/*" 9 10 ], 10 11 "compilerOptions": { 11 12 "types": [