'use strict'; const fs = require('fs'); const path = require('path'); // 角色提示載入:讀取 src/prompts/roles/*.md,解析 YAML frontmatter 取出角色中繼資料。 /** * 載入指定目錄下的全部角色提示檔(`*.md`),解析各檔開頭的 YAML frontmatter 為中繼資料。 * * 只處理副檔名為 `.md` 的檔案,並依檔名字串排序,確保輸出順序穩定。 * frontmatter 採輕量解析:僅支援位於檔案開頭、以 `---` 包夾的「鍵: 值」單行欄位 * (鍵名限英文字母與底線),值外層的一對雙引號會被去除;不支援巢狀或多行值。 * * @param {string} rolesDir - 角色提示檔所在目錄的路徑(例如 action 內的 `src/prompts/roles`)。 * @returns {Array<{file: string, meta: Object., body: string, raw: string}>} * 角色物件陣列(依檔名排序): * - `file`:檔名(不含目錄),例如 `mage.md`。 * - `meta`:frontmatter 鍵值物件(如 `name`、`side`、`focus`、`badge`、`color`、`personality`); * 檔案無 frontmatter 時為空物件。 * - `body`:去除 frontmatter 後的 Markdown 內文;無 frontmatter 時等於全文。 * - `raw`:原始完整檔案內容。 * @throws {Error} 當 `rolesDir` 不存在、無法讀取,或個別檔案讀取失敗時, * 由 `fs.readdirSync` / `fs.readFileSync` 直接拋出(未在函式內捕捉)。 * @remarks * 使用情境:`src/index.js` 於審查流程步驟 4 呼叫 * `loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles'))` 載入全部角色, * 再以 {@link attackersOf} / {@link defendersOf} 依 frontmatter 的 `side` 欄位 * 分出攻擊方(Mage/Assassin/Rogue/Bard/Leo/Maya)與防守方(Paladin), * 供後續組裝各角色的 review 提示詞。 */ function loadRoles(rolesDir) { return fs .readdirSync(rolesDir) .filter((file) => file.endsWith('.md')) .sort() .map((file) => { const raw = fs.readFileSync(path.join(rolesDir, file), 'utf8'); const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw); const meta = {}; if (match) { for (const line of match[1].split(/\r?\n/)) { const kv = /^([A-Za-z_]+):\s*(.*)$/.exec(line.trim()); if (kv) meta[kv[1]] = kv[2].replace(/^"(.*)"$/, '$1'); } } return { file, meta, body: match ? raw.slice(match[0].length) : raw, raw, }; }); } /** * 從角色陣列中過濾出攻擊方角色(frontmatter `side: attack`)。 * * 以嚴格相等比對 `role.meta.side === 'attack'`(大小寫敏感), * 回傳新陣列且保留原輸入順序(即 {@link loadRoles} 的檔名排序),不修改原陣列。 * * @param {Array<{file: string, meta: Object., body: string, raw: string}>} roles * {@link loadRoles} 回傳的角色物件陣列。 * @returns {Array<{file: string, meta: Object., body: string, raw: string}>} * 僅含 `meta.side === 'attack'` 的角色新陣列;無符合者回傳空陣列。 * @remarks * 使用情境:`src/index.js` 在 `loadRoles(...)` 之後呼叫 `attackersOf(roles)`, * 取得攻擊方角色(Mage 邏輯、Assassin 安全、Rogue 效率、Bard 風格、 * Leo 可維護性、Maya 測試)以對 PR diff 發動各面向的攻擊式 review。 */ function attackersOf(roles) { return roles.filter((role) => role.meta.side === 'attack'); } /** * 從角色陣列中過濾出防守方角色(frontmatter `side: defend`)。 * * 以嚴格相等比對 `role.meta.side === 'defend'`(大小寫敏感), * 回傳新陣列且保留原輸入順序(即 {@link loadRoles} 的檔名排序),不修改原陣列。 * * @param {Array<{file: string, meta: Object., body: string, raw: string}>} roles * {@link loadRoles} 回傳的角色物件陣列。 * @returns {Array<{file: string, meta: Object., body: string, raw: string}>} * 僅含 `meta.side === 'defend'` 的角色新陣列;無符合者回傳空陣列。 * @remarks * 使用情境:`src/index.js` 在 `loadRoles(...)` 之後呼叫 `defendersOf(roles)`, * 取得防守方角色(現況為 Paladin,`focus: verdict`)擔任裁決者, * 依原始碼脈絡與排除事項裁定攻擊方提出的 findings 是否成立。 */ function defendersOf(roles) { return roles.filter((role) => role.meta.side === 'defend'); } module.exports = { loadRoles, attackersOf, defendersOf };