From bbd9d8e1716c3fcb7f76c3c65b7d784be48d133e Mon Sep 17 00:00:00 2001 From: deku <1661956284@qq.com> Date: Tue, 11 Aug 2026 21:03:49 +0800 Subject: [PATCH 1/4] fix(docs): keep generic reverse reports flavor-neutral --- skills/docs-generator/SKILL.md | 12 ++--- .../references/security-report-templates.md | 10 ++--- .../references/vendor-report-rules.md | 45 ++++++++++--------- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/skills/docs-generator/SKILL.md b/skills/docs-generator/SKILL.md index c2a2ccf..59ac0cf 100644 --- a/skills/docs-generator/SKILL.md +++ b/skills/docs-generator/SKILL.md @@ -41,15 +41,15 @@ For writing style, tone, and voice guidance, use `Skill(ce:writer)` with **The E ### 厂商报告结构(Issue #65) -安全类正式报告 **MUST** 叠加 `references/vendor-report-rules.md`(只取结构,不抄厂商原文): +安全类正式报告 **MUST** 读取 `references/vendor-report-rules.md`(只取结构,不抄厂商原文)。仅在任务证据或用户明确要求时选择厂商 flavor;普通逆向和其他任务使用 `flavor = null`。 | Flavor | 何时用 | 主参考骨架 | |--------|--------|------------| -| `malware`(**默认**) | 单样本、木马、白加黑、钓鱼投毒、日常程序分析 | 火绒式:概述→流程→样本分析→应急处置→IOC | +| `malware` | 明确恶意样本、木马、白加黑、钓鱼投毒 | 火绒式:概述→流程→样本分析→应急处置→IOC | | `apt` | APT/战役/团伙/多阶段感染链/行业定向 | 卡巴斯基 Securelist 式:摘要→感染链→调查叙事→Interesting findings→技术分析→检测缓解→IOC | -| (无全文 flavor) | 渗透 / CTF / JS 签名 | 原任务模板 + 通用专业元素最小集(摘要、IOC 表可 n/a、建议) | +| `flavor = null` | 普通 APK/ELF/PE/Mach-O 逆向、算法/固件分析、渗透 / CTF / JS 签名 | 原任务模板 + Base 通用元素;不套 malware/APT 专属章节 | -原则:**模板在精不在多** —— 仅上述 2 个 flavor,不另建第三套全文模板。 +原则:**模板在精不在多** —— 仅上述 2 个厂商 flavor,不另建第三套全文模板。 与 §0 Evidence→Finding→Path **同时生效**;冲突时 Evidence 契约优先。 ### 输出规范 @@ -68,7 +68,7 @@ For writing style, tone, and voice guidance, use `Skill(ce:writer)` with **The E - 复现步骤必须让第三方能独立重现 - 敏感信息(真实 token、密码、内部 URL)用占位符替代 - **MUST** 包含 Evidence → Finding → Path 链(见 `../ops/evidence-finding-path.md` 与模板 §0) -- **MUST** 叠加 `references/vendor-report-rules.md`:选定 flavor(默认 `malware`)或任务模板最小集;含概述、IOC 表(可 n/a)、可执行建议 +- **MUST** 读取 `references/vendor-report-rules.md`:选定 `malware` / `apt` 或 `flavor = null`;无 flavor 时只输出原任务模板和适用的 Base 元素,不强制 IOC/ATT&CK - **SHOULD** 引用 case `scope.md` / `timeline.md`(`../scripts/case-init.ps1`) ### 图表集成 @@ -177,7 +177,7 @@ For README, API endpoint, and file organization templates, see [references/templ - `field-journal/` — 报告内容同时作为进化日志的数据来源 **安全报告模板**: `references/security-report-templates.md` -**厂商报告规则**: `references/vendor-report-rules.md`(flavor: malware | apt) +**厂商报告规则**: `references/vendor-report-rules.md`(flavor: malware | apt | null) **通用文档模板**: `references/templates.md` diff --git a/skills/docs-generator/references/security-report-templates.md b/skills/docs-generator/references/security-report-templates.md index 98ee87e..d72992c 100644 --- a/skills/docs-generator/references/security-report-templates.md +++ b/skills/docs-generator/references/security-report-templates.md @@ -38,11 +38,11 @@ | Flavor | 场景 | 骨架一句话 | |--------|------|------------| -| `malware`(默认) | 普通木马/白加黑/单样本 | 火绒式:概述→流程→样本分析→应急处置→IOC | +| `malware` | 明确恶意样本/普通木马/白加黑 | 火绒式:概述→流程→样本分析→应急处置→IOC | | `apt` | APT/战役/多阶段链 | 卡巴式:摘要→感染链→调查→Interesting findings→技术分析→检测缓解→IOC | -| 无全文 flavor | 渗透/CTF/JS 签名 | 本节任务模板 + 通用元素最小集 | +| `flavor = null` | 普通逆向/渗透/CTF/JS 签名 | 本节任务模板 + 适用的 Base 通用元素 | -**通用元素(G1–G7)摘要**:执行摘要 MUST · Scope MUST · E/F/P MUST · IOC 表 MUST(可 n/a)· 可执行建议 MUST · 附录 SHOULD · ATT&CK SHOULD(apt 建议有表) +**通用元素(G1–G7)摘要**:G1 执行摘要 MUST · G2 Scope MUST · G3 E/F/P MUST · G4 IOC 仅 `malware`/`apt` MUST · G5 建议仅 `malware`/`apt` MUST · G6 附录 SHOULD · G7 ATT&CK 在 `apt` MUST 选型与章节顺序以 `vendor-report-rules.md` 为准;与 §0.1–0.5 冲突时 **Evidence 契约优先**。 @@ -123,11 +123,11 @@ ## 1b. 恶意软件 / APT 报告(厂商 flavor) -当任务为恶意软件分析、病毒报告、APT/战役分析时,**不要**仅用上面「逆向工程」骨架交差: +当任务为恶意软件分析、病毒报告、APT/战役分析时,**不要**仅用上面「逆向工程」骨架交差;普通逆向任务保持原模板,不自动选择 vendor flavor: 1. 读 `vendor-report-rules.md` 选 `malware` 或 `apt` 2. 按对应章节顺序输出 -3. 仍 **MUST** 含 §0 Evidence 链与 IOC 表 +3. 仍 **MUST** 含 §0 Evidence 链;`malware` / `apt` flavor 另 **MUST** 含 IOC 表 4. 二进制样本的静态分析 **MUST** 含导入表 Evidence(与 radare2/ida/malware 硬门一致) ## 2. 渗透测试报告模板 diff --git a/skills/docs-generator/references/vendor-report-rules.md b/skills/docs-generator/references/vendor-report-rules.md index 6b5108d..99390b5 100644 --- a/skills/docs-generator/references/vendor-report-rules.md +++ b/skills/docs-generator/references/vendor-report-rules.md @@ -8,40 +8,40 @@ | Flavor | 主参考 | 场景 | |--------|--------|------| -| `malware`(**默认**) | 火绒安全病毒/技术分析报告 | 普通木马、白加黑、钓鱼投毒、单样本逆向 | +| `malware` | 火绒安全病毒/技术分析报告 | 明确的普通木马、白加黑、钓鱼投毒、恶意样本 | | `apt` | 卡巴斯基 Securelist / APT 战役报告(如 MATA) | APT、团伙战役、多阶段感染链、行业定向 | -原则:**模板在精不在多** —— 仅 2 个 flavor + 1 份通用专业元素,不为第三家厂商再复制全文模板。 +原则:**模板在精不在多** —— 仅 2 个厂商 flavor + 1 份通用专业元素;普通逆向、渗透、CTF 和 JS 报告保持任务模板,不默认伪装成恶意软件报告。 --- ## 0. 何时启用 -在 `docs-generator` 生成**安全类**报告时(逆向 / 恶意软件 / 渗透收尾 / 用户明确要求「专业报告」「厂商风格」)**MUST** 读取本文件并选定 flavor。 +在 `docs-generator` 生成**安全类**报告时(逆向 / 恶意软件 / 渗透收尾 / 用户明确要求「专业报告」「厂商风格」)**MUST** 读取本文件。只有任务证据或用户明确要求支持时才选择厂商 flavor;否则使用 `flavor = null`,仅叠加通用专业元素和原任务模板。 | 信号 | Flavor | |------|--------| | APT / 团伙 / 战役 / 多阶段 C2 / 行业定向 / ICS / spear-phish 战役 | `apt` | -| 单样本、木马、窃密、白加黑、仿冒站点、日常程序分析 | `malware`(默认) | -| 渗透测试 / CTF / JS 签名 | **不换 flavor 全文骨架**;仍套用下方「通用专业元素」最小集 + 原任务模板 | +| 明确恶意样本、木马、窃密、白加黑、仿冒站点 | `malware` | +| 普通 APK/ELF/PE/Mach-O 逆向、算法分析、固件分析、渗透测试、CTF、JS 签名 | `flavor = null`;使用原任务模板和通用专业元素最小集 | 用户显式指定「按卡巴/APT」「按火绒/病毒报告」时,覆盖自动选型。 --- -## 1. 通用专业元素(所有安全报告) +## 1. 通用专业元素(Base) -下列元素 **SHOULD** 出现;标 **MUST** 的不可省略(可用一行 `n/a` + 原因占位,禁止整节消失)。 +下列 Base 元素按报告类型应用。标 **MUST** 的不可省略;与特定 flavor 相关的元素不得为了填模板而出现在无关任务中。没有适用内容时,使用 `n/a` 并说明原因。 | # | 元素 | 要求 | |---|------|------| | G1 | 执行摘要 / 概述 | **MUST**:3–8 句:分析了什么、最严重结论、影响面、建议动作 | | G2 | 范围与授权 | **MUST**:链到 case `scope.md`(见模板 §0.1) | | G3 | Evidence→Finding→Path | **MUST**:见 `security-report-templates.md` §0 与 `ops/evidence-finding-path.md` | -| G4 | IOC 表 | **MUST** 有表头;无指标时一行 `n/a` + 原因(未做流量/无外联等) | -| G5 | 建议 / 处置 | **MUST**:至少 1 条可执行建议(检测、缓解或应急步骤) | +| G4 | IOC 表 | `malware` / `apt` **MUST**;其他任务仅在存在相关指标时出现 | +| G5 | 建议 / 处置 | `malware` / `apt` **MUST**:至少 1 条可执行建议;其他任务按原任务模板 | | G6 | 附录元数据 | **SHOULD**:工具与版本、样本哈希、完整复现命令 | -| G7 | ATT&CK 映射 | **SHOULD**(`apt` 下升级为有表可 `n/a` 的硬章节);技术 ID + 简短证据指针 | +| G7 | ATT&CK 映射 | **MUST**(`apt` 下;无适用技术时 `n/a` + 原因);其他任务 **SHOULD** | ### 1.1 IOC 表最小列 @@ -59,7 +59,7 @@ --- -## 2. Flavor:`malware`(火绒式 · 默认) +## 2. Flavor:`malware`(火绒式 · 明确选择) **叙事目标**:让读者 5 分钟内看懂「是什么 → 怎么来的 → 样本怎么干的 → 怎么处置 → 有哪些 IOC」。 @@ -164,9 +164,9 @@ | 任务模板(`security-report-templates.md`) | 叠加方式 | |------------------------------------------|----------| -| 1. 逆向工程报告 | 默认 `malware`:用 §2 顺序重排;原「静态/动态/复现」并入 §3/附录;**保留**导入表等硬门产出为 Evidence | -| 2. 渗透测试报告 | 不套 APT 全文;补 G1(若缺)、G4(若有基础设施 IOC)、G5;攻击路径对齐 §0 Path | -| 3. CTF Writeup | 仅 G1 一句概述 + 可复现;不强制 IOC/ATT&CK | +| 1. 逆向工程报告 | 默认 `flavor = null`,保留原「静态/动态/复现」骨架和导入表等硬门 Evidence;只有明确恶意样本才套 §2 | +| 2. 渗透测试报告 | `flavor = null`;补 Base 中适用的 G1–G3,攻击路径对齐 §0 Path,不强制 IOC | +| 3. CTF Writeup | `flavor = null`;保留原题目、解题思路和复现结构,不强制 IOC/ATT&CK | | 4. JS/Web 签名逆向 | 默认偏 `malware` 精简:概述 → 定位 → 算法 → 复现 → IOC(n/a 常见) | | 恶意软件 / APT 专项 | 显式选 `malware` 或 `apt` 全文骨架 | @@ -177,15 +177,15 @@ ## 5. 选型伪代码 ``` -if user_requests_kaspersky or apt or campaign: +if user_requests_kaspersky or apt or threat_campaign: flavor = apt -elif user_requests_huorong or vir_report or single_malware: +elif user_requests_huorong or vir_report or explicit_malware: flavor = malware -elif task in (pentest, ctf, js_sign): - flavor = null # 任务模板 + 通用元素最小集 else: - flavor = malware # 默认 -emit(report with G1–G7 and flavor outline) + flavor = null # 原任务模板 + Base 中适用的元素 +emit(base_report) +if flavor in (malware, apt): + emit(report with flavor outline) ``` --- @@ -195,8 +195,9 @@ emit(report with G1–G7 and flavor outline) - [ ] 已选 flavor 或显式「任务模板 + 最小集」 - [ ] G1 概述存在且非空话 - [ ] §0 E/F/P 字段完整 -- [ ] IOC 表存在(或 n/a+原因) -- [ ] 有可执行建议/处置 +- [ ] `malware` / `apt` 报告有 IOC 表(或 n/a+原因) +- [ ] `malware` / `apt` 报告有可执行建议/处置 +- [ ] 无 flavor 的任务没有被套入 malware/APT 专属章节 - [ ] 无厂商原文粘贴、无 placeholder/TODO - [ ] 导入表等硬门 Evidence 已进入静态/技术分析(若本任务做过二进制分析) From ab25b8e3e83fce34feab4dc66281170460363cd5 Mon Sep 17 00:00:00 2001 From: deku <1661956284@qq.com> Date: Tue, 11 Aug 2026 21:06:20 +0800 Subject: [PATCH 2/4] fix(docs): preserve evidence before malware remediation --- skills/docs-generator/references/vendor-report-rules.md | 2 +- skills/scripts/verify-routing-coherence.ps1 | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/skills/docs-generator/references/vendor-report-rules.md b/skills/docs-generator/references/vendor-report-rules.md index 99390b5..bbb04c7 100644 --- a/skills/docs-generator/references/vendor-report-rules.md +++ b/skills/docs-generator/references/vendor-report-rules.md @@ -85,7 +85,7 @@ ### 3.4 核心发现(Findings 表或编号列表,挂 evidence_ids) ## 4. 应急处置方式 -(编号可执行步骤:断网 → 杀进程 → 清文件 → 查 hosts/启动项 → 全盘查杀 → 复核) +(仅在授权范围内执行:先确认 scope 并保全样本、内存、进程树、网络连接和日志等证据,再隔离主机;经负责人批准后再终止进程、隔离/清除文件、检查 hosts/启动项、全盘查杀并复核。不得在证据保全前直接删除文件。) ## 5. 总结说明 (给普通用户/运维的风险提醒与预防) diff --git a/skills/scripts/verify-routing-coherence.ps1 b/skills/scripts/verify-routing-coherence.ps1 index 780c5b2..f05bbd0 100644 --- a/skills/scripts/verify-routing-coherence.ps1 +++ b/skills/scripts/verify-routing-coherence.ps1 @@ -182,6 +182,13 @@ Assert-Fields (Join-Path $skillsRoot 'pentest-tools/references\recon-pipeline.md Assert-Fields (Join-Path $skillsRoot 'docs-generator/references\security-report-templates.md') @('Evidence Chain', 'Findings', 'Path') Assert-Fields (Join-Path $skillsRoot 'field-journal/_template.md') @('Scope', 'Evidence', 'Finding') Assert-Fields (Join-Path $skillsRoot 'case-review/SKILL.md') @('ACTION REQUIRED', 'review_case.py', 'Evidence Graph Review') +$vendorRulesPath = Join-Path $skillsRoot 'docs-generator/references\vendor-report-rules.md' +$vendorRulesText = Get-Content $vendorRulesPath -Raw -Encoding UTF8 +if ($vendorRulesText -match '先确认 scope 并保全' -and $vendorRulesText -match '不得在证据保全前直接删除文件') { + Ok 'malware remediation preserves evidence before destructive actions' +} else { + Bad 'malware remediation does not require evidence preservation before destructive actions' +} $fieldLog | Set-Content -LiteralPath (Join-Path $ScratchDir 'template-fields.txt') -Encoding UTF8 # --- role map skills exist for primary rows --- From 5d9d05a5e700e6f80d82f0790f556a89514a0ab5 Mon Sep 17 00:00:00 2001 From: deku <1661956284@qq.com> Date: Tue, 11 Aug 2026 21:07:11 +0800 Subject: [PATCH 3/4] fix(docs): align JS signature report flavor routing --- skills/docs-generator/references/vendor-report-rules.md | 2 +- skills/scripts/verify-routing-coherence.ps1 | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/skills/docs-generator/references/vendor-report-rules.md b/skills/docs-generator/references/vendor-report-rules.md index bbb04c7..fa0cd76 100644 --- a/skills/docs-generator/references/vendor-report-rules.md +++ b/skills/docs-generator/references/vendor-report-rules.md @@ -167,7 +167,7 @@ | 1. 逆向工程报告 | 默认 `flavor = null`,保留原「静态/动态/复现」骨架和导入表等硬门 Evidence;只有明确恶意样本才套 §2 | | 2. 渗透测试报告 | `flavor = null`;补 Base 中适用的 G1–G3,攻击路径对齐 §0 Path,不强制 IOC | | 3. CTF Writeup | `flavor = null`;保留原题目、解题思路和复现结构,不强制 IOC/ATT&CK | -| 4. JS/Web 签名逆向 | 默认偏 `malware` 精简:概述 → 定位 → 算法 → 复现 → IOC(n/a 常见) | +| 4. JS/Web 签名逆向 | `flavor = null`;使用原概述 → 定位 → 算法 → 复现骨架,不套 malware | | 恶意软件 / APT 专项 | 显式选 `malware` 或 `apt` 全文骨架 | **冲突解决**:§0 Evidence 链字段与 scope 门禁 **永远优先**;flavor 只改叙事顺序与专业外壳,不得删除 E/F/P。 diff --git a/skills/scripts/verify-routing-coherence.ps1 b/skills/scripts/verify-routing-coherence.ps1 index f05bbd0..e3e3ff8 100644 --- a/skills/scripts/verify-routing-coherence.ps1 +++ b/skills/scripts/verify-routing-coherence.ps1 @@ -189,6 +189,11 @@ if ($vendorRulesText -match '先确认 scope 并保全' -and $vendorRulesText -m } else { Bad 'malware remediation does not require evidence preservation before destructive actions' } +if ($vendorRulesText -match '(?m)JS/Web 签名逆向报告\s*\|[^\r\n]*malware') { + Bad 'vendor rules route JS signature reports through malware flavor' +} else { + Ok 'vendor rules keep JS signature reports flavor-neutral' +} $fieldLog | Set-Content -LiteralPath (Join-Path $ScratchDir 'template-fields.txt') -Encoding UTF8 # --- role map skills exist for primary rows --- From 9cc4080924912adcde794cc6aaf8e21436773e98 Mon Sep 17 00:00:00 2001 From: deku <1661956284@qq.com> Date: Tue, 11 Aug 2026 21:09:49 +0800 Subject: [PATCH 4/4] fix(docs): restore evidence and vendor source traceability --- .../references/vendor-report-rules.md | 20 +++++++++++++------ skills/scripts/verify-routing-coherence.ps1 | 9 +++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/skills/docs-generator/references/vendor-report-rules.md b/skills/docs-generator/references/vendor-report-rules.md index fa0cd76..ab0ad5b 100644 --- a/skills/docs-generator/references/vendor-report-rules.md +++ b/skills/docs-generator/references/vendor-report-rules.md @@ -37,7 +37,7 @@ |---|------|------| | G1 | 执行摘要 / 概述 | **MUST**:3–8 句:分析了什么、最严重结论、影响面、建议动作 | | G2 | 范围与授权 | **MUST**:链到 case `scope.md`(见模板 §0.1) | -| G3 | Evidence→Finding→Path | **MUST**:见 `security-report-templates.md` §0 与 `ops/evidence-finding-path.md` | +| G3 | Evidence→Finding→Path | **MUST**:见 `security-report-templates.md` §0 与 `skills/ops/evidence-finding-path.md` | | G4 | IOC 表 | `malware` / `apt` **MUST**;其他任务仅在存在相关指标时出现 | | G5 | 建议 / 处置 | `malware` / `apt` **MUST**:至少 1 条可执行建议;其他任务按原任务模板 | | G6 | 附录元数据 | **SHOULD**:工具与版本、样本哈希、完整复现命令 | @@ -46,9 +46,9 @@ ### 1.1 IOC 表最小列 ```markdown -| 类型 | 值 | 上下文 | 置信度 | -|------|----|--------|--------| -| file_sha256 / file_md5 / domain / ip:port / url / mutex / path / registry | … | 何处发现 | high/med/low | +| 类型 | 值 | 上下文 | 首次/最后发现 | 来源证据 | 置信度 | +|------|----|--------|---------------|----------|--------| +| file_sha256 / file_md5 / domain / ip:port / url / mutex / path / registry | … | 何处发现 | YYYY-MM-DD / n/a | E-id | high/med/low | ``` ### 1.2 版权与安全边界 @@ -203,8 +203,16 @@ if flavor in (malware, apt): --- -## 7. 非目标 +## 7. 来源登记 + +- Kaspersky Securelist, “Updated MATA attacks industrial companies in Eastern Europe”: (结构参考;访问日期:2026-08-11) +- 火绒安全公开技术文章入口:(站点入口;访问日期:2026-08-11。具体文章 URL、标题和访问日期应在实际引用时登记) +- ATT&CK 技术编号仅作为规范化映射,必须由本次 Evidence 支撑;不得把外部报告中的 IOC 自动带入当前报告。 + +--- + +## 8. 非目标 - 不维护 Mandiant/CrowdStrike/奇安信等额外全文模板(结构已由双 flavor 覆盖常见需求)。 - 不自动爬取厂商站点填报告。 -- 不因 flavor 降低 Evidence 契约或授权范围。 \ No newline at end of file +- 不因 flavor 降低 Evidence 契约或授权范围。 diff --git a/skills/scripts/verify-routing-coherence.ps1 b/skills/scripts/verify-routing-coherence.ps1 index e3e3ff8..49810f1 100644 --- a/skills/scripts/verify-routing-coherence.ps1 +++ b/skills/scripts/verify-routing-coherence.ps1 @@ -97,6 +97,7 @@ $opsFiles = @( 'case-review/SKILL.md', 'case-review/scripts/review_case.py', 'docs-generator/references\security-report-templates.md', + 'docs-generator/references\vendor-report-rules.md', 'field-journal/_template.md' ) $indexLines = New-Object System.Collections.Generic.List[string] @@ -184,6 +185,13 @@ Assert-Fields (Join-Path $skillsRoot 'field-journal/_template.md') @('Scope', 'E Assert-Fields (Join-Path $skillsRoot 'case-review/SKILL.md') @('ACTION REQUIRED', 'review_case.py', 'Evidence Graph Review') $vendorRulesPath = Join-Path $skillsRoot 'docs-generator/references\vendor-report-rules.md' $vendorRulesText = Get-Content $vendorRulesPath -Raw -Encoding UTF8 +Assert-Fields (Join-Path $skillsRoot 'docs-generator/SKILL.md') @('vendor-report-rules.md', 'flavor = null', '不强制 IOC/ATT&CK') +Assert-Fields $vendorRulesPath @('flavor = null', 'explicit_malware') +if ($vendorRulesText -match '(?m)逆向工程报告\s*\|\s*默认\s*`malware`') { + Bad 'vendor rules default generic reverse engineering to malware flavor' +} else { + Ok 'vendor rules keep generic reverse engineering flavor-neutral' +} if ($vendorRulesText -match '先确认 scope 并保全' -and $vendorRulesText -match '不得在证据保全前直接删除文件') { Ok 'malware remediation preserves evidence before destructive actions' } else { @@ -194,6 +202,7 @@ if ($vendorRulesText -match '(?m)JS/Web 签名逆向报告\s*\|[^\r\n]*malware') } else { Ok 'vendor rules keep JS signature reports flavor-neutral' } +Assert-Fields $vendorRulesPath @('skills/ops/evidence-finding-path.md', '来源证据', 'securelist.com/updated-mata', 'www.huorong.cn') $fieldLog | Set-Content -LiteralPath (Join-Path $ScratchDir 'template-fields.txt') -Encoding UTF8 # --- role map skills exist for primary rows ---