feat(標點預算): 情緒決定標點分布,上限夾死免得變成強度計

只有文字的話標點就是語調:同一句「我知道了」,`我知道了。` 與 `我、我知道了⋯`
是兩種情緒,差別全在標點不在用詞。

PUNCTUATION_BUDGET 只寫拍板指名的四種(焦慮/憤怒/害羞/喜悅)。
其餘八種不配標點——硬給每種情緒配一套會讓標點變成裝飾,沒把握的就不編。

punctuationBudget() 每輪只回一種,門檻跟破口同一條線(40)。兩種情緒各配一套
會互相打架:憤怒幾乎不用問號、焦慮要多斷句,混起來不是複雜的情緒,是標點亂撒。

上限:MAX_EXCLAIM = 1、MAX_ELLIPSIS = 3,另擋 […⋯]{3,} 的連發
(中文刪節號本來就常寫兩個,所以三個起才算)。不夾死的話它會變成 emoji 的
替代品——情緒不夠就多打幾個驚嘆號。界線也寫進注入文字:標點管分布、強度歸 emoji。

selftest 711 → 721。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 08:21:37 +00:00
co-authored by Claude Opus 5
parent 06774ceb2b
commit f26d0315f2
3 changed files with 102 additions and 3 deletions
+28
View File
@@ -1203,6 +1203,34 @@ console.log("⑥-d 鬧彆扭的階梯與三段式");
`${pl.modestyOf("ownsulk").value}${ownShape.slice(0, 220)}`);
check("沒寫覆寫的人格照舊用預設模板", pl.personaSulk("shyladder").length === 0);
}
// 標點預算:情緒 → 標點與句長的分布。上限夾死,否則變成 emoji 的替代品
console.log("⑥-e 標點預算");
check("拍板的四種情緒都有標點指示,而且四種不一樣", (() => {
const keys = ["anxiety", "anger", "shame", "joy"];
const hints = keys.map((k) => pl.PUNCTUATION_BUDGET[k]);
return hints.every(Boolean) && new Set(hints).size === 4;
})(), JSON.stringify(pl.PUNCTUATION_BUDGET));
check("沒把握的情緒就不配標點(不硬給十二種)",
Object.keys(pl.PUNCTUATION_BUDGET).length === 4, JSON.stringify(Object.keys(pl.PUNCTUATION_BUDGET)));
check("一個驚嘆號可以", pl.speechBlockers("太好了!").length === 0);
check("兩個驚嘆號會被擋(強度歸 emoji,不歸標點)",
pl.speechBlockers("太好了!真的!").some((i) => i.kind === "exclaim"));
check("刪節號連發會被擋", pl.speechBlockers("我知道⋯⋯⋯").some((i) => i.kind === "ellipsis-run"));
check("兩個刪節號是正常的中文寫法,不擋",
pl.speechBlockers("我知道⋯⋯").every((i) => !String(i.kind).startsWith("ellipsis")));
check("刪節號整則超過上限會被擋",
pl.speechBlockers("我⋯不知道⋯你說⋯這樣⋯").some((i) => i.kind === "ellipsis"));
check("標點預算取主導情緒,而且只回一種(兩種會互相打架)", (() => {
const p = pl.punctuationBudget("alpha");
return p !== null && typeof p.hint === "string" && typeof p.key === "string" && p.level >= 40;
})(), JSON.stringify(pl.punctuationBudget("alpha")));
check("強度不到就不調標點(跟破口同一條線)",
pl.punctuationBudget("alpha", null, { min: 99 }) === null);
check("標點指示有注入,而且講明上限與「強度歸 emoji」", (() => {
const ctxP = pl.turnContext("alpha", S_SPEAK, "那件事後來怎麼了");
return ctxP.includes("此刻的標點") && ctxP.includes("刪節號不連發") && ctxP.includes("強度歸 emoji");
})(), pl.turnContext("alpha", S_SPEAK, "那件事後來怎麼了").slice(0, 500));
check("說話規則有注入「話短、日常字、看得見的東西、不要回頭解釋」", (() => {
const ctxSay = pl.turnContext("alpha", S_SPEAK, "隨便講");
return ["話短一點", "平常會說的字", "看得見的東西", "不要回頭解釋"].every((w) => ctxSay.includes(w));