mj-queue: cap the --no list, and refuse to build past it

The gryphon fix added six armour negatives and three weapon ones, which took
the prompt to eight --no terms. This project has a recorded case where ten or
more never returned a grid at all while the same prompt with four came back
in two minutes — and a job that never arrives is indistinguishable from a
moderation refusal, because those are ephemeral and never appear in the
channel either. It cost twenty minutes of wondering whether the account was
out of fast hours, and the note about it was written down precisely so it
would not be diagnosed from scratch a second time. I walked into it anyway.

Trimmed to three armour terms and one weapon term, which puts every prompt at
five. promptFor now throws above six and names the offending terms, so the
next person to reach for a long negative list is stopped at the tool rather
than by a half-hour stall. All 204 specs build under the cap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-23 00:35:21 +01:00
co-authored by Claude Opus 5
parent 25d8f04a65
commit 533f2d8093
+24 -4
View File
@@ -134,12 +134,17 @@ const armourWords = (n, era = "antique") => bandOf(Number(n) || 0, [
function negativesFor(spec, era, isPerson) {
if (isPerson) return [];
const out = [];
if (era !== "future" && era !== "anomalous") {
out.push("worn armour", "plate armour", "armour plating", "machinery", "robot", "mecha");
}
/* SHORT, deliberately. The base style already carries two negatives, and this project
has a recorded failure where prompts with ten or more --no terms never returned a
grid at all while the same prompt with four came back in two minutes — a stall that
is indistinguishable from a moderation refusal and so gets misdiagnosed every time.
The first version of this function pushed six armour terms and three weapon terms,
which took the gryphon to eight and into that band. Three terms and one; steer with
the prompt, which is what the armourWords rewrite above is for. */
if (era !== "future" && era !== "anomalous") out.push("plate armour", "machinery", "robot");
// "no weapon — it uses its own limbs" is already in the prompt, positively. An animal
// is still handed a spear often enough to be worth stating twice.
if (!(spec.weapons ?? []).length) out.push("weapons", "swords", "armour straps");
if (!(spec.weapons ?? []).length) out.push("weapons");
return out;
}
@@ -311,6 +316,21 @@ export function promptFor(spec, kind = "npc") {
const base = expand(clauses.join(", ").replace(/\s+/g, " ").trim());
const extra = negativesFor(spec, era, isPerson);
const prompt = extra.length ? `${base}, ${extra.join(", ")}` : base;
/* A prompt with too many --no terms does not fail, which is the problem: it never
returns a grid at all, and a job that never arrives looks exactly like a moderation
refusal — those are ephemeral and never appear in the channel either. The recorded
case cost twenty minutes of wondering whether the account was out of fast hours.
Refuse to build one, and say which terms, rather than letting it be diagnosed again
from scratch. Six is under the observed cliff with room to spare. */
const NEGATIVE_CAP = 6;
const negatives = (prompt.split("--no ")[1] ?? "").split(",").map(x => x.trim()).filter(Boolean);
if (negatives.length > NEGATIVE_CAP) {
throw new Error(`${spec.key}: ${negatives.length} --no terms (cap ${NEGATIVE_CAP}) — `
+ `a long negative list stalls the job silently rather than failing it. `
+ `Steer in the prompt instead. Terms: ${negatives.join(", ")}`);
}
const warnings = BANNED.filter(b => prompt.toLowerCase().includes(b));
return { prompt, token, warnings };
}