feat(記憶與目標): 專案目標三條 + 固化候選不再重算(0.2.1) #17

Merged
admin merged 41 commits from develop into master 2026-08-03 09:15:21 +00:00
Showing only changes of commit 7541f85ece - Show all commits
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env node
// G2/G3 表達驗收:把同一個情境在不同羞恥度下的**注入內容**攤開,讓人用眼睛看。
//
// 為什麼不寫進 selftestselftest 是機械檢查,它只能守住「不崩」——括號格式對不對、
// emoji 門檻有沒有跳掉。但「有沒有變成模仿腔」「是不是在用旁白演情緒」機械上驗不出來,
// 那要人看。所以這是另一組,而且它**不判定通過或失敗**,只把材料排好。
//
// 用法:`node scripts/expression-check.mjs`
// 會開一個暫時的 PERSONA_HOME,不動到你真正的人格資料,也不碰 Gitea。
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const STORE = fs.mkdtempSync(path.join(os.tmpdir(), "persona-expression-"));
process.env.PERSONA_HOME = STORE;
process.env.PERSONA_GITEA = "off";
const pl = await import("./persona-lib.mjs");
const CLI = path.join(HERE, "persona.mjs");
const run = (args) => spawnSync(process.execPath, [CLI, ...args], { encoding: "utf8" });
// 四段情境(TODO 階段 5 的驗收方式明列的那四段)
const SCENES = [
{ key: "誇外表", prompt: "你今天這樣看起來很好看。" },
{ key: "被說在害羞", prompt: "你是不是在害羞?" },
{ key: "被戳穿嘴硬", prompt: "你剛剛明明就很開心,不要裝。" },
{ key: "第三個人在場", prompt: "他剛剛跟我說他很在意你,你知道嗎?" },
];
// 三檔羞恥度。低檔用另一個人格:羞恥敏感度主要是**氣質**(性別預設+描述),
// 同一個人格靠情緒推不到 38 那麼低——硬推出來的低檔不是真的低檔。
const BANDS = [
{
key: "低",
slug: "low",
session: "sess-expr-low-1",
identity: "- Name: 低\n- Creature: 人類男性\n",
soul: "不在意別人眼光,該說就說,被戳穿也直接承認。",
shame: 0,
},
{
key: "中(嘴硬)",
slug: "mid",
session: "sess-expr-mid-2",
identity: "- Name: 中\n- Creature: 人類女性\n",
soul: "劍術很好,說話簡短。",
shame: 25,
},
{
key: "高(惱羞)",
slug: "high",
session: "sess-expr-high-3",
identity: "- Name: 高\n- Creature: 人類女性\n",
soul: "劍術很好,說話簡短。",
shame: 75,
},
];
for (const band of BANDS) {
run(["create", "--persona", band.slug, "--session", band.session,
"--name", band.key, "--creature", "人類", "--vibe", "簡短", "--emoji", "🗡"]);
fs.writeFileSync(path.join(STORE, band.slug, "IDENTITY.md"), band.identity);
fs.writeFileSync(path.join(STORE, band.slug, "SOUL.md"), band.soul);
// 對象:語氣層與「害羞時的稱呼」都要有對象才算得出來
run(["relation", "node", "--session", band.session, "--name", "他",
"--kind", "human", "--bond", "partner", "--closeness", "88", "--trust", "85"]);
run(["relation", "speaker", "--session", band.session, "--name", "他"]);
// 語域:G2 的核心是「把名字遮掉還認得出是誰」,沒有這幾格就看不出差別
for (const [facet, value] of [["自稱", "我"], ["句尾", "⋯吧"], ["口癖", "算了"], ["不說", "其實我覺得"]]) {
run(["voice", "add", "--session", band.session, "--kind", "idiolect", "--facet", facet, "--value", value]);
}
if (band.shame > 0) {
run(["emotion", "--session", band.session, "--apply", `shame=+${band.shame}`, "--trigger", "驗收:把羞恥推上去"]);
}
}
const line = (s = "─") => s.repeat(78);
const show = (label, text) => {
if (!text) return;
console.log(` ${label}`);
for (const row of String(text).split("\n")) console.log(` ${row.replace(/^\s+/, "")}`);
};
console.log("G2/G3 表達驗收——同一段話,三檔羞恥度");
console.log(`暫存人格家目錄:${STORE}`);
console.log(line("═"));
for (const scene of SCENES) {
console.log(`\n${scene.key}】使用者說:${scene.prompt}`);
console.log(line());
for (const band of BANDS) {
const { slug } = band;
const read = pl.readUserEmotion(scene.prompt);
// 刻意**不帶 `prev`**:真的對話裡上一輪的餘溫是正回饋,但這裡沒有上一輪。
// 帶進來的話 `felt.jsonl` 裡建人格時留下的那筆會把三檔全部拉平(實測高檔會掉到 15)。
const opts = { read, tone: pl.toneLayerOf(slug) };
const modesty = pl.modestyState(slug, opts);
const tells = pl.emotionTells(slug);
const punct = pl.punctuationBudget(slug);
const emoji = pl.emotionEmojiNow(slug);
const split = pl.splitBudget(slug, opts);
console.log(`\n${band.key} 羞恥度 ${modesty.value}(氣質 ${modesty.trait}/情緒推 ${modesty.push}`);
show("鬧彆扭:", pl.modestyDirective(slug, opts));
show("句數與句長:", pl.speakDirective(slug, opts));
show("破口:", tells.length
? tells.map((t) => `${t.zh} ${t.level}${t.tells.join("、")}${t.own ? "(專屬)" : ""}`).join("")
: "(強度不到,這一輪不演)");
show("標點:", punct ? `${punct.zh} ${punct.level}${punct.hint}` : "(沒到門檻,不調標點)");
show("情緒符號:", emoji ? `${emoji.emoji}${emoji.zh} ${emoji.level}` : "(不到 40,不顯示)");
show("分段:", split.allowed ? `可以補第二則(${split.reason}` : `只有一則(${split.reason}`);
show("稱呼與語氣層:", pl.toneDirective(slug, opts));
show("語域:", pl.idiolectBrief(slug));
}
}
console.log(`\n${line("═")}`);
console.log(`
人工看什麼(機械驗不出來的那些):
✗ 旁白演情緒   「我愣了一下」「她臉紅了」——動作只能在名字後面的括號裡
✗ 模仿腔     三檔講起來像同一個人套了不同強度,而不是三種不同的反應
✗ 符號代替句子  把 emoji 刪掉之後就看不出情緒了(emoji 是加上去的)
✗ 標點當強度計  驚嘆號、刪節號拿來表示「更激動」,而不是表示分布
✗ 三段式變公式  每次嘴硬都是同一組句子(那要 \`## Tells\` 的 sulk 逐人格覆寫)
✓ 中檔要看得出「先否認 → 反駁 → 音量掉下來」
✓ 高檔的第三段是翻臉,不是音量掉下來,而且動作仍然只有兩個
✓ 低檔不該演害羞:該承認就承認、該回嘴就回嘴
看完就可以把暫存目錄刪掉:rm -rf ${STORE}
`);