feat(persona): 時區改為+08:00等值轉換、補機密遮蔽,並接上shared共用規範
依 todo.md 執行的規範治理專案: - persona-lib.mjs 的 iso()/nowIso() 改為輸出 +08:00 offset(等值瞬間轉換, 非加8小時),新增 nowDisplay()(人讀顯示格式)與 todayTaipei()(台北曆日 日期鍵),修正 11 處原本用 UTC 字串切片產生日期鍵的錯誤用法;新增 redactSecrets() 並套用到 persona-gitea.mjs 的 Gitea 錯誤路徑,避免機密 外洩。 - 新增一次性遷移腳本 scripts/migrate-tz.mjs(備份/dry-run/冪等/衝突偵測 中止),供之後有真實人格資料需要轉換時使用。 - persona 13 個 skill 與 AGENTS.md 接上 /jsc-shared 共用規範(spec-output、 spec-script-path、spec-subagent、spec-gitea、spec-git-safety、 spec-skill-invocation 等),不再各自重抄,且完全未變動人格行為本身的規範 (情緒模型、記憶衰減、劇場模式、鎖與隔離機制等逐字保留)。 - selftest 全數通過(788/788,含本次新增的時區邊界/冪等性測試)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+42
-1
@@ -3773,7 +3773,7 @@ console.log("\n㉝ 故事匯入(novel):機械的那半");
|
||||
first_seen: "2024-06-14", last_seen: "2024-06-20", salience: 55 }]);
|
||||
cli(["novel", "write", "--session", S_N, "--work", W]);
|
||||
const one = pl.longTermEntries("novelist").find((m) => m._name === "兩年前的劇情");
|
||||
const today = pl.nowIso().slice(0, 10);
|
||||
const today = pl.todayTaipei();
|
||||
return one && one.happened_at === "2024-06-14" && one.happened_until === "2024-06-20" &&
|
||||
one.first_seen === today && one.last_seen === today;
|
||||
})());
|
||||
@@ -3833,6 +3833,47 @@ console.log("\n㉝ 故事匯入(novel):機械的那半");
|
||||
cli(["release", "--session", S_N]);
|
||||
}
|
||||
|
||||
console.log("\n㉞ 時區工具(todayTaipei / iso / redactSecrets / migrate-tz 冪等性)");
|
||||
{
|
||||
// (a) 時區邊界:UTC 16:00 之後已經跨到台北的隔天,16:00 之前還是同一天。
|
||||
check("UTC 16:30 → 台北已經跨到隔天",
|
||||
pl.todayTaipei(new Date("2026-08-10T16:30:00Z")) === "2026-08-11");
|
||||
check("UTC 15:59 → 台北還沒跨日",
|
||||
pl.todayTaipei(new Date("2026-08-10T15:59:00Z")) === "2026-08-10");
|
||||
|
||||
// (b) iso() 是等值時區轉換,不是加 8 小時再蓋掉:epoch 要完全不變,
|
||||
// 只是換了一套「牆上時間」的顯示格式(+08:00 而不是 Z)。
|
||||
check("iso() 換算前後是同一個瞬間(epoch 相等)", (() => {
|
||||
const src = new Date("2026-08-11T02:10:13Z");
|
||||
return new Date(pl.iso(src)).getTime() === src.getTime();
|
||||
})());
|
||||
check("iso() 輸出以 +08:00 結尾(不是 Z)",
|
||||
/\+08:00$/.test(pl.iso(new Date("2026-08-11T02:10:13Z"))), pl.iso(new Date("2026-08-11T02:10:13Z")));
|
||||
|
||||
// (c) redactSecrets:機密不能原樣留在輸出裡。
|
||||
check("redactSecrets 遮掉 40 字元 hex token 與 email", (() => {
|
||||
const token = "a".repeat(40);
|
||||
const email = "someone@example.com";
|
||||
const out = pl.redactSecrets(`token=${token} 聯絡信箱 ${email}`);
|
||||
return !out.includes(token) && !out.includes(email) && out.includes("***");
|
||||
})());
|
||||
|
||||
// (d) migrate-tz.mjs 冪等性:已經是 +08:00 格式的值不能被誤判成「還沒換算、需要
|
||||
// 再轉一次」。migrate-tz.mjs 的 scanTree() 只對 Z_RE 命中的值產生 timestamp 改寫
|
||||
// (見該檔第 139~140 行、183 行附近),這裡直接複製那兩條正則來驗證判斷邏輯本身:
|
||||
// 一個 +08:00 字串必須落在 TW_RE、絕不可以同時落在 Z_RE,否則會被重複轉換。
|
||||
const Z_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
|
||||
const TW_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+08:00$/;
|
||||
check("migrate-tz 判斷邏輯:+08:00 值不會被誤判成待轉換的 Z 值", (() => {
|
||||
const already = "2026-08-11T02:10:13+08:00";
|
||||
return TW_RE.test(already) && !Z_RE.test(already);
|
||||
})());
|
||||
check("migrate-tz 判斷邏輯:Z 值仍然會被正確認出待轉換", (() => {
|
||||
const legacy = "2026-08-11T02:10:13Z";
|
||||
return Z_RE.test(legacy) && !TW_RE.test(legacy);
|
||||
})());
|
||||
}
|
||||
|
||||
console.log(`\n${"=".repeat(60)}\n通過 ${passed} 項,失敗 ${failed} 項 → ${failed === 0 ? "全部通過 ✅" : "有測試失敗 ❌"}`);
|
||||
console.log(`(暫存倉庫留在 ${STORE},可自行刪除)`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user