whats-new drafts from player-facing notes first (whatsNew.source, docs/RELEASE-<version>.md, the CHANGELOG section of the version or Unreleased; --release, --from-git) and only then from the git log; internal jargon warnings (SDK / vendor names, developer and version-control words, file paths, issue ids, code identifiers) with plain alternatives in the draft and whats-new check, one list in cli/lib/jargon.ts, whatsNew.allowWords keeps a word; skill: write from what players feel, never from commit messages; tests

This commit is contained in:
CRTkafa 2026-10-14 14:45:19 +08:00
parent e9507d8884
commit bab3f63227
9 changed files with 287 additions and 21 deletions

View file

@ -206,7 +206,7 @@ describe('whats-new', () => {
expect([...formatNotes(many, { limit: 500 })].length).toBeLessThanOrEqual(500);
expect(formatNotes([], { limit: 500 })).toBe('Bug fixes and performance improvements.');
expect(checkNotes('x'.repeat(501), 'play')[0]).toContain('501 > 500');
expect(checkNotes('bump version', 'apple')[0]).toContain('commit log');
expect(checkNotes('bump version', 'apple')[0]).toBe('internal jargon (developer word): "bump"; instead: "update"');
});
test('writes into both listing files, keeping them parseable', () => {
const play = readFileSync(join(T, 'listing.md'), 'utf8');

View file

@ -0,0 +1,75 @@
// Release notes in player language: a player-facing source beats the git log, and internal jargon is flagged.
import { afterEach, describe, expect, test } from 'bun:test';
import { jargonFindings, jargonMessage } from '../cli/lib/jargon';
import { changelogSection, checkLanguageNotes, cutNotes, findNotesSource, notesFromMarkdown } from '../cli/lib/whats-new';
import { run as whatsNew } from '../cli/whats-new';
import { BASE, capture, jsonOut, project, restore } from './helpers/store-mock';
afterEach(restore);
const words = (t: string, allow?: string[]) => jargonFindings(t, allow).map(f => `${f.kind}:${f.word}`);
describe('internal jargon', () => {
test('vendors, dev and version-control words, paths, issue ids and code identifiers', () => {
expect(words('Added Umami telemetry and a Keeper OS fix')).toEqual(['vendor:Umami', 'dev:telemetry', 'vendor:Keeper OS']);
expect(words('Sentry, Firebase, Capacitor, Tauri, Jenkins and Forgejo')).toEqual(['vendor:Sentry', 'vendor:Firebase', 'vendor:Capacitor', 'vendor:Tauri', 'vendor:Jenkins', 'vendor:Forgejo']);
expect(words('refactor the endpoint payload via the API in CI, PR merged, commit bump of deps, OTA fingerprint'))
.toEqual(['dev:refactor', 'dev:endpoint', 'dev:payload', 'dev:API', 'dev:CI', 'dev:PR', 'vcs:merged', 'vcs:commit', 'dev:bump', 'dev:deps', 'dev:OTA', 'dev:fingerprint']);
expect(words('Fixed src/game/save.ts and Player.gd')).toEqual(['path:src/game/save.ts', 'path:Player.gd']);
expect(words('Fixes #123 and GAME-42')).toEqual(['issue:#123', 'issue:GAME-42']);
expect(words('saveSlot now works, as does max_lives')).toEqual(['identifier:saveSlot', 'identifier:max_lives']);
});
test('player language passes: product names, links, dates, plain words', () => {
expect(words('New: 20 desert levels on iPhone and iPad! Faster loading. Help: https://example.com/help/api_v2 on 12/10.')).toEqual([]);
expect(words('Capital letters, a pricey item, a picnic and the kick-off')).toEqual([]); // no "api" / "ci" inside words
});
test('allowWords keeps a word, any case; messages name the word and a plain alternative', () => {
expect(words('New Firebase level and saveSlot', ['firebase', 'SAVESLOT'])).toEqual([]);
expect(jargonMessage(jargonFindings('endpoint')[0]!)).toBe('internal jargon (developer word): "endpoint"; instead: "server"');
expect(jargonMessage(jargonFindings('Umami')[0]!)).toBe('internal jargon (SDK / vendor name): "Umami"; instead: drop the name and say what changed for the player');
expect(jargonMessage(jargonFindings('merge')[0]!)).toBe('internal jargon (reads like a commit log): "merge"; drop it');
});
test('language check: jargon is a warning, never a blocking problem', () => {
const c = checkLanguageNotes({ id: 'en', apple: 'en-US' }, { lang: 'en', source: 'en', status: 'source', text: 'Telemetry via Umami.' });
expect(c.problems).toEqual([]);
expect(c.warnings).toHaveLength(2);
expect(checkLanguageNotes({ id: 'en', apple: 'en-US' }, { lang: 'en', source: 'en', status: 'source', text: 'Telemetry via Umami.' }, undefined, ['telemetry', 'umami']).warnings).toEqual([]);
});
});
describe('player-facing sources', () => {
const CL = '# Changelog\n\n## Unreleased\n\n- **Night mode** for the map.\n\n## [1.2.0] - 2026-10-01\n\nBig update.\n\n- New `desert` world, see [the trailer](https://example.com).\n\n## 1.1.0\n\n- Old.\n';
test('changelog sections, markdown to text, cuts', () => {
expect(changelogSection(CL, '1.2.0')).toContain('New `desert` world');
expect(changelogSection(CL, 'v1.1.0')).toBe('- Old.');
expect(changelogSection(CL)).toBe('- **Night mode** for the map.');
expect(changelogSection(CL, '9.9.9')).toBeUndefined();
expect(notesFromMarkdown(changelogSection(CL, '1.2.0')!)).toBe('Big update.\n\n• New desert world, see the trailer.');
expect(cutNotes('aaa\nbbb\nccc', 7)).toBe('aaa\nbbb');
});
test('order: whatsNew.source, docs/RELEASE-<version>.md, CHANGELOG (version, then Unreleased), else none', () => {
const files: Record<string, string> = { 'CHANGELOG.md': CL, 'docs/RELEASE-1.2.0.md': '# 1.2.0\n\n- Desert world!\n', 'notes.md': 'Hand written.' };
const read = (rel: string) => files[rel];
expect(findNotesSource({ version: '1.2.0', source: 'notes.md', read })).toEqual({ kind: 'file', path: 'notes.md', text: 'Hand written.' });
expect(findNotesSource({ version: '1.2.0', read })).toEqual({ kind: 'release-doc', path: 'docs/RELEASE-1.2.0.md', text: '• Desert world!' });
expect(findNotesSource({ version: '1.1.0', read })?.text).toBe('• Old.');
expect(findNotesSource({ version: '2.0.0', read })?.text).toBe('• Night mode for the map.');
expect(findNotesSource({ read: () => undefined })).toBeUndefined();
expect(() => findNotesSource({ source: 'missing.md', read })).toThrow('whatsNew.source: missing.md does not exist');
});
test('command: the release doc wins over commit messages; --from-git and jargon warnings', async () => {
const p = project({ ...BASE, whatsNew: { allowWords: ['sentry'] } }, { 'package.json': '{"version":"1.2.0"}', 'docs/RELEASE-1.2.0.md': '# 1.2.0\n\n- 20 desert levels.\n- Sentry the guard dog joins you.\n' });
const git = (...a: string[]) => Bun.spawnSync(['git', ...a], { cwd: p.root, env: { ...process.env, GIT_AUTHOR_NAME: 'T', GIT_AUTHOR_EMAIL: 't@example.com', GIT_COMMITTER_NAME: 'T', GIT_COMMITTER_EMAIL: 't@example.com' } });
git('init', '-q'); git('add', '.'); git('commit', '-qm', 'chore: init'); git('tag', 'v1.1.0'); git('commit', '-q', '--allow-empty', '-m', 'feat: Umami telemetry endpoint');
let r = await capture(() => whatsNew(p.ctx(['--dry', '--json'])));
let j = jsonOut(r.out);
expect(j.source).toEqual({ kind: 'release-doc', path: 'docs/RELEASE-1.2.0.md' });
expect(j.play).toBe('• 20 desert levels.\n• Sentry the guard dog joins you.');
expect(r.err).not.toContain('jargon'); // "Sentry" is allowed by whatsNew.allowWords
r = await capture(() => whatsNew(p.ctx(['--dry', '--json', '--from-git'])));
j = jsonOut(r.out);
expect(j.source).toBeNull();
expect(j.play).toContain('Umami telemetry endpoint');
expect(r.err).toContain('internal jargon (SDK / vendor name): "Umami"');
expect(r.err).toContain('"endpoint"; instead: "server"');
});
});