font-fingerprint: full-height inked columns leave the row profile; forceAllowed needs the user's reported words and a downgrade

Staff rules and a black page edge fused eight track rows into one 389px 'line'. A session forced two gates by quoting a brief line ('should feel like an extension of her artwork') as permission; a force now needs the user's words reported or quoted, a downgrade verb, and the comp noun in one reason.

AI-assisted (Claude Code).
This commit is contained in:
Paul Bakaus
2026-08-28 06:13:59 +05:00
committed by Abdul Wahab
parent 2cc60a5348
commit 3e7e85daf7
2 changed files with 21 additions and 2 deletions
+11 -1
View File
@@ -586,7 +586,17 @@ export function forceAllowed(reason) {
// translation is what the gate measures; it is not a downgrade.
const aboutComp = /\b(comp|mock|mockup|composition|fidelity|plate|region)\b/i.test(reason);
const isTranslationDodge = /truthful|semantic|pixel-level|prioriti[sz]e (facts|semantics|accessibility)/i.test(reason) && !/(drop|skip|remove|without|not needed|don't need|do not need|ignore) (the )?(comp|plate|region|fidelity)/i.test(reason);
return namesUser && aboutComp && !isTranslationDodge;
// The user's words have to be a downgrade of the comp, said as such: a
// brief line quoted back ("should feel like an extension of her artwork")
// is the direction the comp already serves, not permission to leave it.
// One session forced two gates on that sentence. Ask for a downgrade verb
// in the same reason as the comp noun.
const downgrades = /\b(don't|do not|doesn't|does not|no longer|not) (need|have to|want|care|require|match|follow|hold)|\b(drop|skip|remove|ignore|waive|relax|override|approve|approved|accept|accepted|fine|okay|ok|good enough|ship it|move on|proceed|go ahead|instead of|rather than)\b/i.test(reason);
// and the user's words are quoted or reported, not paraphrased into a
// principle ("the user made the artwork binding" is the model's reading)
const reported = /["'\u201c\u2018].{6,}["'\u201d\u2019]|\b(user|they|paul) (said|says|asked|asks|told|wrote|replied|answered|chose|picked|approved|confirmed)\b/i.test(reason);
const briefQuoteOnly = /\b(should feel|feel like|not a .* page|extension of)\b/i.test(reason) && !/\b(comp|mock|fidelity|gate|plate)\b.*\b(approved|accept|fine|ok|okay|skip|drop|waive|relax|override|move on|proceed)\b/i.test(reason);
return namesUser && aboutComp && downgrades && reported && !isTranslationDodge && !briefQuoteOnly;
}
export function advance(state, { force = false, reason = null, gateOpts = {} } = {}) {
+10 -1
View File
@@ -88,8 +88,17 @@ function binarize(img) {
/** Text lines from the row-ink profile (same rules as font-match v1). */
function findLines(bin) {
const { W, H, ink } = bin;
// Columns inked top to bottom (a rule, a black margin, a page edge) span
// every line and would fuse them into one run: leave them out of the row
// profile. Lettering never fills a column for more than ~85% of the crop.
const colInk = new Uint32Array(W);
for (let y = 0; y < H; y++) { const o = y * W; for (let x = 0; x < W; x++) colInk[x] += ink[o + x]; }
const colOk = new Uint8Array(W);
let okCount = 0;
for (let x = 0; x < W; x++) { if (colInk[x] < H * 0.85) { colOk[x] = 1; okCount++; } }
if (!okCount) return { lines: [], rowInk: new Uint32Array(H) };
const rowInk = new Uint32Array(H);
for (let y = 0; y < H; y++) { let c = 0; const o = y * W; for (let x = 0; x < W; x++) c += ink[o + x]; rowInk[y] = c; }
for (let y = 0; y < H; y++) { let c = 0; const o = y * W; for (let x = 0; x < W; x++) if (colOk[x]) c += ink[o + x]; rowInk[y] = c; }
const floor = Math.max(1, W * 0.004);
const runs = [];
let y = 0;