263 lines
11 KiB
TypeScript
263 lines
11 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* brag-list-kiss-budget-smoke.
|
|
*
|
|
* Part 122 cp60 STRUCTURAL DEFENSE (LL #62 / O-12).
|
|
*
|
|
* Closes the cp59-D1 class: brag-list entries growing long-winded
|
|
* across multiple checkpoints despite the Memory rule "BRAG LIST
|
|
* entries: concise (~2-4 sentences), public-facing wins only,
|
|
* inserted in proper themed section, not appended to end."
|
|
*
|
|
* Drift surfaced at cp59: Ken's "braglist items 274 onward most
|
|
* of them got long-winded again. what did i tell you about that?
|
|
* REMEMBER, STOP DOING THAT!!!" Initial audit found 36 entries
|
|
* over the ≤4-sentence budget across the file; cp59 rewrote 35.
|
|
* This smoke pins the budget so the next checkpoint that drifts
|
|
* fails CI before merge instead of accumulating over many shipped
|
|
* checkpoints.
|
|
*
|
|
* Budget:
|
|
* - ≤4 sentences (memory: "concise ~2-4 sentences")
|
|
* - ≤100 words (chosen as the line where prose becomes essay)
|
|
*
|
|
* STACCATO_ALLOWLIST: a small set of entries use intentional
|
|
* multi-sentence punchy emphasis ("No leverage. No margin. No
|
|
* futures. No options.") — those are K.I.S.S. by design and
|
|
* are exempted from the sentence-count budget but still subject
|
|
* to the word-count budget.
|
|
*
|
|
* Recurring class scope progression (12 defenses across 11 checkpoints):
|
|
* cp48-O1: standalone smoke scripts
|
|
* cp49-O2: vitest unit tests
|
|
* cp50-O3: HTTP route handler regex
|
|
* cp51-O4: ops-cli per-ticker tables
|
|
* cp51-O5: per-asset i18n FAQ key coverage
|
|
* cp52-O6: Ansible env-template required-vars
|
|
* cp53-O7: operator doc per-asset coverage (totally absent)
|
|
* cp54-O8: what_is_<asset> FAQ native-locale floor
|
|
* cp55-O9: multi-family per-asset native-locale floor (registry)
|
|
* cp56-O10: operator doc per-asset CONFIG EXAMPLE coverage (shallow)
|
|
* cp57-O11: env-example ↔ schema parity (bidirectional)
|
|
* cp60-O12: brag-list K.I.S.S. budget (THIS)
|
|
*
|
|
* Mutation test verification: M-126 — appending 200 words of
|
|
* extra prose to entry #5 fires:
|
|
* "brag-list-kiss-budget FAILED: entry 5 over budget:
|
|
* Xs / Yw (limit: 4 sentences, 100 words)."
|
|
*/
|
|
|
|
import { readFileSync } from 'fs';
|
|
import { join, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
const REPO_ROOT = join(__dirname, '..', '..', '..');
|
|
|
|
let failed = 0;
|
|
let passed = 0;
|
|
function pass(name: string): void {
|
|
console.log(` ✓ ${name}`);
|
|
passed++;
|
|
}
|
|
function fail(name: string, detail: string): void {
|
|
console.error(` ✗ ${name}`);
|
|
console.error(` ${detail}`);
|
|
failed++;
|
|
}
|
|
|
|
console.log('\n── brag-list-kiss-budget smoke (cp60 LL #62 / O-12) ──\n');
|
|
|
|
const BRAG_PATH = join(REPO_ROOT, 'MORPHIT-BRAG-LIST.md');
|
|
const content = readFileSync(BRAG_PATH, 'utf-8');
|
|
|
|
/**
|
|
* Entries that use intentional multi-sentence staccato punctuation
|
|
* for rhetorical emphasis. K.I.S.S. by design (short punchy
|
|
* sentences); exempt from sentence-count budget but still subject
|
|
* to word-count budget.
|
|
*
|
|
* cp112 addition: "No leverage. No margin. No futures. No options."
|
|
* (originally #195) is the same "No X. No Y. No Z." rhetorical
|
|
* pattern as #3 and #12. Adding to allowlist preserves Ken's
|
|
* punchy style (also: shortening it would obscure the four-things-
|
|
* Morphit-isn't claim, which is exactly the claim that needs the
|
|
* visual hammer).
|
|
*
|
|
* cp125 numbering shift: the 4 new reputation-hardening entries
|
|
* (cp123-cp125 H1/H4 in §8 + Signal D in §8 + H5+H6 in §8) shifted
|
|
* subsequent entries by +4 (#186 → #190, #195 → #199). Entries #3
|
|
* and #12 are in the brag-list preamble (before the reputation-
|
|
* hardening section) so they're unaffected.
|
|
*
|
|
* cp130 numbering shift: 1 new entry added in §4 (multi-asset
|
|
* self-sovereign pricing #91, between Defense F #90 and wallet-
|
|
* embed previously #91). Subsequent entries shift +1: old #195
|
|
* → new #196, old #204 → new #205.
|
|
*/
|
|
// Staccato-exempt entries: 3, 12, 197, 206, 210, 213, 215.
|
|
// Numbers 197/206/210/213 were 196/205/209/212 before cp140's
|
|
// AI-MCP brag entry was inserted at #99; the renumbering shifted
|
|
// every staccato-allowlisted entry by +1. Numbers 3 and 12 are
|
|
// below the insertion point so they didn't shift.
|
|
//
|
|
// cp165 added two new entries (#12 Lightweight pages, #79
|
|
// RPC-pool resilience); cp166 broadened the RPC-pool entry to
|
|
// also cover the BTC/XMR explorer pool but didn't add a new
|
|
// entry. Net effect: the historical "No leverage..." staccato
|
|
// entry at #213 shifted to #215. '215' added to track that;
|
|
// '213' kept in the allowlist for back-compat (the entry now
|
|
// at #213 is prose, but exemption is harmless and removes a
|
|
// noisy bisect direction if entries get rearranged again).
|
|
// cp176: '14' added. Entry #14's "Period. Zero." is deliberate
|
|
// punchy emphasis — the same staccato style as the allowlisted "No
|
|
// leverage. No margin..." entry — not sprawl. It is well within the
|
|
// word budget; only the sentence-count heuristic trips on it, so it
|
|
// belongs here. (Surfaced when the parser's end-of-input bug was
|
|
// fixed: #14 contains "Zero", whose capital Z used to truncate its
|
|
// body before the sentence counter ever saw the staccato tail.)
|
|
// cp344: a new privacy entry ("Your broadcasts go through your
|
|
// operator's node…") was inserted at #15, shifting every entry ≥15
|
|
// by +1. The staccato exemptions above #15 move with them:
|
|
// 197→198, 206→207, 210→211, 213→214, 215→216. '3', '12', '14'
|
|
// are below the insertion point and don't shift.
|
|
// cp388: a new market-maker brag entry was inserted at #11, shifting
|
|
// every entry ≥11 down by +1. The staccato exemptions move with them:
|
|
// 12→13, 14→15, 198→199, 207→208, 211→212, 214→215, 216→217. '3' is
|
|
// before the insertion point and is unchanged.
|
|
// cp399: a new identity/trust entry ("A poster's cryptographic identity
|
|
// is shown…") was inserted at #147 (section 8), shifting every entry
|
|
// ≥147 by +1. The staccato exemptions ≥147 move with them:
|
|
// 199→200, 208→209, 212→213, 215→216, 217→218. '3', '13', '15' are
|
|
// before the insertion point and are unchanged.
|
|
/** cp445 — this used to be keyed by ENTRY NUMBER, which silently shifted every
|
|
* time an entry was inserted above it (adding three entries to §5 moved the
|
|
* staccato entry from #218 to #221 and the smoke went red on unchanged prose).
|
|
* A brag-list entry's number is not its identity; its opening words are. Keyed
|
|
* on a distinctive prefix of the entry body, insertions can no longer break it. */
|
|
const STACCATO_ALLOWLIST: readonly string[] = [
|
|
'**No email required. No phone number or SMS.',
|
|
'**Lightweight pages even on slow connections.',
|
|
'**We do not get, store, log, transmit, or otherwise touch your IP',
|
|
"**Morphit doesn't have admin keys.",
|
|
'**Some P2P platforms have admin "dispute resolution"',
|
|
'**No "DAO."',
|
|
'**No "premium" customer support.',
|
|
'**No leverage. No margin. No futures. No options.'
|
|
];
|
|
|
|
const SENTENCE_LIMIT = 4;
|
|
const WORD_LIMIT = 100;
|
|
|
|
interface Entry {
|
|
num: string;
|
|
title: string;
|
|
body: string;
|
|
full: string;
|
|
}
|
|
|
|
function parseEntries(src: string): Entry[] {
|
|
const entries: Entry[] = [];
|
|
// Match: `^N. **Title.** body...` until next entry or section heading.
|
|
// cp137 parser fix: `\s+` after the closing `**` used to be mandatory,
|
|
// which silently dropped entries where the title ends with `**:`
|
|
// (no space between the closing `**` and the colon — entry #48
|
|
// is the canonical example: `**Two independent verification paths**:`).
|
|
// Use `[\s:.]*` so the parser consumes optional terminal punctuation
|
|
// AND/OR whitespace before the body. The body capture itself is
|
|
// non-greedy so it stops at the next entry or section heading.
|
|
// Body terminator: next entry, a section heading, or end-of-input.
|
|
// NOTE: JS regex has no `\Z`; an earlier version used it and it was
|
|
// silently treated as the literal char "Z", truncating any entry
|
|
// body at its first capital Z (e.g. "Zcash"/"Zero") and undercounting
|
|
// words/sentences. `(?![\s\S])` is the correct end-of-input assert.
|
|
const re = /^(\d+[a-z]?)\.\s+(\*\*[^*]+\*\*)[\s:.]*(.*?)(?=\n\d+[a-z]?\.|\n## |(?![\s\S]))/gms;
|
|
let m: RegExpExecArray | null;
|
|
while ((m = re.exec(src)) !== null) {
|
|
entries.push({
|
|
num: m[1],
|
|
title: m[2],
|
|
body: m[3].trim(),
|
|
full: `${m[2]} ${m[3]}`.trim()
|
|
});
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
function countSentences(text: string): number {
|
|
// Strip markdown formatting that could confuse the regex.
|
|
const plain = text
|
|
.replace(/\*\*|\*|`/g, '')
|
|
.replace(/\[[^\]]*\]\([^)]+\)/g, '') // strip markdown links
|
|
.replace(/\s+/g, ' ');
|
|
// Count terminal punctuation followed by space, EOL, closing bracket,
|
|
// or em-dash (common Morphit prose style).
|
|
return (plain.match(/[.!?](?:\s|$|\)|—)/g) || []).length;
|
|
}
|
|
|
|
function countWords(text: string): number {
|
|
return text
|
|
.replace(/\*\*|\*|`/g, '')
|
|
.replace(/\[[^\]]*\]\([^)]+\)/g, '')
|
|
.trim()
|
|
.split(/\s+/)
|
|
.filter(Boolean).length;
|
|
}
|
|
|
|
const entries = parseEntries(content);
|
|
console.log(`Total brag entries: ${entries.length}`);
|
|
console.log(
|
|
`Budget: ≤${SENTENCE_LIMIT} sentences, ≤${WORD_LIMIT} words (staccato allowlist exempt from sentence limit)\n`
|
|
);
|
|
|
|
const overSentence: string[] = [];
|
|
const overWord: string[] = [];
|
|
|
|
for (const e of entries) {
|
|
if (!/^\d/.test(e.num)) continue;
|
|
const sentences = countSentences(e.full);
|
|
const words = countWords(e.body);
|
|
|
|
const staccato = STACCATO_ALLOWLIST.some((prefix) => e.title.startsWith(prefix));
|
|
if (sentences > SENTENCE_LIMIT && !staccato) {
|
|
overSentence.push(`#${e.num}: ${sentences}s (${e.title.replace(/\*\*/g, '').slice(0, 50)}…)`);
|
|
}
|
|
if (words > WORD_LIMIT) {
|
|
overWord.push(`#${e.num}: ${words}w (${e.title.replace(/\*\*/g, '').slice(0, 50)}…)`);
|
|
}
|
|
}
|
|
|
|
if (overSentence.length === 0) {
|
|
pass(
|
|
`every entry within ≤${SENTENCE_LIMIT}-sentence budget (excluding ${STACCATO_ALLOWLIST.length} staccato-exempt: ${[...STACCATO_ALLOWLIST].join(', ')})`
|
|
);
|
|
} else {
|
|
fail(
|
|
`every entry within sentence budget`,
|
|
`${overSentence.length} over-budget:\n ${overSentence.join('\n ')}`
|
|
);
|
|
}
|
|
|
|
if (overWord.length === 0) {
|
|
pass(`every entry within ≤${WORD_LIMIT}-word budget`);
|
|
} else {
|
|
fail(
|
|
`every entry within word budget`,
|
|
`${overWord.length} over-budget:\n ${overWord.join('\n ')}`
|
|
);
|
|
}
|
|
|
|
const total = passed + failed;
|
|
console.log(`\n${passed} passed, ${failed} failed (${total} total)`);
|
|
if (failed > 0) {
|
|
console.error('\nbrag-list-kiss-budget smoke FAILED');
|
|
console.error(
|
|
'Memory rule: BRAG LIST entries concise (~2-4 sentences), public-facing wins only, K.I.S.S. for grandma.'
|
|
);
|
|
console.error(
|
|
'Rewrite the over-budget entries to fit the budget. See cp59 audit log for the template.'
|
|
);
|
|
process.exit(1);
|
|
}
|
|
console.log(`✓ all ${total} K.I.S.S. budget checks pass`);
|