
Nsfc Research Foundation Writer
- 206 installs
- 2.6k repo stars
- Updated July 20, 2026
- huangwb8/chineseresearchlatex
Author NSFC literature-review and research-foundation sections summarizing prior work, gaps, and rationale that underpin a Chinese natural-science grant proposal.
About
Drafts NSFC research-foundation and literature-review sections for Chinese grant proposals, synthesizing prior studies, stating knowledge gaps, and building scientific rationale in the formal structure expected by National Natural Science Foundation reviewers.
- Literature review synthesis
- Research gap identification
- Prior work summarization
- Scientific rationale
- NSFC foundation format
Nsfc Research Foundation Writer by the numbers
- 206 all-time installs (skills.sh)
- +3 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #516 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/huangwb8/chineseresearchlatex --skill nsfc-research-foundation-writerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 206 |
|---|---|
| repo stars | ★ 2.6k |
| Last updated | July 20, 2026 |
| Repository | huangwb8/chineseresearchlatex ↗ |
What it does
Author NSFC literature-review and research-foundation sections summarizing prior work, gaps, and rationale that underpin a Chinese natural-science grant proposal.
Files
NSFC(三)研究基础编排写作器
与 bensz-collect-bugs 的协作约定
- 当用户环境中出现因本 skill 设计缺陷导致的 bug 时,优先使用
bensz-collect-bugs按规范记录到~/.bensz-skills/bugs/,严禁直接修改用户本地 Claude Code / Codex 中已安装的 skill 源码。 - 若 AI 仍可通过 workaround 继续完成用户任务,应先记录 bug,再继续完成当前任务。
- 当用户明确要求“report bensz skills bugs”等公开上报动作时,调用本地
gh与bensz-collect-bugs,仅上传新增 bug 到huangwb8/bensz-bugs;不要 pull / clone 整个 bug 仓库。
目标输出(契约)
- 写入落点(2 个文件):
extraTex/3.1.研究基础.tex(包含“研究风险的应对措施”)extraTex/3.2.工作条件.tex- 禁止改动:
main.tex、extraTex/@config.tex、任何.cls/.sty - 核心目标:用“证据链 + 条件对位 + 风险预案”回答评审的三个问题:你做过吗?你做得成吗?出问题你怎么兜底?
参数与输出模式(建议显式提供)
project_root:标书项目根目录(如projects/NSFC_Young)output_mode(默认apply):preview:只输出两段可复制的 LaTeX 正文草稿(并标注应写入的目标文件路径),不写入文件apply:仅写入两份目标文件(见“目标输出”),不触碰其他文件
必需输入(最小信息表)
- 若用户未提供,请先收集/补全:references/info_form.md
写入安全约束(必须遵守)
1. 仅编辑两份 extraTex/3.*.tex 文件;不得修改 main.tex、extraTex/@config.tex、任何 .cls/.sty 2. 目标文件若已包含标题命令(如 \\subsection{...} / \\subsubsection{...}),只替换正文内容,不改标题与结构层级 3. 信息不全时先提问补齐;不得捏造论文题目/期刊/专利号/样本量/指标等“看起来像真的”细节
工作流(按顺序执行)
1. 定位项目与目标文件:
- 验证
project_root是否存在,不存在时报错并提示用户指定正确路径 - 检查
extraTex/目录是否存在,不存在时提示用户先初始化/拷贝模板项目;必要时可在用户确认后创建 - 确认
project_root和output_mode(默认为apply) preview模式:只生成内容预览,不写入文件(适合调试)apply模式:将生成的内容写入extraTex/3.1.研究基础.tex和extraTex/3.2.工作条件.tex- 仅编辑这两个文件,禁止修改
main.tex、extraTex/@config.tex、任何.cls/.sty
2. 生成 `3.1 研究基础`(证据链优先):
- 研究积累:围绕
2.1的关键任务,列出“做过什么/掌握什么/已有平台什么”。 - 阶段性成果:只写可核验内容(论文/专利/数据/原型/预实验现象);不确定的细节用占位符要求用户补齐。
- 可行性四维:理论/技术/团队/条件各给 1–3 个支撑点,并与研究内容逐条对齐。
3. 在 `3.1` 中显式写“研究风险的应对措施”:
- 至少 3 条风险(技术/进度/资源各至少 1 条)
- 每条:风险描述 → 早期信号(触发阈值/现象)→ 预案/替代路线(含降级目标与可交付)
4. 生成 `3.2 工作条件`(条件对位研究内容):
- 已具备条件:逐条列出,格式建议:
- 平台:XXX 平台(已具备 / 可访问)
- 数据:XXX 数据集(已获取 / 可公开获取)
- 样本:XXX 医院/机构(已签署合作协议 / 伦理审批中)
- 算力:XXX 服务器 / GPU(已配置 / 共享使用)
- 团队分工:成员 A 负责 XX,成员 B 负责 YY
- 合规路径:伦理审批(XXX 委员会,周期 X 个月)
- 尚缺条件与解决途径:逐条列出,格式建议:
- 缺少条件:XXX(影响:YYY)
- 解决途径:采购 / 合作 / 替代数据源 / 实验降级方案
- 时间表与责任人:如用户提供,应写入;如未提供,用占位符标记
5. 一致性校验:
- 校验 1:检查
3.2 工作条件是否能逐条支撑2.1的关键任务 - 方法:列出
2.1的每个关键任务,确认3.2中有对应的条件支撑 - 示例:
- 任务 1:XXX 实验 → 条件:XXX 平台、XXX 样本
- 任务 2:YYY 算法 → 条件:YYY 算力、ZZZ 数据
- 校验 2:检查风险预案是否与年度计划可兼容
- 方法:列出
2.3的每年里程碑,确认风险预案有对应的降级方案 - 示例:
- 第一年:样本获取 → 风险:样本入组慢 → 预案:有替代数据源与降级验证方案
- 校验失败时的处理:
- 如果发现不一致,标记为
[需补充:XXX]并提示用户 - 如果用户未提供
2.1或2.3,跳过对应校验并提示用户
验收标准(Definition of Done)
质量契约验证(来自 config.yaml)
- 研究基础必须覆盖:
- 与本项目相关的研究积累(证据链)
- 已取得的阶段性成果(可核验)
- 可行性要点(理论/技术/团队/条件)
- 研究风险与应对措施(显式写出)
- 工作条件必须覆盖:
- 已具备条件(平台/数据/样本/算力/团队/合规)
- 尚缺条件与解决途径(时间/预算/合作/采购/替代方案)
详细检查清单
见:references/dod_checklist.md
可选脚本自检(只读)
- 仅校验 skill 自身结构一致性:
python3 skills/nsfc-research-foundation-writer/scripts/validate_skill.py - 同时检查某个项目的输出文件(存在性 + 轻量内容启发式):
python3 skills/nsfc-research-foundation-writer/scripts/run_checks.py --project-root <你的project_root>
常见问题与边缘情况
Q1: 项目目录中没有 extraTex/ 目录怎么办?
A: 建议先确认 project_root 指向正确的标书项目根目录;若确实缺少 extraTex/,请手动创建:
mkdir -p "<你的project_root>/extraTex"Q2: 我还没有写 2.1 研究内容,可以直接写 3.1 研究基础 吗?
A: 不建议。3.2 工作条件 需要与 2.1 的关键任务对齐。建议先使用 nsfc-research-content-writer 完成 2.1。
Q3: 信息表中的某些内容我不方便公开怎么办?
A: 可以提供"可核验线索"而非完整内容。例如:
- ❌ "我们在 Nature 上发表了论文 XXX"
- ✅ "我们发表过相关论文(可提供 DOI/题录/接收函编号等可核验线索)"
Q4: 风险应对必须写 3 条吗?
A: 是的,至少 3 条(技术/进度/资源各至少 1 条)。即便你认为风险较低,也建议按三类各写 1 条“低风险 + 监测信号 + 预案”,避免评审认为你没有兜底意识。
Q5: 我的信息表内容很少,能生成完整的研究基础吗?
A: 可以。AI 会根据你提供的信息生成内容,并在不确定的地方使用占位符(如 [请补充:XXX]),请你后续补全。
变更记录
- 本技能的变更历史记录在本目录的
CHANGELOG.md,并同步到根级CHANGELOG.md;SKILL.md仅维护“AI 执行规范”。
变更记录
[0.1.2] - 2026-04-24
Fixed
- 修复
SKILL.mdfrontmatter 中混入 Markdown 正文导致技能加载器 YAML 解析失败的问题。
Changed
- 将
bensz-collect-bugs协作约定移出 YAML frontmatter,保留为正文执行规则。 - 增强
scripts/validate_skill.py,在 PyYAML 可用时对SKILL.mdfrontmatter 执行严格 YAML 解析。
[0.1.1] - 2026-02-16
Added
- 新增只读自检脚本:
scripts/validate_skill.py、scripts/check_project_outputs.py、scripts/run_checks.py
Changed
- 强化 SKILL.md 的写入安全约束与参数说明,降低误改 LaTeX 结构风险
- 信息表与文档表述去年份化,提升通用性
- README 增加
output_mode用法与可选自检入口
[0.1.0] - 2026-01-14
Added
- 初始版本发布
- 支持为 NSFC 标书正文"(三)研究基础"写作/重构
- 支持同步编排"工作条件"和"研究风险应对"
- 支持证据链验证、可行性四维分析、风险预案生成
Changed
- 增强 SKILL.md 工作流步骤的详细指导
- 增加 config.yaml 的注释说明
- 增强 README.md 的用户引导
Fixed
- 修复 quality_contract 配置未在工作流中引用的问题
- 修复工作流步骤缺少路径验证说明的问题
- 修复边缘情况处理说明缺失的问题
# ================================
# nsfc-research-foundation-writer 默认配置
# ================================
skill_info:
name: nsfc-research-foundation-writer
version: 0.1.2
# template_year: 移除年份限制,使技能适用于不同年份的 NSFC 模板
category: writing
description: "为 NSFC 正文“(三)研究基础”写作/重构,并同步编排“工作条件/研究风险应对”(不适用:非标书正文场景)"
author: "Bensz Conan"
parameters:
project_root:
type: string
required: true
description: 项目根目录(如 projects/NSFC_Young 或你的标书项目路径)
output_mode:
type: string
required: false
default: apply
allowed_values: [preview, apply]
# preview: 预览模式,只生成内容不写入文件
# apply: 应用模式,将生成的内容写入 extraTex 文件
targets:
foundation_tex: extraTex/3.1.研究基础.tex
conditions_tex: extraTex/3.2.工作条件.tex
guardrails:
allowed_write_files:
- extraTex/3.1.研究基础.tex
- extraTex/3.2.工作条件.tex
forbidden_write_files:
- main.tex
- extraTex/@config.tex
forbidden_write_globs:
- "**/*.cls"
- "**/*.sty"
quality_contract:
foundation_must_cover:
- 与本项目相关的研究积累(证据链)
- 已取得的阶段性成果(可核验)
- 可行性要点(理论/技术/团队/条件)
- 研究风险与应对措施(显式写出)
conditions_must_cover:
- 已具备条件(平台/数据/样本/算力/团队/合规)
- 尚缺条件与解决途径(时间/预算/合作/采购/替代方案)
nsfc-research-foundation-writer
用于 NSFC 标书正文 (三)研究基础 的写作/重构,并同时编排:
3.2 工作条件3.1 中的研究风险应对
目标是用"证据链"证明做得成,并把条件与风险对位到研究内容。
技能依赖关系
本技能属于 NSFC 申请书写作流程的一部分,建议按以下顺序使用:
graph LR
A[nsfc-research-justification-writer<br/>(一)立项依据] --> B[nsfc-research-content-writer<br/>(二)研究内容]
B --> C[nsfc-research-foundation-writer<br/>(三)研究基础]
style C fill:#f9f,stroke:#333,stroke-width:2px推荐顺序: 1. 先写(一)立项依据:使用 nsfc-research-justification-writer 2. 再写(二)研究内容:使用 nsfc-research-content-writer 3. 最后写(三)研究基础:使用本技能
原因:
3.2 工作条件需要与2.1研究内容的关键任务对齐- 风险应对需要与
2.3年度研究计划兼容 - 研究基础需要证明"有能力完成研究内容"
如果必须跳过前置步骤:
- 请手动提供
2.1和2.3的内容,以便 AI 进行一致性校验 - 或者在完成
2.1和2.3后,重新使用本技能进行校验和调整
快速开始
1. 准备项目
确保你的 NSFC 项目在 projects/NSFC_Young(或其他路径)
2. 准备信息表
按照 references/info_form.md 准备以下信息:
- 前期基础(证据链素材)
- 团队与分工
- 条件与资源
- 风险清单(至少 3 条)
3. 执行技能
请使用 nsfc-research-foundation-writer:
目标项目:projects/NSFC_Young
信息表:<按 references/info_form.md 提供>
输出:写入 extraTex/3.1.研究基础.tex、extraTex/3.2.工作条件.tex
额外要求:风险应对至少 3 条,每条要有早期信号与备选方案
output_mode:apply(默认)/ preview(只预览不写入)输出文件
extraTex/3.1.研究基础.tex:研究基础 + 风险应对extraTex/3.2.工作条件.tex:工作条件
输出示例
见 references/example_output.md
注意事项
- 本技能只修改
extraTex/3.*.tex文件,不会修改main.tex或模板文件 - 风险应对至少需要 3 条(技术/进度/资源各至少 1 条)
- 工作条件必须与
2.1研究内容的关键任务对齐
可选自检(只读)
- 仅校验 skill 自身一致性:
python3 skills/nsfc-research-foundation-writer/scripts/validate_skill.py - 同时检查某个项目的输出文件:
python3 skills/nsfc-research-foundation-writer/scripts/run_checks.py --project-root projects/NSFC_Young
Definition of Done(NSFC(三)研究基础 + 工作条件 + 风险应对)
- 输出落点正确:只写
extraTex/3.1.研究基础.tex、extraTex/3.2.工作条件.tex - 证据链可核验:成果/数据/平台不夸大;必要时给出可核验线索或由用户补充
- 可行性“四维对齐”:理论/技术/团队/条件各有支撑点(与研究内容一致)
- 风险应对显式且可执行:至少 3 条风险,每条给早期信号 + 预案/替代路线
- 工作条件对位研究内容:条件描述能逐条支撑
2.1的关键任务 - 不破坏模板结构:不修改
main.tex、不修改extraTex/@config.tex、不修改任何.cls/.sty
研究基础写作输出示例
说明:此示例仅用于展示结构与写法;其中“期刊名/影响因子/样本量/阈值/预算”等细节必须来自用户可核验信息,不得凭空编造。不确定处请用占位符标记并向用户追问补齐。
示例:3.1 研究基础(部分)
3.1.1 与本项目相关的研究积累
申请人长期从事 XXX 研究,已发表 SCI 论文 X 篇,其中 JCR Q1 论文 Y 篇...
3.1.2 已取得的阶段性成果
1. 理论成果:提出了 XXX 方法,发表于 [期刊名],影响因子 X.X... 2. 技术成果:开发了 XXX 原型系统,已应用于 XXX 场景... 3. 数据资源:收集了 XXX 数据集,样本量 XXX...
3.1.3 可行性分析
理论可行性
本研究基于 XXX 理论,该理论已在 [引用论文] 中得到验证...
技术可行性
申请人已掌握 XXX 技术,并在 [前期项目] 中成功应用...
团队可行性
团队由 X 名成员组成,分别负责 XXX、YYY、ZZZ...
条件可行性
已具备 XXX 平台、XXX 数据集、XXX 算力资源...
3.1.4 研究风险与应对措施
风险 1:样本获取困难
早期信号:前 3 个月样本入组量 < 预期的 50% 应对措施: 1. 联系合作医院 XXX,扩大样本来源 2. 采用公开数据集 XXX 作为替代 3. 降级目标:将样本量从 XXX 降至 YYY,但仍可验证核心假设
风险 2:XXX 方法不收敛
早期信号:实验运行 100 轮后损失函数仍未收敛 应对措施: 1. 调整超参数:XXX 从 YYY 改为 ZZZ 2. 替代方法:使用 AAA 方法代替 BBB 方法 3. 降级目标:简化模型复杂度,重点验证核心假设
风险 3:算力不足
早期信号:单次实验时间 > 48 小时 应对措施: 1. 使用云算力平台 XXX(已有预算) 2. 优化算法,减少计算复杂度 3. 分阶段验证:先在小规模数据上验证,再扩展到全量数据
--- 注意:此为示例,实际输出会根据用户提供的信息生成。
NSFC 写作信息表(面向(三)研究基础)
说明:请按以下格式提供信息,标 [必填] 的为必需项。
---
1. 前期基础(证据链素材)[必填]
1.1 已发表/已接收论文
- 论文 1:
- 题目:
- 期刊/会议:
- 年份:
- DOI/链接:[如不便公开可给"可核验线索"]
- 论文 2:
- ...
1.2 专利/软件著作权/数据集/平台/原型
- 专利 1:
- 名称:
- 专利号:
- 状态:[已授权 / 公开中 / 实审中]
- ...
1.3 预实验/初步结果
- 预实验 1:
- 现象:
- 数据概况:
- 可复现实验条件:
- ...
2. 团队与分工 [必填]
2.1 关键成员
- 成员 1:
- 角色:
- 负责模块:
- 相关能力:
- ...
2.2 合作单位
- 合作单位 1:
- 名称:
- 贡献:[数据/样本/平台/算法/临床/工程验证]
- ...
3. 条件与资源 [必填]
- 平台:
- 设备:
- 算力:
- 数据/样本来源:
- 伦理/合规路径与周期:
4. 风险清单(至少 3 条)[必填]
4.1 技术风险
- 风险 1:
- 描述:
- 早期信号(触发阈值/现象):
- 备选方案/降级路线:
4.2 进度风险
- ...
4.3 资源风险
- ...
--- 填写完成后,请复制此内容提供给 AI。
from __future__ import annotations
import re
from typing import Iterable
def extract_yaml_value_under_block(lines: Iterable[str], block_key: str, key: str) -> str | None:
"""
Minimal YAML extractor for a scalar value under a top-level mapping block.
This skill intentionally avoids external YAML dependencies; keep this helper
small and predictable, and only support the patterns we generate in config.yaml.
"""
in_block = False
block_indent: int | None = None
key_re = re.compile(rf"^(\s*){re.escape(key)}:\s*(.*?)\s*$")
for line in lines:
if not in_block:
if re.match(rf"^{re.escape(block_key)}:\s*$", line):
in_block = True
block_indent = len(line) - len(line.lstrip(" "))
continue
if line.strip() == "":
continue
indent = len(line) - len(line.lstrip(" "))
if block_indent is not None and indent <= block_indent and not line.startswith(" " * (block_indent + 1)):
break
m = key_re.match(line)
if m and indent >= (block_indent or 0) + 2:
return m.group(2).strip().strip('"').strip("'")
return None
def extract_yaml_list_under_block(lines: Iterable[str], block_key: str, key: str) -> list[str] | None:
"""Extract a simple YAML list under `block_key: { key: [ - item ] }` style."""
in_block = False
block_indent: int | None = None
key_indent: int | None = None
items: list[str] = []
for line in lines:
if not in_block:
if re.match(rf"^{re.escape(block_key)}:\s*$", line):
in_block = True
block_indent = len(line) - len(line.lstrip(" "))
continue
if line.strip() == "":
continue
indent = len(line) - len(line.lstrip(" "))
if block_indent is not None and indent <= block_indent and not line.startswith(" " * (block_indent + 1)):
break
if key_indent is None:
if re.match(rf"^\s*{re.escape(key)}:\s*$", line) and indent >= (block_indent or 0) + 2:
key_indent = indent
continue
if indent <= key_indent:
break
stripped = line.strip()
if stripped.startswith("- "):
items.append(stripped.removeprefix("- ").strip().strip('"').strip("'"))
return items if items else None
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
from _yaml_utils import extract_yaml_value_under_block
RISK_PHRASES = [
"首次",
"领先",
"国际领先",
"国内领先",
"唯一",
"填补空白",
"世界领先",
"国内首创",
]
def _err(message: str) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return 1
def _warn(message: str) -> None:
print(f"WARNING: {message}", file=sys.stderr)
def _load_targets(config_yaml: Path) -> tuple[str, str]:
lines = config_yaml.read_text(encoding="utf-8").splitlines()
foundation = extract_yaml_value_under_block(lines, "targets", "foundation_tex") or ""
conditions = extract_yaml_value_under_block(lines, "targets", "conditions_tex") or ""
if not foundation or not conditions:
raise ValueError("missing targets.foundation_tex / targets.conditions_tex in config.yaml")
return foundation, conditions
def main() -> int:
parser = argparse.ArgumentParser(
description="Check nsfc-research-foundation-writer outputs under a LaTeX project root (existence + light heuristics)."
)
parser.add_argument("--project-root", required=True, help="LaTeX project root (must contain extraTex/).")
parser.add_argument(
"--no-content-check",
action="store_true",
help="Only check that target files exist (skip content heuristics).",
)
parser.add_argument(
"--no-risk-scan",
action="store_true",
help="Skip scanning for risk phrases like '首次/领先'.",
)
parser.add_argument(
"--fail-on-risk-phrases",
action="store_true",
help="Treat risk phrases as errors (default: warnings).",
)
args = parser.parse_args()
skill_root = Path(__file__).resolve().parents[1]
config_yaml = skill_root / "config.yaml"
if not config_yaml.exists():
return _err(f"missing config.yaml: {config_yaml}")
try:
target_foundation, target_conditions = _load_targets(config_yaml)
except ValueError as exc:
return _err(str(exc))
project_root = Path(args.project_root).expanduser().resolve()
extra_tex = project_root / "extraTex"
if not extra_tex.exists() or not extra_tex.is_dir():
return _err(f"missing extraTex/ under project root: {extra_tex}")
foundation_path = project_root / target_foundation
conditions_path = project_root / target_conditions
for p in [foundation_path, conditions_path]:
if not p.exists() or not p.is_file():
return _err(f"missing target file: {p}")
if args.no_content_check:
print("OK: target files exist (content checks skipped)", flush=True)
return 0
foundation_text = foundation_path.read_text(encoding="utf-8", errors="replace")
conditions_text = conditions_path.read_text(encoding="utf-8", errors="replace")
# Foundation must include explicit risk responses; require >=3 risk items.
if "风险" not in foundation_text:
return _err(f"{foundation_path} does not contain '风险' (risk section missing?)")
if not (("应对" in foundation_text) or ("预案" in foundation_text) or ("替代" in foundation_text)):
_warn(f"{foundation_path} has '风险' but lacks common response keywords (应对/预案/替代); please confirm risk responses are explicit")
# Try to count "risk items" from common LaTeX heading patterns.
risk_items: list[str] = []
risk_items += re.findall(r"\\subsubsubsection\{[^}]*风险[^}]*\}", foundation_text)
risk_items += re.findall(r"\\subsubsection\{[^}]*风险[^}]*\}", foundation_text)
risk_items += re.findall(r"(?m)^\\s*#+\\s*.*风险.*$", foundation_text) # markdown-like headings (rare)
risk_items += re.findall(r"风险\\s*(?:\\d+|[一二三四五六七八九十])", foundation_text)
# Deduplicate near-identical hits.
risk_items = list(dict.fromkeys(risk_items))
if len(risk_items) < 3:
return _err(f"{foundation_path} seems to contain < 3 risk items (found {len(risk_items)})")
# Conditions should reflect "have" and "lack + plan" structure.
if not (("已具备" in conditions_text) or ("具备" in conditions_text)):
_warn(f"{conditions_path} does not mention '已具备/具备' explicitly; please confirm it lists existing conditions")
if not (("尚缺" in conditions_text) or ("缺少" in conditions_text) or ("不足" in conditions_text)):
_warn(f"{conditions_path} does not mention '尚缺/缺少/不足' explicitly; please confirm it covers missing conditions + plan")
# Placeholders are allowed in preview / when info is missing; warn for apply-mode outputs.
if "[请补充:" in foundation_text or "[需补充:" in foundation_text or "[请补充:" in conditions_text or "[需补充:" in conditions_text:
_warn("found placeholder markers like '[请补充:...]' in outputs; confirm this is intentional and consistent with provided info")
if not args.no_risk_scan:
hits: list[tuple[str, str]] = []
combined = f"{foundation_text}\n{conditions_text}"
for phrase in RISK_PHRASES:
if phrase in combined:
hits.append((phrase, "found"))
if hits:
msg = "risk phrases present: " + ", ".join(p for p, _ in hits)
if args.fail_on_risk_phrases:
return _err(msg)
_warn(msg)
print("OK: output checks passed", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
def _run(cmd: list[str]) -> int:
print("+ " + " ".join(cmd), flush=True)
proc = subprocess.run(cmd, text=True)
return proc.returncode
def main() -> int:
parser = argparse.ArgumentParser(
description="Run nsfc-research-foundation-writer minimal checks (validate skill + optional project output checks).",
)
parser.add_argument(
"--project-root",
default="",
help="Optional LaTeX project root to check outputs (must contain extraTex/).",
)
parser.add_argument(
"--no-content-check",
action="store_true",
help="Only check that target files exist (skip content heuristics).",
)
parser.add_argument(
"--no-risk-scan",
action="store_true",
help="Skip scanning for risk phrases like '首次/领先'.",
)
parser.add_argument(
"--fail-on-risk-phrases",
action="store_true",
help="Treat risk phrases as errors (default: warnings).",
)
args = parser.parse_args()
skill_root = Path(__file__).resolve().parents[1]
validate = skill_root / "scripts" / "validate_skill.py"
check_outputs = skill_root / "scripts" / "check_project_outputs.py"
rc = _run([sys.executable, str(validate)])
if rc != 0:
return rc
if args.project_root:
cmd = [
sys.executable,
str(check_outputs),
"--project-root",
args.project_root,
]
if args.no_content_check:
cmd.append("--no-content-check")
if args.no_risk_scan:
cmd.append("--no-risk-scan")
if args.fail_on_risk_phrases:
cmd.append("--fail-on-risk-phrases")
rc = _run(cmd)
if rc != 0:
return rc
print("OK: all checks passed", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
from __future__ import annotations
import re
import sys
from pathlib import Path
from typing import Any
from _yaml_utils import extract_yaml_list_under_block, extract_yaml_value_under_block
def _err(message: str) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return 1
def _warn(message: str) -> None:
print(f"WARNING: {message}", file=sys.stderr)
def _extract_frontmatter(text: str) -> str:
if not text.startswith("---"):
return ""
parts = text.split("\n---\n", 1)
if len(parts) != 2:
return ""
return parts[0]
def _parse_frontmatter_yaml(frontmatter: str) -> dict[str, Any] | None:
try:
import yaml # type: ignore[import-untyped]
except ImportError:
_warn("PyYAML is not installed; skipping strict frontmatter YAML parsing")
return None
try:
parsed = yaml.safe_load(frontmatter) or {}
except yaml.YAMLError as exc:
raise ValueError(f"SKILL.md frontmatter is invalid YAML: {exc}") from exc
if not isinstance(parsed, dict):
raise ValueError("SKILL.md frontmatter must parse to a YAML mapping")
return parsed
def _extract_frontmatter_field(frontmatter: str, key: str) -> str | None:
match = re.search(rf"(?m)^{re.escape(key)}:\s*(.+?)\s*$", frontmatter)
if not match:
return None
return match.group(1).strip().strip('"').strip("'")
def main() -> int:
skill_root = Path(__file__).resolve().parents[1]
repo_root = skill_root.parents[1]
skill_md = skill_root / "SKILL.md"
config_yaml = skill_root / "config.yaml"
required_files = [
skill_md,
config_yaml,
skill_root / "README.md",
skill_root / "CHANGELOG.md",
skill_root / "references" / "info_form.md",
skill_root / "references" / "dod_checklist.md",
skill_root / "references" / "example_output.md",
skill_root / "scripts" / "_yaml_utils.py",
]
for path in required_files:
if not path.exists():
return _err(f"missing required file: {path}")
skill_text = skill_md.read_text(encoding="utf-8")
frontmatter = _extract_frontmatter(skill_text)
if not frontmatter:
return _err("SKILL.md missing YAML frontmatter block")
try:
_parse_frontmatter_yaml(frontmatter)
except ValueError as exc:
return _err(str(exc))
fm_name = _extract_frontmatter_field(frontmatter, "name")
fm_version = _extract_frontmatter_field(frontmatter, "version")
fm_config = _extract_frontmatter_field(frontmatter, "config")
fm_references = _extract_frontmatter_field(frontmatter, "references")
if not fm_name or not fm_version or not fm_config or not fm_references:
return _err("SKILL.md frontmatter missing required fields: name/version/config/references")
fm_config_path = Path(fm_config)
resolved_config_path = (
(repo_root / fm_config_path) if str(fm_config_path).startswith("skills/") else (skill_root / fm_config_path)
).resolve()
if resolved_config_path != config_yaml.resolve():
return _err(f"config path mismatch: SKILL.md={resolved_config_path} expected={config_yaml.resolve()}")
fm_refs_path = Path(fm_references)
resolved_refs_path = (
(repo_root / fm_refs_path) if str(fm_refs_path).startswith("skills/") else (skill_root / fm_refs_path)
).resolve()
if not resolved_refs_path.exists() or not resolved_refs_path.is_dir():
return _err(f"references path invalid: {resolved_refs_path}")
config_lines = config_yaml.read_text(encoding="utf-8").splitlines()
cfg_name = extract_yaml_value_under_block(config_lines, "skill_info", "name")
cfg_version = extract_yaml_value_under_block(config_lines, "skill_info", "version")
if not cfg_name or not cfg_version:
return _err("config.yaml missing skill_info.name or skill_info.version")
if cfg_name != fm_name:
return _err(f"skill name mismatch: SKILL.md={fm_name} config.yaml={cfg_name}")
if cfg_version != fm_version:
return _err(f"skill version mismatch: SKILL.md={fm_version} config.yaml={cfg_version}")
target_foundation = extract_yaml_value_under_block(config_lines, "targets", "foundation_tex")
target_conditions = extract_yaml_value_under_block(config_lines, "targets", "conditions_tex")
if not target_foundation or not target_conditions:
return _err("config.yaml missing targets.foundation_tex or targets.conditions_tex")
allowed = extract_yaml_list_under_block(config_lines, "guardrails", "allowed_write_files") or []
if target_foundation not in allowed or target_conditions not in allowed:
return _err("config.yaml guardrails.allowed_write_files must include both targets.* paths")
# Heuristic checks: keep them lightweight; warn rather than fail when ambiguous.
for needle in [target_foundation, target_conditions, "main.tex", "extraTex/@config.tex"]:
if needle not in skill_text:
_warn(f"SKILL.md does not mention expected guardrail/target string: {needle}")
info_form = (skill_root / "references" / "info_form.md").read_text(encoding="utf-8")
if re.search(r"(?i)\bNSFC\s*20\d{2}\b", info_form):
_warn("references/info_form.md contains a year-like token (e.g., 'NSFC 2026'); consider keeping it year-agnostic")
print("OK: skill validation passed", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())