95 lines
4.4 KiB
JavaScript
95 lines
4.4 KiB
JavaScript
'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.<string, string>, 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` 於審查流程步驟 5 呼叫
|
||
* `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.<string, string>, body: string, raw: string}>} roles
|
||
* {@link loadRoles} 回傳的角色物件陣列。
|
||
* @returns {Array<{file: string, meta: Object.<string, string>, 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.<string, string>, body: string, raw: string}>} roles
|
||
* {@link loadRoles} 回傳的角色物件陣列。
|
||
* @returns {Array<{file: string, meta: Object.<string, string>, 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 };
|