
Nsfc Justification Writer
- 330 installs
- 2.6k repo stars
- Updated July 20, 2026
- huangwb8/chineseresearchlatex
nsfc-justification-writer is a Claude Code skill that drafts NSFC grant justification sections explaining project necessity, innovation, and expected impact in the formal tone and structure Chinese funders require for ac
About
nsfc-justification-writer is a Claude Code skill from the chineseresearchlatex repository for drafting National Natural Science Foundation of China (NSFC) grant justification sections. The skill produces formal Chinese-language prose covering project necessity, innovation claims, and expected scientific impact in the structure and tone NSFC reviewers expect. Researchers reach for nsfc-justification-writer when preparing 立项依据 or related justification portions of NSFC proposals and need agent assistance matching funder conventions rather than generic academic writing. The skill complements LaTeX-focused research skills by targeting the argumentative justification layer of Chinese grant applications.
- NSFC justification drafting
- Innovation and significance framing
- Chinese grant conventions
- Structured argumentation
- Funder-aligned tone
Nsfc Justification Writer by the numbers
- 330 all-time installs (skills.sh)
- +3 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #436 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-justification-writerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 330 |
|---|---|
| repo stars | ★ 2.6k |
| Last updated | July 20, 2026 |
| Repository | huangwb8/chineseresearchlatex ↗ |
How do you write NSFC grant justification sections?
Draft NSFC grant justification sections explaining project necessity, innovation, and expected impact in the formal tone and structure Chinese funders require.
Who is it for?
Chinese academic researchers drafting NSFC proposal justification sections who need formal funder-aligned structure and tone.
Skip if: English-language NIH or ERC proposals, or researchers not submitting to China's National Natural Science Foundation.
When should I use this skill?
A researcher asks to draft NSFC 立项依据, justify project necessity and innovation, or write expected impact sections for a Chinese grant proposal.
What you get
Drafted NSFC justification prose covering project necessity, innovation arguments, and expected impact in formal Chinese structure.
- NSFC justification draft
- Innovation and impact argument prose
Files
科研立项依据写作器
与 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 仓库。
输出契约
- 唯一默认写入落点:
extraTex/1.1.立项依据.tex - 禁止改动:
main.tex、extraTex/@config.tex、任何.cls/.sty - 目标:把“为什么要做、现状为什么不够、科学问题是什么、项目如何切入”写清楚
- 默认写作导向是
theoretical,可在config.yaml:style.mode切为mixed或engineering
输入
- 最小信息表优先使用
references/info_form.md - 科学问题与假说口径统一看:
references/scientific_question_guidelines.mdreferences/scientific_hypothesis_guidelines.md- 推荐用
scripts/run.py init帮用户快速生成和补全信息表
硬规则
- 只编辑
extraTex/1.1.立项依据.tex - 优先保留现有
\subsubsection骨架,只替换正文 - 不写无法核验的“国际领先/国内首次”等表述
- 引用外部工作前先要求用户提供 DOI/链接或可核验题录信息
- 若 AI 不可用,必须回退到硬编码能力,不得直接停工
推荐工作流
1. 定位项目与目标文件。 2. 抽取现有小标题骨架与正文范围。 3. 用 scripts/run.py coach --stage auto 判断当前处于 skeleton / draft / revise / polish 哪一阶段。 4. 围绕 4 段闭环组织内容:
- 价值与必要性
- 现状与不足
- 科学问题 / 科学假设
- 本项目切入点与贡献
5. 做可核验性与引用守护,避免吹牛式表述。 6. 检查与 2.1 研究内容 的术语、缩写、指标一致性。 7. 解析目标字数;无显式要求时再用配置兜底。 8. 输出诊断、评审建议或安全写回结果。
关键能力
- Tier1 硬编码诊断:结构、字数、引用键、危险命令、高风险表述提示
- AI 语义能力:内容维度覆盖、吹牛式表述识别、术语一致性、阶段判断、示例推荐
- 安全写入:按
\subsubsection{...}精确替换正文并自动备份 - 可视化报告、diff、rollback、review 建议
常用脚本
scripts/run.py initscripts/run.py coach --stage autoscripts/run.py diagnosescripts/run.py reviewscripts/run.py apply-sectionscripts/run.py diffscripts/run.py rollback
只读集成
- 支持只读访问
research-literature-review的结果目录,用于提取研究现状和验证引用一致性;历史工作区目录名仍为.systematic-literature-review/ - 集成逻辑见
scripts/core/review_integration.py - 该集成是只读的,不得修改综述目录内容
重点参考
references/theoretical_innovation_guidelines.mdreferences/methodology_term_examples.mdreferences/boastful_expression_guidelines.mdreferences/dimension_coverage_design.mdreferences/dod_checklist.mdscripts/README.md
category: biology
keywords:
- 生物学
- 组学
- 单细胞
- 通路
- 因果验证
- 外部验证
difficulty: starter
word_count: 150
structure_version: nsfc2026
description: 生物学方向:机制/通路研究的立项依据结构骨架示例(引用需自行核验后替换)
% 示例(生物):仅供结构与措辞参考
\subsubsection{研究背景}
\justifying\indent
(示例)围绕某疾病机制/关键通路,先用“临床或生物学重大问题→现有证据不足→迫切需要可验证的新机制/新靶点”说明必要性。
\subsubsection{国内外研究现状}
\indent
(示例)按主流技术路线(组学测量/模型系统/因果验证)分 2–3 类概括,强调代表性结论与证据类型;引用需先核验再写 \cite{...}。
\subsubsection{现有研究的局限性}
\indent
(示例)用 2–4 条可验证不足收束:例如样本异质性导致结论不稳、因果链条缺关键节点验证、跨尺度整合不足、可重复性与外部验证缺失等。
\subsubsection{研究切入点}
\indent
(示例)用“差异化切口(因果验证 + 外部验证 + 可复现实验方案)+ 量化指标 + 过渡到研究内容”结束。
category: chemistry
keywords:
- 化学
- 催化
- 反应选择性
- 机理
- 动力学
- 稳定性
difficulty: starter
word_count: 140
structure_version: nsfc2026
description: 化学方向:反应/催化立项依据的结构与语气骨架示例(引用需自行核验后替换)
% 示例(化学):仅供结构与措辞参考
\subsubsection{研究背景}
\justifying\indent
(示例)围绕某关键反应/催化体系,先说明“应用需求→性能瓶颈→现有路线难以兼顾”的必要性与紧迫性。
\subsubsection{国内外研究现状}
\indent
(示例)按主流路线(催化剂设计/反应条件/机理解析)分 2–3 类概括,避免堆砌作者年份;确需引用时先核验 DOI/链接再写 \cite{...}。
\subsubsection{现有研究的局限性}
\indent
(示例)用 2–4 条可量化不足收束:例如活性-选择性权衡、稳定性衰减、放大可重复性不足、机理证据链不闭合等。
\subsubsection{研究切入点}
\indent
(示例)用“差异化切口(结构-性能-机理一体化)+ 可验证指标 + 过渡到研究内容”结束。
category: cs
keywords:
- 隐私
- 联邦学习
- 差分隐私
- 大模型
- 推理
- 可审计
- 威胁模型
difficulty: intermediate
word_count: 620
structure_version: nsfc2026
description: 计算机/信息方向:隐私保护与系统落地的立项依据示例(不含可直接引用条目)
% 示例(计算机):仅供结构与措辞参考
\subsubsection{研究背景}
\justifying\indent
(示例)面向真实业务系统的安全与隐私需求,数据共享受限导致模型难以获得足够多样的分布;同时在线推理与运维约束(时延、成本、可审计)使得“只追求离线精度”的方案难以落地。
\subsubsection{国内外研究现状}
\indent
(示例)现有工作主要集中在三类:其一,隐私保护学习(如联邦学习/差分隐私)缓解数据孤岛;其二,大模型与知识增强提升泛化;其三,系统侧的训练/推理优化提升效率与可用性。上述路线在跨域迁移、端侧约束与可审计性方面仍存在明显差距。
\subsubsection{现有研究的局限性}
\indent
(示例)现有方案往往在“隐私-性能-效率”三者间难以兼顾:隐私增强带来性能衰减,系统优化依赖特定硬件/框架,且缺乏面向真实故障与攻击面的稳健性评估。由此需要提出一个可证伪假说:在明确威胁模型与资源约束下,通过“算法-系统-评估”一体化设计,可在给定指标上实现可量化改进。
\subsubsection{研究切入点}
\indent
(示例)本项目拟从可审计的威胁模型出发,构建可复现基准与评测协议,提出面向资源约束的训练/推理协同方法,并以端到端指标(隐私预算、精度、时延/成本、稳健性)验证有效性,最后自然过渡到后续“研究内容与技术路线”。
category: engineering
keywords:
- 算法工程化
- 系统部署
- 边缘计算
- 时延
- 吞吐
- 鲁棒性
- 资源约束
difficulty: starter
word_count: 120
structure_version: nsfc2026
description: 工程/系统场景:结构与语气骨架示例(强调可复现对比维度)
% 示例(工程):仅供结构与措辞参考
\subsubsection{研究背景}
\justifying\indent
(示例)从应用需求/系统约束出发,明确任务边界、关键指标与工程代价。
\subsubsection{国内外研究现状}
\indent
(示例)按“传统方法/学习方法/系统方法”分层;强调评审可复现的对比维度(数据、指标、算力、部署)。
\subsubsection{现有研究的局限性}
\indent
(示例)聚焦 2–4 条瓶颈:端到端误差来源、鲁棒性、时延/吞吐、跨域迁移等。
\subsubsection{研究切入点}
\indent
(示例)写清“拟解决什么 + 怎么验证 + 产出什么”,并自然引出后续研究内容。
category: materials
keywords:
- 电池
- 催化
- 表界面
- 缺陷工程
- 原位表征
- 相变
- 循环寿命
difficulty: intermediate
word_count: 640
structure_version: nsfc2026
description: 材料方向:结构-界面-性能闭环的立项依据示例(强调可验证对照与指标)
% 示例(材料):仅供结构与措辞参考
\subsubsection{研究背景}
\justifying\indent
(示例)面向高能量密度与长寿命储能器件的需求,关键材料体系在循环稳定性与界面失效方面仍是瓶颈;现有工艺在成本、可规模化与一致性方面也存在约束,因此亟需在材料设计与机理表征上形成可验证的改进路径。
\subsubsection{国内外研究现状}
\indent
(示例)国内外研究主要围绕:成分/结构调控提升本征性能,表界面工程改善副反应,原位/多尺度表征解析失效机理,以及计算辅助筛选加速材料发现。尽管进展显著,但在“结构-界面-性能”因果链的可验证性、复杂工况下的可重复性与可放大工艺方面仍存在不足。
\subsubsection{现有研究的局限性}
\indent
(示例)现有工作常见局限包括:对缺陷/相变的动态演化缺少定量表征,对界面副反应与传质耦合的机制认识不足,样品制备差异导致结果可比性不强。基于此,本项目提出可证伪假说:通过可控缺陷工程与界面调控的协同设计,并配套统一的表征与评价协议,可在关键性能指标上获得可量化提升。
\subsubsection{研究切入点}
\indent
(示例)本项目将以“可控结构单元 + 可追踪界面过程”为切入点,构建材料设计-制备-表征-性能验证闭环,明确对照组与评价指标(容量保持率/倍率/循环寿命/一致性等),并以可交付的材料配方与评价基准支撑后续研究内容展开。
category: math
keywords:
- 数学
- 优化
- 收敛
- 复杂度
- 数值方法
- 误差界
difficulty: starter
word_count: 150
structure_version: nsfc2026
description: 数学/优化方向:理论与方法并重的立项依据结构骨架示例(引用需自行核验后替换)
% 示例(数学/优化):仅供结构与措辞参考
\subsubsection{研究背景}
\justifying\indent
(示例)围绕某类优化/方程问题,先说明“应用或理论驱动→现有理论无法覆盖关键情形→需要新的可证明结果/可计算方法”的必要性。
\subsubsection{国内外研究现状}
\indent
(示例)按主流路线(理论收敛/复杂度分析/数值算法)分 2–3 类概括,强调核心假设与结论边界;确需引用时先核验再写 \cite{...}。
\subsubsection{现有研究的局限性}
\indent
(示例)用 2–4 条可验证不足收束:例如依赖过强假设、缺少统一框架、最坏情形界不紧、数值稳定性与误差控制不足等。
\subsubsection{研究切入点}
\indent
(示例)用“差异化切口(可证明性 + 可计算性)+ 可验收指标(定理/界/算法)+ 过渡到研究内容”结束。
category: medical
keywords:
- 深度学习
- 医学影像
- 临床决策
- 真实世界
- 队列
- 泛化
- 可解释性
difficulty: starter
word_count: 120
structure_version: nsfc2026
description: 医学/临床场景:结构与语气骨架示例(引用需自行核验后替换)
% 示例(医学):仅供结构与措辞参考
\subsubsection{研究背景}
\justifying\indent
(示例)围绕某临床决策场景,先用“疾病负担/诊疗痛点/现有路径不足”说明必要性与紧迫性。
\subsubsection{国内外研究现状}
\indent
(示例)按主流路线分 2–3 类描述,避免堆砌作者年份;确需引用时先核验 DOI/链接再写 \cite{...}。
\subsubsection{现有研究的局限性}
\indent
(示例)用 2–4 条可量化不足收束:例如泛化差、标注成本、可解释性不足、真实世界落地受限等。
\subsubsection{研究切入点}
\indent
(示例)用“差异化切口 + 可验证指标 + 过渡到研究内容”结束。
示例库
目的:提供可对照的“结构骨架 + 语气”参考。示例为演示用途,引用需按你的课题替换并核验。
每个示例可选配套一个 *.metadata.yaml,用于示例推荐(scripts/run.py examples / coach --topic ...)的关键词匹配与分类。
目录:
medical/:医学/临床场景示例engineering/:工程/算法场景示例cs/:计算机/信息方向示例materials/:材料/化学方向示例chemistry/:化学方向示例biology/:生物学方向示例math/:数学/优化方向示例
metadata.yaml 约定(用于示例推荐)
建议字段:
category:示例类别(与目录名一致即可)keywords:关键词列表(用于匹配)description:一句话描述(用于输出展示)difficulty:starter|intermediate|advanced(可选)word_count:示例字数(可选)structure_version:结构版本标识(可选,例如 nsfc2026)
terminology:
dimensions:
研究对象:
系统/平台: ["系统", "平台", "框架", "架构", "pipeline"]
数据集: ["数据集", "数据", "dataset", "benchmark"]
基线方法: ["基线", "baseline", "对照方法", "对照组"]
指标:
时延: ["时延", "延迟", "latency", "响应时间"]
吞吐: ["吞吐", "吞吐量", "throughput", "QPS", "TPS"]
资源约束: ["算力", "计算预算", "资源约束", "compute budget", "显存", "内存"]
能耗: ["能耗", "功耗", "energy", "power"]
鲁棒性: ["鲁棒性", "robustness", "稳定性"]
术语:
消融: ["消融", "ablation", "消融实验"]
泛化: ["泛化", "generalization", "泛化能力"]
可扩展性: ["可扩展性", "scalability", "扩展性"]
terminology:
dimensions:
研究对象:
研究对象: ["患者", "病例", "受试者", "队列", "样本", "就诊者", "入组者"]
纳排标准: ["纳入标准", "入组标准", "排除标准", "纳排标准"]
对照组: ["对照组", "对照", "对照人群", "对照队列"]
干预: ["干预", "治疗", "处理", "用药", "术式"]
指标:
结局指标: ["结局", "终点", "主要终点", "次要终点", "预后", "预后结局"]
诊断性能: ["敏感度", "特异度", "准确率", "精确度", "召回率", "precision", "recall"]
AUC: ["AUC", "ROC-AUC", "auc"]
风险分层: ["风险分层", "风险评分", "风险评估", "分层"]
术语:
回顾性: ["回顾性", "回顾性研究"]
前瞻性: ["前瞻性", "前瞻性研究"]
随机对照: ["随机对照", "随机", "RCT", "randomized controlled trial"]
你是 NSFC 立项依据的“评审人视角质疑生成器”。
输入:
- dod_checklist: 验收清单(要点)
- tier1: 硬编码诊断结果(结构/引用/字数/不可核验表述)
- tex: 立项依据正文(可截断)
任务:输出 markdown(不要写 LaTeX),包含两部分:
1) 评审人可能会问的 8-12 个问题(每条可直接用于修改)
2) 对应的 8-12 条可执行修改建议(尽量给到“改哪里/怎么改/验证标准”)
约束:
- {style_preamble}
- 不要杜撰引用与 DOI;如需要引用,用“需用户提供 DOI/链接(或可核验题录信息),并先补齐 references/*.bib”
- 避免绝对化表述(国际领先/国内首次等)
- 关注“瓶颈→科学问题约束→科学假设→研究切入点”的逻辑闭环:是否存在漏配/凭空新增/自相矛盾
dod_checklist:
{dod_checklist}
tier1(json):
{tier1_json}
tex:
{tex}
你是 NSFC 立项依据“语义诊断器”。请基于以下 LaTeX 文本,输出诊断要点(JSON):
字段:
- logic: 逻辑连贯性问题(列表)
- terminology: 术语/缩写不一致问题(列表)
- evidence: 证据不足/不可量化陈述(列表)
- suggestions: 3-6 条可执行修改建议(列表)
要求:
- 只输出 JSON
- 不生成新的引用;若需要引用,请提示“需用户提供 DOI/链接(或可核验题录信息),并先补齐 references/*.bib”
- 重点关注“瓶颈/局限性 → 科学问题约束(疑问句) → 科学假设(陈述句) → 切入点 → 研究内容”的闭环是否存在:漏配、凭空新增约束、把研究目标当科学问题、把验证方式写进假设
LaTeX 文本:
{tex}
你是 NSFC 立项依据的"渐进式写作教练"。
目标:帮助用户用最小压力完成 1.1 立项依据,从"骨架 → 段落 → 逻辑闭环 → 润色 → 验收"逐步推进。
{style_preamble}
输入:
- stage: skeleton|draft|revise|polish|final(或 auto)
- info_form: 用户已提供的信息(可能不完整)
- tier1: 结构/引用/字数/质量硬编码诊断
- term_matrix: 跨章节术语一致性矩阵(可为空)
- tex: 当前立项依据(可为空)
输出:markdown,格式固定为:
## 当前阶段判断
一句话说明当前处于哪个阶段以及原因。
## 本轮只做三件事
1) ...
2) ...
3) ...
## 需要你补充/确认的问题(不超过 8 个)
- ...
## 下一步可直接复制的写作提示词
给出 1 段可复制的提示词,用于让写作助手生成/修改某个 \\subsubsection 的正文(必须强调:不新增引用/引用先核验)。
约束:
- 永远先保证结构不被破坏
- 永远优先可核验性与术语一致性
- 遵循"写作导向"要求,并把要求落到可验证锚点/对照维度
- 科学问题必须是“疑问句”(追问认知缺口),避免写成“能否构建/开发/实现...”的研究目标
- 科学假设必须是“陈述句”(预测性结果),避免写“在...验证中/通过...验证”等验证方式
方法学术语使用规范(重要):
- 立项依据的核心驱动力是"科学问题与假说",方法学术语仅作为验证手段的背景说明
- 禁止用方法术语撑段落主线:每段主句的主语应为"科学问题/假说/理论空白"而非"方法/技术/平台"
- 方法术语的合理位置:
- ✅ 在"现状与不足"中简述主流方法路线(为指出理论局限性做铺垫)
- ✅ 在"验证方式"中提及验证手段(理论证明/数值验证/对照实验)
- ❌ 不作为段落主句的主语
- 检查方法:每段主句的主语/核心驱动力是否为"科学问题/假说/理论空白"而非"方法/技术/平台"
assets/(资源目录)
本目录存放 可复用的静态资源,避免散落在 skill 根目录,便于维护与移植。
约定:
assets/prompts/:可配置 Prompt 文件(由config.yaml:prompts.*指向)assets/templates/:模板文件(如 HTML 报告模板、结构骨架模板)assets/examples/:示例库(*.tex+*.metadata.yaml),用于scripts/run.py examples/coach --topic推荐参考骨架assets/presets/:学科预设(--preset <name>),用于覆盖术语维度等配置
说明:
- 历史路径
prompts/、templates/、examples/、config/presets/已迁移到此处;代码仍保留兼容回退。 - 仓库不再保留
config/目录;如你手头仍有旧的config/presets/<name>.yaml,可自行在 skill 根目录创建该路径,或直接迁移到assets/presets/。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>$title</title>
<style>
:root {
--bg: #0b1020;
--panel: #111936;
--text: #e7ecff;
--muted: #a7b1d6;
--ok: #5eead4;
--warn: #fbbf24;
--bad: #fb7185;
--codebg: #0a0f22;
--line: rgba(255, 255, 255, 0.08);
--hl: rgba(251, 191, 36, 0.22);
--hlbad: rgba(251, 113, 133, 0.26);
}
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
background: radial-gradient(1200px 700px at 10% 0%, rgba(94, 234, 212, 0.12), transparent),
radial-gradient(1200px 700px at 90% 20%, rgba(251, 191, 36, 0.10), transparent), var(--bg);
color: var(--text);
line-height: 1.5;
}
a { color: inherit; text-decoration: none; }
.wrap { max-width: 1080px; margin: 0 auto; padding: 18px 16px 36px; }
.top { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
h1 { margin: 0; font-size: 20px; letter-spacing: 0.3px; }
.meta { color: var(--muted); font-size: 12px; }
.grid { display: grid; grid-template-columns: 1fr; gap: 12px; margin-top: 12px; }
@media (min-width: 980px) { .grid { grid-template-columns: 1fr 1fr; } }
.card {
background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
border: 1px solid rgba(255,255,255,0.10);
border-radius: 12px;
padding: 12px 12px 10px;
backdrop-filter: blur(8px);
}
.card h2 { margin: 0 0 8px; font-size: 14px; color: #d8deff; }
.kvs { display: grid; grid-template-columns: 1fr; gap: 6px; }
.kv { display: flex; gap: 8px; align-items: baseline; }
.k { color: var(--muted); font-size: 12px; min-width: 120px; }
.v { font-size: 13px; }
.ok { color: var(--ok); }
.warn { color: var(--warn); }
.bad { color: var(--bad); }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.code {
background: var(--codebg);
border: 1px solid rgba(255,255,255,0.10);
border-radius: 12px;
overflow: hidden;
}
.codehdr {
display: flex; justify-content: space-between; align-items: center;
padding: 10px 12px;
background: rgba(255,255,255,0.04);
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 12px;
}
.actions { display: flex; align-items: center; gap: 10px; }
.hint { color: rgba(167, 177, 214, 0.85); }
.btn {
border: 1px solid rgba(255,255,255,0.14);
background: rgba(255,255,255,0.06);
color: var(--text);
padding: 4px 10px;
border-radius: 10px;
cursor: pointer;
font-size: 12px;
}
.btn:hover { background: rgba(255,255,255,0.10); }
.codelines { margin: 0; padding: 0; list-style: none; }
.line {
display: grid;
grid-template-columns: 72px 1fr;
gap: 12px;
padding: 0 12px;
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.ln {
color: rgba(167, 177, 214, 0.75);
border-right: 1px solid rgba(255,255,255,0.06);
padding: 4px 0;
text-align: right;
padding-right: 12px;
}
.src {
padding: 4px 0;
white-space: pre-wrap;
word-break: break-word;
}
.tag { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; margin-left: 6px; }
.tag.ok { background: rgba(94, 234, 212, 0.14); border: 1px solid rgba(94, 234, 212, 0.35); }
.tag.warn { background: rgba(251, 191, 36, 0.14); border: 1px solid rgba(251, 191, 36, 0.35); }
.tag.bad { background: rgba(251, 113, 133, 0.14); border: 1px solid rgba(251, 113, 133, 0.35); }
.hl { background: var(--hl); border-radius: 4px; padding: 0 2px; }
.hlbad { background: var(--hlbad); border-radius: 4px; padding: 0 2px; }
.sec { margin-top: 12px; }
.md { color: #d9def8; font-size: 13px; }
.md ul { margin: 8px 0 0 18px; padding: 0; }
.md li { margin: 4px 0; }
table { width: 100%; border-collapse: collapse; overflow: hidden; border-radius: 10px; }
th, td { border-bottom: 1px solid var(--line); padding: 6px 8px; font-size: 12px; }
th { text-align: left; color: var(--muted); }
tr:hover td { background: rgba(255,255,255,0.03); }
.toast {
position: fixed;
left: 50%;
bottom: 18px;
transform: translateX(-50%);
background: rgba(17, 25, 54, 0.92);
border: 1px solid rgba(255,255,255,0.14);
color: var(--text);
padding: 8px 12px;
border-radius: 999px;
font-size: 12px;
z-index: 9999;
}
</style>
</head>
<body>
<div class="wrap">
<div class="top">
<h1>$headline</h1>
<div class="meta">$meta</div>
</div>
<div class="grid">
<div class="card">
<h2>Tier1 诊断摘要</h2>
$tier1_summary_html
</div>
<div class="card">
<h2>下一步建议</h2>
$next_steps_html
</div>
</div>
<div class="sec">
<div class="card">
<h2>Tier2(可选)</h2>
$tier2_html
</div>
</div>
<div class="sec">
<div class="card">
<h2>跨章节术语一致性</h2>
$terms_html
</div>
</div>
<div class="sec">
<div class="code">
<div class="codehdr">
<div>文件:<span class="mono">$target_relpath</span></div>
<div class="actions">
<span class="hint">点击行号:跳转并复制行号;Shift+点击:复制链接</span>
<button class="btn" id="copy-page-link" type="button">复制页面链接</button>
</div>
</div>
<ol class="codelines">$code_lines_html</ol>
</div>
</div>
</div>
<div id="toast" class="toast" style="display:none;"></div>
<script>
const toast = document.getElementById("toast");
const showToast = (msg) => {
if (!toast) return;
toast.textContent = msg;
toast.style.display = "block";
clearTimeout(window.__toastTimer);
window.__toastTimer = setTimeout(() => { toast.style.display = "none"; }, 1200);
};
const copyText = async (text) => {
try {
await navigator.clipboard.writeText(text);
showToast("已复制:" + text);
} catch (e) {
showToast("复制失败(浏览器权限限制)");
}
};
document.addEventListener("click", (ev) => {
const ln = ev.target && ev.target.closest ? ev.target.closest("a.ln") : null;
if (!ln) return;
const id = (ln.getAttribute("id") || "").trim();
if (!id) return;
const base = window.location.href.split("#")[0];
const text = ev.shiftKey ? (base + "#" + id) : id;
copyText(text);
});
const btn = document.getElementById("copy-page-link");
if (btn) {
btn.addEventListener("click", () => copyText(window.location.href));
}
</script>
</body>
</html>
常用句式(可核验优先,理论创新导向默认)
默认优先采用理论创新导向表述,详见 references/theoretical_innovation_guidelines.md价值与必要性(理论创新导向默认)
- "XXX 领域存在理论空白/认知缺失(如因果关系不明/表征不足/假设过强),导致对 YYY 现象缺乏解释力;亟需建立统一的理论框架/方法学"
- "现有理论在 XXX 场景下存在因果缺失/范式不统一,难以解释 contradictory findings;因此需要..."
- (工程项目可用过渡句)"上述理论缺失导致在实际应用中出现了性能瓶颈/一致性不足;因此亟需从理论层面突破"
现状与不足(理论创新导向默认)
- "现有研究主要沿着 A/B/C 三条路线展开,但理论层面仍受限:① 依赖过强假设(如...) ② 缺乏统一框架导致结论难以整合 ③ 因果机制尚未阐明"
- "现有方法的理论局限性在于:① 假设过强(XXX)在真实场景难以成立 ② 最坏情形界不紧(XXX)无法提供可靠保证 ③ YYY 与 ZZZ 之间的因果链条缺失"
科学问题/假说(理论创新导向默认)
- "本项目提出可证伪假说:通过引入 XXX 表征/YYY 约束,可在弱假设下获得 ZZZ 性质;据此提出科学问题:① 在约束 A 下 ZZZ 性质是否仍成立?② XXX 与 YYY 的因果链条遵循什么机制/在什么条件下成立?"
- "核心假设:XXX 机制的阐明将为 YYY 提供统一的理论范式;科学问题:① XXX 的关键驱动因素是什么?② 在 ZZZ 约束下上述机制是否仍成立/边界条件是什么?"
切入点与贡献(理论创新导向默认)
- "本项目拟从 XXX 这一切口入手,建立 YYY;通过理论证明/数值验证/对照实验验证其有效性,并形成 ZZZ 理论框架/方法学作为可交付成果"
- "预期理论贡献:① 提供 XXX 的统一表征 ② 证明 YYY 条件下的 ZZZ 定理 ③ 建立可外推的理论范式"
工程项目过渡句(仅在必要时使用)
- "上述理论突破将为后续的工程实现/系统优化提供理论基础(具体技术路线将在 2.1 研究内容中展开)"
% 立项依据结构模板(与 projects/NSFC_Young/extraTex/1.1.立项依据.tex 对齐)
\subsubsection{研究背景}
\justifying\indent
% 建议写作要点:痛点/需求→影响范围或成本→为何现在必须做(1–2 段即可)
\subsubsection{国内外研究现状}
\indent
% 建议写作要点:主流路线与代表性工作→2–4 条明确不足(尽量可量化/可验证,避免口号)
\subsubsection{现有研究的局限性}
\indent
% 建议写作要点:2–4 条关键瓶颈→1–3 个科学问题(疑问句,非研究目标;约束与瓶颈一一映射)→一句科学假设(陈述句,预测性结果,不写验证方式)→验证维度(数据/指标/对照/消融)
\subsubsection{研究切入点}
\indent
% 建议写作要点:差异化切口→可交付成果与指标→最后 1 句承上启下引到 2.1 研究内容
Changelog
All notable changes to this skill will be documented in this file.
The version number is the single source of truth in config.yaml (skill_info.version).
[Unreleased]
(暂无)
[1.0.0] - 2026-02-24
Changed
config.yaml:版本号0.7.9 → 1.0.0,标记为正式稳定版本
[0.7.9] - 2026-02-22
Added
- 第三方约束(瘦身提质)诊断预警:预估页数(经验估算)、核心文献数(去重 cite keys)、开篇 300 字信号检查(启发式)
Changed
- 配置兜底字数调整为 9000±800,并新增
constraints.*约束区间(页数/字数/文献数量/开篇长度) test-session将 pytest/python 缓存隔离到会话目录,保证测试中间产物可追溯且集中收口
[0.7.8] - 2026-02-17
Added
- 科学问题与科学假设写作要点参考文档(用于“瓶颈→约束→问题→假设”的闭环自检)
Changed
- 信息表与写作教练:强化“科学问题≠研究目标”“假设不写验证方式”“瓶颈→约束映射”提示
- 信息表生成标题去年份化(避免时间敏感硬编码)
# ═══════════════════════════════════════════════════════════════════════
# nsfc-justification-writer 配置文件
# ═══════════════════════════════════════════════════════════════════════
# 本文件是 nsfc-justification-writer 的配置单一真相来源(Single Source of Truth)
# 修改配置时,请优先编辑本文件,而非 scripts/core/config_loader.py
# ═══════════════════════════════════════════════════════════════════════
skill_info:
name: nsfc-justification-writer
version: 1.0.0
category: writing
author: "Bensz Conan"
# 写作导向(可配置)
# - theoretical:理论创新导向(默认)
# - mixed:理论 + 应用混合导向
# - engineering:工程/应用导向
style:
mode: theoretical
parameters:
project_root:
type: string
required: true
description: 项目根目录(如 projects/NSFC_Young 或你的标书项目路径)
output_mode:
type: string
required: false
default: apply
allowed_values: [preview, apply]
description: preview 仅输出建议文本;apply 则写入目标 .tex 文件(如由平台/工具支持)
word_count_target:
type: integer
required: false
default: 9000
description: 目标字数(中文字符数,不含 LaTeX 命令/注释;兜底值,优先从用户意图/信息表/学科预设解析)
word_count_tolerance:
type: integer
required: false
default: 800
description: 允许偏差范围(兜底值;若用户给出区间/±范围则优先使用)
targets:
justification_tex: extraTex/1.1.立项依据.tex
related_tex:
research_content: extraTex/2.1.研究内容.tex
research_foundation: extraTex/3.1.研究基础.tex
bib_globs:
- references/*.bib
- references/**/*.bib
workspace:
# runs/ 属于运行时产物:统一放在 tests/_artifacts/ 下(测试/运行产物集中收口)
runs_dir: tests/_artifacts/runs
limits:
# 文件大小限制(用于决定是否启用“流式分块”读取;避免一次性加载超大文件造成峰值内存)
max_file_size_mb: 5
# AI 输入字符限制(用于语义分析、术语一致性等)
ai_max_input_chars: 20000
# 写作教练预览字符数(用于快速展示当前状态/阶段判定)
writing_coach_preview_chars: 3000
# 字数目标范围限制(用于解析用户意图/信息表时的安全夹紧)
word_target:
min: 100
max: 20000
ai:
enabled: true
tier2_chunk_size: 12000
tier2_max_chunks: 20
# 运行时缓存统一放在 tests/_artifacts/ 下(测试/运行产物集中收口)
cache_dir: tests/_artifacts/cache/ai
prompts:
tier2_diagnostic: assets/prompts/tier2_diagnostic.txt
review_suggestions: assets/prompts/review_suggestions.txt
writing_coach: assets/prompts/writing_coach.txt
references:
# 默认严格:发现缺失 bibkey 的 \\cite{...} 直接拒绝写入(防止幻觉引用)
allow_missing_citations: false
structure:
# 仅作为"推荐模板",不再用作严格标题匹配的硬性规则(标题是形式,内容维度是本质)
recommended_subsubsections:
- 研究背景
- 国内外研究现状
- 现有研究的局限性
- 研究切入点
# 默认 false:允许用户改写标题用词(结构检查不再机械匹配标题字符串)
strict_title_match: false
# strict_title_match=false 时启用"模糊标题匹配"(必要时会调用 AI 在候选标题中做语义匹配)
min_title_similarity: 0.6
min_subsubsection_count: 4
# 启用"内容维度覆盖检查"(不依赖标题用词;AI 不可用时回退启发式)
enable_dimension_coverage_check: true
guardrails:
allowed_write_files:
- extraTex/1.1.立项依据.tex
forbidden_write_files:
- main.tex
- extraTex/@config.tex
forbidden_write_globs:
- "**/*.cls"
- "**/*.sty"
quality:
# 高风险示例(用于快速提示;不穷举同义词变体)
high_risk_examples:
- 国际领先
- 国内首次
- 世界领先
- 填补空白
# 默认 false:示例词命中仅警告,不作为阻断(避免"玩文字游戏")
strict_mode: false
# 启用 AI 语义判断(识别"可能引起评审不适的表述")
enable_ai_judgment: true
ai_judgment_mode: semantic # semantic | keyword(兜底)
avoid_commands:
- "\\section"
- "\\subsection"
- "\\input"
- "\\include"
# 第三方约束(“瘦身提质”写作指标):
# - 目标场景:NSFC 正文原则不超过 30 页,其中立项依据建议 6-10 页(推荐 6-8 页)
# - 本处仅做“诊断+预警”,不作为写入阻断(避免被历史遗留内容卡死)
constraints:
page_limit:
min: 6
max: 10
recommended: [6, 8]
warning_threshold: 9
# 经验估算:小四/1.5 倍行距下,约 1000 个中文字符 ≈ 1 页(仅用于预警,不代表最终排版)
chars_per_page: 1000
word_count:
min: 8000
max: 10000
references:
min: 30
max: 50
opening:
cjk_chars: 300
_note: "开篇 300 字尽量直击核心:领域卡点/局限 -> 为什么难 -> 本项目的突破式切入(仅做启发式检查)"
word_count:
target: 9000
tolerance: 800
_note: "9000 字为兜底值;实际目标字数优先从用户意图/信息表/学科预设动态解析"
# cjk_only: 仅剔除注释后统计 CJK 字符数(默认)
# cjk_strip_commands: 粗剔除命令/数学环境/类代码环境后统计(更接近"正文字符数"的估计)
mode: cjk_only
terminology:
# auto: AI 可用则叠加 AI 语义检查,否则仅 legacy 规则矩阵
# ai: 强制尝试 AI(不可用则回退)
# legacy: 仅使用硬编码维度/别名组
mode: auto
enable_ai_semantic_check: true
ai_mode: auto # auto | semantic_only | legacy_only
ai:
enabled: true
max_chars: 20000
dimensions:
研究对象:
研究对象: ["患者", "病例", "受试者", "样本"]
指标:
准确率: ["准确率", "精确度"]
术语:
深度学习: ["深度学习", "DL"]
writing_coach:
# 默认启用 AI 阶段推断(AI 不可用时自动回退到硬编码规则)
enable_ai_stage_inference: true
ai_inference_mode: auto # auto | ai_only | fallback
fallback_rules:
draft_threshold_ratio: 0.4
draft_min_chars: 600
# research-literature-review 集成配置(历史目录名仍为 .systematic-literature-review)
slr_integration:
# 启用 research-literature-review 目录检测
enabled: true
# 标记文件夹名称(用于识别 research-literature-review 生成的历史工作区目录)
marker_folder: ".systematic-literature-review"
# 只读访问保护(禁止写入 research-literature-review 生成的目录)
read_only: true
# 自动验证引用一致性
validate_citations: true
# 允许的文件模式(用于过滤 .tex 和 .bib 文件)
tex_patterns:
- "*.tex"
- "*_review.tex"
bib_patterns:
- "*.bib"
- "*_参考文献.bib"
- "references.bib"
nsfc-justification-writer
用于科研申请书"立项依据"章节的写作/重构:把"价值与必要性、现状不足、科学问题/假说、切入点与贡献"写成一段可直接落到 LaTeX 模板的正文,并保持模板结构不被破坏。适用于 NSFC 及各类科研基金申请书的立项依据写作场景。
⚠️ AI 能力建议:本 skill 涉及复杂的科学问题识别、假说可证伪性判断、理论创新导向写作等高阶任务,建议使用你当前运行环境中“最高档位/最强能力”的模型或配置,以获得更稳定的逻辑与表达质量。
主推"渐进式写作引导"(coach),配合"诊断→(分步写作)→安全写入→验收"形成闭环。
AI 能力默认来自运行环境的 Claude Code / Codex 原生智能,无需额外配置外部 API Key;不可用时自动回退到硬编码能力。
路径提示:
- 在本仓库根目录运行:python skills/nsfc-justification-writer/scripts/run.py ...- 在本 skill 目录运行:python scripts/run.py ...能力亮点:
- 默认不强制标题精确匹配,改为检查“价值/现状/科学问题/切入点”内容维度覆盖(AI + 兜底启发式)
- AI 语义识别“吹牛式表述”(绝对化/填补空白/无依据夸大/自我定性)并给出改写建议,高风险词仅提示不做机械阻断
- 目标字数优先从用户意图/信息表的“字数/范围/±容差”解析,再用配置兜底
- 第三方约束预警(瘦身提质):预估页数(经验估算)/核心文献数(去重 cite keys)/开篇 300 字“卡点+突破”信号检查(启发式,默认不阻断写入)
coach --stage auto支持 AI 阶段判断(skeleton/draft/revise/polish/final),AI 不可用则回退到硬编码阈值- 写作导向可配置:
style.mode=theoretical|mixed|engineering(默认theoretical)
推荐用法(Prompt 模板)
开发者建议:多轮对话优化立项依据
使用 Claude Code / Codex CLI 时,建议先让 skill 在本轮对话全局生效,便于多轮迭代优化:
接下来,我要使用 nsfc-justification-writer 这个skill 优化立项依据。仅使用skill,不要修改skill的任何文件。保持这个skill在本轮对话全局生效。先做好准备,不要开始干活。你准备好了吗?然后在同一轮对话中持续使用 coach→diagnose→apply 的闭环,直到满意为止。
---
1)主推:渐进式写作引导(骨架→段落→修订→润色→验收)
第一步先跑引导(不需要你一步到位写完):
python skills/nsfc-justification-writer/scripts/run.py coach --project-root projects/NSFC_Young --stage auto --topic "你的课题一句话"如果你已经进入本 skill 目录,也可以用更短的写法:
python scripts/run.py coach --project-root projects/NSFC_Young --stage auto --topic "你的课题一句话"按 coach 输出的“下一步可直接复制的写作提示词”去生成某个小标题正文后,用 apply-section 安全写入;再重复 coach→apply 的迭代,直到 diagnose 通过。
2)从零生成(一次性写完)
请使用 nsfc-justification-writer:
目标项目:projects/NSFC_Young
主题:<一句话题目/方向>
信息表:<按 references/info_form.md 提供>
输出:写入 extraTex/1.1.立项依据.tex3)基于已有草稿重构
请使用 nsfc-justification-writer 重构(强调逻辑闭环与可核验性),不要改 main.tex:
目标项目:<你的项目路径>
现有草稿:<粘贴或指向 extraTex/1.1.立项依据.tex>
补充信息:<按 references/info_form.md 缺啥补啥>开发者建议的Prompt如下:
立项依据的{某个写得不满意的部分} 和前面的xxx主题差得有点远{或者其它你觉得不满意的地方}。 请使用nsfc-justification-writer这个skill进行优化。输出文件
extraTex/1.1.立项依据.tex
配置(可选)
全局配置加载顺序(后者覆盖前者): 1. skills/nsfc-justification-writer/config.yaml 2. skills/nsfc-justification-writer/assets/presets/<preset>.yaml(可选;兼容旧路径 config/presets/:如你有旧文件可自行创建该目录) 3. ~/.config/nsfc-justification-writer/override.yaml(可选,可用 --no-user-override 关闭) 4. --override /path/to/override.yaml(可选,优先级最高)
示例:
python skills/nsfc-justification-writer/scripts/run.py --preset medical diagnose --project-root projects/NSFC_Young
python skills/nsfc-justification-writer/scripts/run.py --override /path/to/override.yaml terms --project-root projects/NSFC_Young配套脚本(可选但推荐)
python skills/nsfc-justification-writer/scripts/run.py diagnose --project-root projects/NSFC_Young
python skills/nsfc-justification-writer/scripts/run.py wordcount --project-root projects/NSFC_Young
python skills/nsfc-justification-writer/scripts/run.py refs --project-root projects/NSFC_Young
python skills/nsfc-justification-writer/scripts/run.py terms --project-root projects/NSFC_Young
python skills/nsfc-justification-writer/scripts/run.py review --project-root projects/NSFC_Young
python skills/nsfc-justification-writer/scripts/run.py coach --project-root projects/NSFC_Young --stage auto
python skills/nsfc-justification-writer/scripts/run.py check-ai安全写入(替换指定 \\subsubsection{...} 的正文):
python skills/nsfc-justification-writer/scripts/run.py apply-section \\
--project-root projects/NSFC_Young \\
--title "国内外研究现状" \\
--body-file /path/to/new_body.txt标题未命中时输出候选(便于修正 --title):
python skills/nsfc-justification-writer/scripts/run.py apply-section \\
--project-root projects/NSFC_Young \\
--title "现状" \\
--body-file /path/to/new_body.txt \\
--suggest-alias可视化诊断报告(HTML):
python skills/nsfc-justification-writer/scripts/run.py diagnose --project-root projects/NSFC_Young --html-report autoFAQ
- Q:AI 能力为什么有时“不生效”?
A:本仓库脚本默认不假设可直接调用宿主 AI;需要运行环境注入 responder 才会启用 AI(不可用会自动回退到硬编码能力)。可先运行 python skills/nsfc-justification-writer/scripts/run.py check-ai 查看当前是否处于降级模式。
- Q:为什么 `apply-section` 会拒绝写入?
A:默认严格:若新正文里出现 \\cite{...} 但项目 references/*.bib 找不到对应 key,会拒绝写入以避免“幻觉引用”。先用 refs 生成核验清单/可复制提示词,按提示补齐 references/*.bib 后再写入。 如你使用 --allow-missing-citations 放宽该检查,建议同时加 --strict-quality 启用“新正文质量闸门”(命中绝对化表述/危险命令则拒绝写入)。
- Q:我想按学科调整术语一致性检查怎么做?
A:先试 --preset medical/engineering(已提供更丰富的三维矩阵示例),或写一个 override.yaml 覆盖 terminology.dimensions(推荐):
terminology:
dimensions:
研究对象:
研究对象: ["患者", "受试者", "样本"]
指标:
AUC: ["AUC", "ROC-AUC"]
术语:
深度学习: ["深度学习", "DL"]如需临时关闭该检查,可设置 terminology.dimensions: {}(或兼容的 terminology.alias_groups: {})。如需叠加 AI 语义检查,可设置 terminology.mode: auto/ai(AI 不可用时会自动回退到矩阵规则)。
- Q:行号怎么复制?
A:HTML 报告里点击行号会复制 Lxx;Shift+点击 复制带锚点链接(便于讨论定位)。
更多文档
skills/nsfc-justification-writer/references/docs/tutorial.mdskills/nsfc-justification-writer/references/docs/architecture.md
版本回滚:
python skills/nsfc-justification-writer/scripts/run.py list-runs
python skills/nsfc-justification-writer/scripts/run.py diff --project-root projects/NSFC_Young --run-id <某次apply/rollback的run_id>
python skills/nsfc-justification-writer/scripts/run.py rollback --project-root projects/NSFC_Young --run-id <run_id> --yes“可能引起评审不适的表述”识别:判别与改写指南
本文件用于解释 scripts/core/boastful_expression_checker.py 的设计边界:什么需要标记、什么是可接受表述,以及如何改写为可核验/可对照的写法。
总原则
评审不反感“好结果”,反感的是:
- 没有对照:只说“更好/领先”,但不说明比谁好、好多少
- 没有证据:没有数据、没有引用、没有实验设计支撑
- 自我定性:用形容词替代论证(重大/关键/突破),且缺少可验证锚点
常见类别与示例
1) 绝对化表述
- 典型:国际领先、世界首创、国内首次、唯一
- 风险:几乎不可证明,且容易触发评审反感
2) 填补空白式
- 典型:填补空白、开创性、颠覆性
- 风险:未说明“空白是什么”“空白如何验证”
3) 无依据夸大
- 典型:重大意义、关键作用、显著提升(无具体指标)
- 风险:缺少对照组/统计检验/效果量
4) 自我定性
- 典型:本研究具有创新性/先进性(只做判断,不给证据)
- 风险:把结论当事实陈述
可接受(通常 OK)的写法
- 有对照维度:相比基线方法/现有方案/指南推荐方案
- 有指标与范围:准确率提升 2–5%,AUC 提升 0.03–0.06
- 有证据锚点:数据集规模、对照设计、统计检验、或可追溯引用(bibkey+DOI)
改写模板(从“定性”到“可核验”)
- ❌ 国际领先 → ✅ “在 XX 数据集上,相比 YY 方法,指标 ZZ 提升 3–6%(n=…,p<0.05)”
- ❌ 填补空白 → ✅ “针对 XX 场景现有方法缺少 YY 能力,本项目提供 ZZ,并以 AA/BB 作为验收指标”
- ❌ 重大意义 → ✅ “若实现 XX,可降低 YY 成本/时间/并发症风险;以 ZZ 作为量化指标评估”
可配置项
字符截断与输入上限使用 config.yaml:
limits.ai_max_input_chars:AI 输入字符上限(避免超长文本导致成本/延迟显著上升)
诊断报告示例(节选)
示例 1:结构完整但引用缺失
诊断结果:
- ✅ 结构完整:subsubsection=4
- ❌ 引用缺失:.bib 未找到 keys:Smith2020, Super1957
- ℹ️ 字数统计(中文字符,不含注释):2210
- ⚠️ 不可核验表述:国际领先
建议:
- 先补齐/核验 BibTeX 条目(优先提供 DOI/链接或可核验题录信息),再在正文中使用
\\cite{...}。 - 将“国际领先/国内首次”等绝对表述替换为可核验描述(如“相对现有方法在 XXX 指标上仍存在 ……”)。
示例 2:结构缺失(阻塞)
诊断结果:
- ❌ 结构缺失:subsubsection=2,缺少:研究背景、国内外研究现状、现有研究的局限性、研究切入点
- ✅ 引用格式:所有 \\cite{...} 均在 .bib 中存在
- ℹ️ 字数统计(中文字符,不含注释):830
说明:
- 结构不完整时建议先补齐 4 个
\\subsubsection骨架,再写正文;避免直接“硬写一大段”导致后续章节对齐困难。
内容维度覆盖检查:设计说明
本文件说明 内容维度覆盖检查 的设计目的、检查维度、输出格式与回退策略,便于维护与自定义。
设计目的
- 避免“只改标题不补内容”的结构性空洞:标题存在但叙事闭环缺失
- 降低对标题用词的依赖:允许用户改写
\\subsubsection标题,但仍能检查内容是否覆盖关键叙事维度 - 给出可执行的“缺什么补什么”建议,支持渐进式写作
四个维度定义(叙事闭环)
1) 价值与必要性
- 回答:为什么要做、痛点是什么、影响范围/成本/迫切性
2) 现状与不足
- 回答:主流路线/代表性工作是什么、局限性是什么、瓶颈在哪里
3) 科学问题/假说
- 回答:核心假说是什么、关键科学问题是什么、如何可验证/可证伪
4) 项目切入点
- 回答:本项目相对现有工作的差异化切口是什么、准备怎么做、如何验证
输出 JSON(契约)
scripts/core/dimension_coverage.py 输出结构:
{
"dimensions": {
"价值与必要性": {"covered": true, "confidence": 0.95, "evidence": "…"},
"现状与不足": {"covered": true, "confidence": 0.88, "evidence": "…"},
"科学问题/假说": {"covered": false, "confidence": 0.40, "evidence": ""},
"项目切入点": {"covered": true, "confidence": 0.83, "evidence": "…"}
},
"missing_dimensions": ["科学问题/假说"],
"suggestions": ["建议补充核心假说…"],
"_mode": "ai|fallback"
}字段约束:
evidence:原文片段(不超过 50 字),不得杜撰引用与 DOIconfidence:0–1 之间的置信度(AI 模式由模型给出;fallback 模式为启发式估计)
回退策略(fallback)
当 AI 不可用/异常时,使用启发式规则:
- 去注释后文本中匹配“维度信号词”(如“痛点/不足/假说/本项目将…”)
- 若命中则视为
covered=true,否则列入missing_dimensions suggestions只提供少量、可直接补写的提示语句
该回退策略不追求完美,只用于:
- 防止检查完全失效
- 给写作流程提供方向性提示
可配置项
字符截断与输入上限使用 config.yaml:
limits.ai_max_input_chars:AI 输入字符上限(同时影响术语一致性、吹牛式表述等语义任务)
架构说明:nsfc-justification-writer
目标:把“立项依据”的写作流程拆成两部分:
- 脚本/硬编码能力:可复现、可验收、可回滚(诊断、定位、写入、报告)
- AI(可选):只负责生成/改写文字,且始终有 fallback 方案
目录结构(核心)
scripts/run.py:CLI 入口(diagnose/coach/apply-section/refs/terms/review/diff/rollback)scripts/core/hybrid_coordinator.py:协同编排(把各模块串成闭环)scripts/core/diagnostic.py:Tier1 诊断(结构/引用/字数/禁用表述与命令)scripts/core/writing_coach.py:渐进式引导(阶段判断 + 可复制提示词 + AI fallback)scripts/core/security.py:白名单写入策略(拒绝写入 main.tex/.cls/.sty 等)scripts/core/editor.py+scripts/core/latex_parser.py:按\\subsubsection{...}精确替换正文scripts/core/versioning.py:runs 备份、diff、rollbackscripts/core/html_report.py+assets/templates/html/report_template.html:HTML 可视化报告scripts/core/example_matcher.py+assets/examples/**:示例推荐(支持*.metadata.yaml)scripts/core/config_loader.py:配置加载(基础配置 + preset + 用户 override)
数据流(简化)
1. CLI 读取配置:load_config() 2. Coordinator 定位目标文件:targets.justification_tex 3. 读取 tex → Tier1 诊断:run_tier1() 4. (可选)Tier2:AIIntegration.process_request() 5. 输出:
- 终端文本(diagnose/coach/review)
- HTML 报告(diagnose --html-report)
6. 写入(apply-section):
security.validate_write_target()白名单校验latex_parser.replace_subsubsection_body()精确替换正文versioning/ensure_run_dir()+ backup → 可 diff/rollback
可扩展点
- 学科预设:在
assets/presets/增加<preset>.yaml(兼容旧路径config/presets/:如你有旧文件可自行创建该目录),优先覆盖terminology.dimensions(推荐;三维矩阵),也兼容terminology.alias_groups - 示例库:在
assets/examples/<category>/增加.tex与*.metadata.yaml(keywords/description) - 质量规则:在
config.yaml的quality.forbidden_phrases/avoid_commands里增补规则
docs/(参考文档索引)
这些文档用于解释本 skill 的使用方式与实现边界(面向用户/维护者)。
路径提示:
- 在本仓库根目录运行脚本:
python skills/nsfc-justification-writer/scripts/run.py ... - 在本 skill 目录运行脚本:
python scripts/run.py ...
文档:
tutorial.md:从零到可用的快速教程(init/diagnose/coach/apply/ref/terms)architecture.md:模块拆分与关键设计点workflows/01_draft_to_pass.md:从草稿到通过的典型流程workflows/02_citations_and_doi.md:引用/DOI 核验与写入的闭环
教程:从 0 到可验收的“立项依据”
目标:在不破坏 NSFC 2026 模板结构的前提下,完成 extraTex/1.1.立项依据.tex 的“诊断→分步写作→安全写入→验收”闭环。
路径提示:
- 在本仓库根目录运行:
python skills/nsfc-justification-writer/scripts/run.py ... - 在本 skill 目录运行:
python scripts/run.py ...
0)准备
- 确认你的标书项目目录(示例:
projects/NSFC_Young) - 确认目标文件存在:
extraTex/1.1.立项依据.tex - 推荐先跑一次结构模板:
skills/nsfc-justification-writer/assets/templates/structure_template.tex
1)生成信息表(最小输入)
生成模板(手工填写):
python skills/nsfc-justification-writer/scripts/run.py init --out /tmp/info_form.md或交互式填写:
python skills/nsfc-justification-writer/scripts/run.py init --interactive --out /tmp/info_form_filled.md2)先诊断(把坑提前挖出来)
python skills/nsfc-justification-writer/scripts/run.py diagnose --project-root projects/NSFC_Young
python skills/nsfc-justification-writer/scripts/run.py diagnose --project-root projects/NSFC_Young --html-report auto建议:
- 结构不完整时,先补齐 4 个
\\subsubsection标题骨架,再进入正文写作 - 有引用但缺 bibkey 时,先修引用,再写正文
3)用 coach 拆解任务(每轮只改一个小标题)
python skills/nsfc-justification-writer/scripts/run.py coach \
--project-root projects/NSFC_Young \
--stage auto \
--topic "一句话主题/关键词"你会得到:
- “本轮只做三件事”(把工作量压到可执行)
- “需要你补充的问题”(补齐关键信息)
- “可直接复制的写作提示词”(给 AI 生成某个小标题正文用)
4)安全写入:只替换某个 \\subsubsection 的正文
把 AI 生成的正文保存为文件(例如 /tmp/new_body.txt),然后写入:
python skills/nsfc-justification-writer/scripts/run.py apply-section \
--project-root projects/NSFC_Young \
--title "国内外研究现状" \
--body-file /tmp/new_body.txt说明:
- 只允许写入白名单文件(默认仅
extraTex/1.1.立项依据.tex) - 默认严格:若正文里新增
\\cite{...}且.bib缺 key,会拒绝写入(防止“幻觉引用”)
5)引用与术语一致性(跨章节对齐)
引用核验并生成可复制提示词:
python skills/nsfc-justification-writer/scripts/run.py refs --project-root projects/NSFC_Young术语一致性矩阵:
python skills/nsfc-justification-writer/scripts/run.py terms --project-root projects/NSFC_Young6)回滚与差异查看(放心大胆迭代)
python skills/nsfc-justification-writer/scripts/run.py list-runs
python skills/nsfc-justification-writer/scripts/run.py diff --project-root projects/NSFC_Young --run-id <run_id>
python skills/nsfc-justification-writer/scripts/run.py rollback --project-root projects/NSFC_Young --run-id <run_id> --yes7)最后验收
diagnose通过(结构/引用/字数/表述)terms结果可接受(跨章节关键术语口径一致)- 需要引用的外部工作全部可核验(DOI/链接/题录信息明确)
如需编译 PDF(含参考文献),按本仓库建议执行 4 步:
xelatex → bibtex → xelatex → xelatex工作流:已有草稿 → 诊断通过
适用:你已经有 extraTex/1.1.立项依据.tex 的初稿,但结构/引用/字数/表述存在问题,希望快速“诊断→迭代→可回滚→验收”。
路径提示:
- 在本仓库根目录运行:
python skills/nsfc-justification-writer/scripts/run.py ... - 在本 skill 目录运行:
python scripts/run.py ...
0)先校验配置(可选)
python skills/nsfc-justification-writer/scripts/run.py validate-config1)先跑一次 Tier1 诊断(把硬性问题先暴露)
python skills/nsfc-justification-writer/scripts/run.py diagnose --project-root <你的项目>若结构缺失:先用 assets/templates/structure_template.tex 补齐 4 个 \subsubsection{...} 骨架,再进入正文优化。
2)必要时补齐引用(防止“幻觉引用”)
python skills/nsfc-justification-writer/scripts/run.py refs --project-root <你的项目>按输出里的“核验清单/可复制提示词”,补齐并核验 references/*.bib(优先使用 DOI/链接或可核验题录信息),再回来继续写作。
3)用 coach 把任务拆小(每轮只改一个小标题)
python skills/nsfc-justification-writer/scripts/run.py coach --project-root <你的项目> --stage auto --topic "一句话主题"把本轮生成的正文保存为文件(例如 /tmp/new_body.txt),然后安全写入:
python skills/nsfc-justification-writer/scripts/run.py apply-section \
--project-root <你的项目> \
--title "国内外研究现状" \
--body-file /tmp/new_body.txt4)跨章节术语口径检查(对齐 2.1/3.1)
python skills/nsfc-justification-writer/scripts/run.py terms --project-root <你的项目>5)最终验收
python skills/nsfc-justification-writer/scripts/run.py diagnose --project-root <你的项目> --html-report auto
python skills/nsfc-justification-writer/scripts/run.py review --project-root <你的项目>如需更严格的语义检查,可开启 Tier2(大文件可用分块与缓存控制):
python skills/nsfc-justification-writer/scripts/run.py diagnose --project-root <你的项目> --tier2 --chunk-size 12000 --max-chunks 20工作流:引用补齐与 DOI 可核验
适用:你希望保证“立项依据”中所有外部工作都可核验(bibkey 存在、doi 字段齐全、避免杜撰)。
路径提示:
- 在本仓库根目录运行:
python skills/nsfc-justification-writer/scripts/run.py ... - 在本 skill 目录运行:
python scripts/run.py ...
1)生成引用核验摘要(包含缺失 bibkey / 缺 DOI 的条目)
python skills/nsfc-justification-writer/scripts/run.py refs --project-root <你的项目>2)手动核验与补齐 BibTeX
按上一条命令输出里的“可直接复制提示词”,逐条补充 DOI/链接/题录信息并更新 references/*.bib(无法核验的信息请明确标注“待核验”,不要杜撰)。
3)写作时的引用守护
apply-section 默认严格:若正文里新增 \cite{...} 但 .bib 缺 key,会拒绝写入,避免把“幻觉引用”写进标书。
如你确实要临时跳过,可使用:
python skills/nsfc-justification-writer/scripts/run.py apply-section --allow-missing-citations ...不推荐长期使用:建议尽快补齐并回归严格模式。
Definition of Done((一)立项依据)
结构完整性
- 输出落点正确:只写
extraTex/1.1.立项依据.tex - 不破坏模板结构:不修改
main.tex、不修改extraTex/@config.tex - 逻辑闭环完整:价值/必要性 → 现状与不足 → 科学问题/假说 → 本项目切入点与贡献 → 小结过渡到研究内容
内容质量
- 术语口径一致:研究对象、缩写、关键指标与后续章节一致
- 可核验性:不写"国际领先/国内首次"等不可证明表述;引用必须可追溯(bibkey 存在、来源可核验、DOI/链接可追溯)
- 理论创新导向(默认):优先关注科学问题/假说的可证伪性、理论贡献的清晰性、验证维度的完备性(理论证明/定理/数值验证),详见
theoretical_innovation_guidelines.md
方法学术语使用规范(重要)
- 禁止用方法学术语撑段落主线:立项依据的核心驱动力是科学问题与可证伪假说,方法学术语仅作为验证手段的背景说明
- 方法术语的合理位置:
- ✅ 在"现状与不足"中简述主流方法路线(为指出理论局限性做铺垫)
- ✅ 在"验证方式"中提及验证手段(理论证明/数值验证/对照实验)
- ❌ 不作为段落主句的主语(如"本研究采用深度学习方法..."应改为"本研究假说:XXX机制...")
- 检查方法:每段主句的主语/核心驱动力是否为"科学问题/假说/理论空白"而非"方法/技术/平台"
---
理由:立项依据的评审焦点是"为什么要做"(科学问题)而非"怎么做"(方法术语)。方法学术语应在"研究内容/技术路线"章节展开,此处仅作为背景铺垫。
NSFC 写作信息表(模板)
用途:本技能在写(一)立项依据前,需要这份信息表来避免"默认某个具体课题"的隐含假设;并与extraTex/1.1.立项依据.tex的 4 个\subsubsection对齐。
>
说明:交互式生成的信息表会自动写入当前技能版本号(来源:config.yaml),本模板不固定版本号。默认采用理论创新导向:优先关注科学问题/假说的可证伪性、理论贡献的清晰性、验证维度的完备性(详见 theoretical_innovation_guidelines.md)
请按要点提供(可用 Markdown/YAML/自然语言)。标注为【必填】的字段缺失时,应先追问补全再写作:
1. 【必填】研究对象/应用场景:一句话边界(人群/疾病/材料/系统/任务)。 2. 【必填】痛点与现有不足(理论层面):
- 一句话问题定义(评审听得懂;避免写成“本项目要做什么”)。
- 2–4 条现有方案的关键瓶颈/约束(尽量做到“瓶颈 = 现状论述里已铺垫的障碍”,避免凭空新增)。
- (推荐)给出“瓶颈 → 科学问题约束”的一一映射:避免“瓶颈讲了 3 条,科学问题只覆盖 2 条”的逻辑缝隙。
3. 【必填】关键科学问题(疑问句,非研究目标):1–3 条,断点式表述。
- 科学问题追问“认知缺口”,常见句式:
X 是否具有 Y 性质?/在约束 A,B,C 下,X 是否仍成立?/什么条件下 X 成立? - 避免工程任务句式:
能否构建/开发/实现...(更像研究目标)
4. 【必填】核心科学假设(陈述句,预测性结果,不写验证方式):1 句可证伪表述。
- 推荐结构:
[核心方法/核心机制] 通过 [应对约束的策略] → 可 [预期结果](并相对于 [对照/基准] 展现 [增量价值]) - 避免:
在...验证中/通过...验证/在外部队列中...等“验证方式”表述
5. 【必填】本项目切入点(理论层面):差异化切口,1 段(如新表征/新方法学/统一框架/可证明定理),并用 1 句承上启下到 2.1 研究内容。 6. 【选填】拟解决技术/方法概览:输入→处理→验证→交付(只要骨架)。 7. 【选填】前期基础(可核验):论文/专利/数据/原型/预实验现象(给到可核验线索或可公开描述)。 8. 【选填】主流路线与代表工作:如需引用,请提供 DOI/链接(或可核验题录信息),先补齐 references/*.bib 后再写 \cite{...}。
可选补充(按需):
- 字数限制:例如目标 4000 字(中文字符)与允许偏差 ±200。
- 术语口径:研究对象如何称呼(患者/病例/受试者等)、核心指标名称、缩写首次出现是否要全称等。
扩展写作要点(可选阅读):
- 科学问题写作要点:
references/scientific_question_guidelines.md - 科学假设写作要点:
references/scientific_hypothesis_guidelines.md
方法学术语误用对比示例
用于提醒 AI:立项依据里方法术语只能做背景或验证手段,不能抢走“科学问题/理论缺口”的主线。
核心规则
- 错误主线:方法术语 → 性能指标 → 工程目标
- 正确主线:科学问题 / 理论缺口 → 假说 → 验证方式
四类高频误用
1. 研究背景
- 错:以“深度学习/CNN/Transformer 发展很快”开头
- 对:以“关键机制未阐明 / 统一表征缺失 / 认知缺口”开头
2. 现状不足
- 错:只说准确率低、过拟合、诊断效能差
- 对:指出假设过强、理论界不紧、框架不统一、因果链不清
3. 科学问题/假说
- 错:写成“开发一个系统/算法/平台”
- 对:写成“通过 XXX 可以证明/阐明/统一 YYY”
4. 项目切入点
- 错:交付物是软件、平台、工具
- 对:交付物是理论、定理、方法学、统一框架
快速自检
1. 段落主句主语是不是“科学问题/假说/理论空白”? 2. 删除方法术语后,这段是否还能成立? 3. 本段在回答“为什么要做”,还是“准备怎么做”?
科学假设写作要点(用于立项依据)
目标:用一句可证伪的“预测性陈述”回答科学问题,并为研究内容提供可追溯的“方法/策略 → 结果”逻辑。
科学假设的本质
科学假设 = 对“是什么/将会发生什么”的预测性陈述(陈述句),而不是“怎么验证”的方法描述。
- 推荐(结果预测):X 可以/能够实现 Y(并给出应对约束的策略)
- 避免(验证方式):在 Z 条件下验证/在外部队列验证/通过实验验证……
与科学问题的呼应关系
建议结构:
科学问题(疑问句):在约束 A、B、C 下,X 是否仍能实现/保有 Y?
科学假设(陈述句):X 通过策略 P、Q、R 应对 A、B、C,可实现/保有 Y(并相对于 baseline 展现增量价值)写作闸门(推荐):
- 问题中的关键约束,在假设中必须出现对应的应对策略;
- 假设中的关键策略,在研究内容中必须有落地的实施方案。
推荐的“一句话结构”
标准模板:
[核心方法/核心机制] 通过 [应对约束的关键策略] → 可 [预期结果],
并相对于 [对照/基准] 展现 [增量价值/稳定性/泛化性]。要素拆解:
- 核心方法/核心机制:本研究的关键创新点(点名即可,细节放前文/研究内容)
- 应对策略:对应约束的关键策略(并列列出即可)
- 预期结果:可检验的产出/性质(尽量具体)
- 对照/基准:至少给一个可比较对象(避免“好/强/有效”空泛词)
精简与克制
- 去掉前文已述的技术细节(假设里只点名核心方法/机制)
- 去掉可从上下文推断的修饰
- 避免绝对化与过度承诺:不用“首创/国际领先/完全解决/必然/最优”等
常见错误(强提醒)
1) 把验证方式写进假设:
- 反例:在独立外部验证中展现……
- 修正:展现……
2) 只有目标,没有策略与结果:
- 反例:拟构建 X 用于 Y
- 修正:X 通过(策略)可实现(结果)
3) 约束覆盖不全:
- 反例:瓶颈讲了三个,假设只回应两个
- 修正:假设显式覆盖全部关键约束
科学问题写作要点(用于立项依据)
目标:把“现状不足/关键瓶颈”收束成可检验的“认知缺口”,并为后续“科学假设与研究内容”提供可追溯的逻辑链条。
科学问题 ≠ 研究目标
常见误区:把“要做什么”写成“科学问题”。
- 科学问题:追问未知(是什么/为什么/在什么条件下成立)
- 研究目标:描述任务(构建/开发/实现/完成什么)
快速自检(启发式):
- 如果把句子主语替换为“本项目”,仍然通顺,那它更像研究目标而非科学问题。
好的科学问题的五个标准
1. 聚焦性:指向明确的认知缺口,不要泛泛而谈。 2. 科学性:追问性质/规律,而不是问“能不能做出来”。 3. 可检验性:能被本项目研究设计回答,且答案不是显而易见的。 4. 与瓶颈精确对应:问题中的约束应与前文“瓶颈/局限性”一一映射。 5. 开放性:尽量避免只有“是/否”的封闭问法,优先“什么条件下/通过什么机制/遵循什么规律”。
“瓶颈 → 约束 → 问题”的映射规则
强制要求(推荐作为写作闸门):
- 每个关键瓶颈,都必须在科学问题中体现为对应的约束/前提;
- 科学问题中不要凭空新增约束(每个约束都应能在前文找到论述支撑);
- 术语口径与全文一致(瓶颈怎么叫,约束就怎么叫)。
可用格式(推荐):
瓶颈1:...
约束1:...
瓶颈2:...
约束2:...
...
科学问题:在约束1、约束2、... 下,X 是否仍具有 Y 性质/信息/规律?推荐句式模板(可直接复用)
- 在约束 A、B、C 下,X 是否仍保有/满足 Y 性质?
- 什么条件下 X 的 Y 性质成立?(开放式)
- 在条件 A 发生变化时,X 的 Y 是否稳定/可迁移/可泛化?
避免句式(更像研究目标/工程任务):
- 能否构建/开发/实现/完成 XXX?
- 本项目拟建立/拟提出 XXX 以达到 XXX(这是目标陈述)
理论创新导向立项依据写作指南
本文件用于提醒 AI:写立项依据时,默认先讲 科学问题、理论缺口、可证伪假说,而不是先讲工程实现、平台建设或性能提升。
核心口径
- 立项依据最看重:科学问题是否明确、假说是否可证伪、理论贡献是否清楚
- 立项依据不应主打:系统搭建、成本优化、部署细节
四段闭环
1. 价值与必要性
- 推荐写法:理论框架解释力不足、现象无法统一解释、关键机制或边界条件不清
- 避免写法:流程繁琐、时延高、成本大、需要自动化
2. 现状与不足
- 推荐写法:
- 假设过强
- 框架不统一
- 因果机制不明
- 理论界不紧
- 避免只写“准确率不够高”“设备太贵”
3. 科学问题与假说
- 科学问题要写成可回答的研究问题
- 假说要写成可证伪的预测性表述
- 验证维度优先:
- 理论证明
- 反例排除
- 数值验证
- 必要的对照实验
4. 项目切入点与理论贡献
- 推荐强调:
- 新表征
- 新理论框架
- 新方法学
- 可外推的统一范式
- 避免把“软件系统 / 平台 / 原型”写成立项依据的主交付
方法术语误用警示
- 方法术语可以出现,但只适合作为:
- 现状背景
- 验证手段
- 不适合作为段落主线
快速自检:
1. 本段主句主语是“科学问题/假说/理论缺口”,还是“方法/技术/平台”? 2. 删除方法术语后,这段是否仍能成立? 3. 本段是否真的在回答“为什么要做”,而不是“准备怎么做”?
学科适配
- 数学/理论物理:强调定理、界、收敛性、反例
- 机制研究:强调因果链条、机制阐明、跨尺度表征
- 方法学研究:强调弱假设、统一框架、理论保证
写作检查清单
- [ ] 价值与必要性聚焦理论空白
- [ ] 现状不足聚焦理论局限
- [ ] 科学问题是可证伪的
- [ ] 项目切入点承诺的是理论贡献或方法学贡献
- [ ] 段落主线不是“方法/平台/实现”
理论创新导向优化计划摘要
历史计划文档,现仅保留“理论创新导向”落地后的稳定要点。
已落地的核心口径
- 立项依据默认优先理论创新,而非工程落地
- 四段闭环仍然成立,但每段都应以理论问题为中心
- 写作教练、诊断与评审建议都应优先问:
- 假设是否过强
- 框架是否统一
- 因果是否清楚
- 验证是否可证伪
当前最重要的配套文件
references/theoretical_innovation_guidelines.mdreferences/scientific_question_guidelines.mdreferences/scientific_hypothesis_guidelines.md
维护提醒
- 若未来切换默认风格,先更新
SKILL.md与config.yaml - 该文档不再维护版本计划、旧版任务清单或完成率评分
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
内部 Python 模块入口(非稳定对外 API)。
说明:
- scripts/run.py 会直接从各模块导入实现;本文件仅提供“方便交互/调试”的聚合导出。
- 若你需要稳定接口,请优先通过 scripts/run.py 的子命令调用。
"""
from .ai_integration import AIIntegration
from .config_loader import get_runs_dir, load_config, validate_config
from .diagnostic import DiagnosticReport
from .editor import ApplyResult
from .errors import SkillError
from .hybrid_coordinator import HybridCoordinator
from .review_integration import (
ReviewDirectoryInfo,
analyze_review_directory,
detect_slr_directory,
extract_citation_keys_from_bib,
extract_citations_from_tex,
format_review_directory_summary,
validate_citation_consistency,
validate_read_access,
)
__all__ = [
"AIIntegration",
"ApplyResult",
"DiagnosticReport",
"HybridCoordinator",
"ReviewDirectoryInfo",
"SkillError",
"analyze_review_directory",
"detect_slr_directory",
"extract_citation_keys_from_bib",
"extract_citations_from_tex",
"format_review_directory_summary",
"get_runs_dir",
"load_config",
"validate_citation_consistency",
"validate_config",
"validate_read_access",
]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
import hashlib
import json
import logging
from pathlib import Path
from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
JsonDict = Dict[str, Any]
Responder = Callable[[str, str, str], Union[str, JsonDict, None, Awaitable[Union[str, JsonDict, None]]]]
class AIIntegration:
"""
AI 集成层(优雅降级)
说明:
- 本仓库内的 Python 脚本默认不假设"可直接调用宿主 AI"。
- 若未提供 responder(或 enable_ai=False),将自动回退到 fallback。
- 该接口为后续真正的 AI 调用预留扩展点,同时保证当前功能可用。
"""
def __init__(
self,
*,
enable_ai: bool = True,
config: Optional[Dict[str, Any]] = None,
responder: Optional[Responder] = None,
) -> None:
self.enable_ai = bool(enable_ai)
self.config = config or {}
self.responder = responder
self.fallback_mode = False
self.request_count = 0
self.success_count = 0
def is_available(self) -> bool:
return bool(self.enable_ai and (not self.fallback_mode) and (self.responder is not None))
def get_stats(self) -> Dict[str, Any]:
return {
"enabled": self.enable_ai,
"fallback_mode": self.fallback_mode,
"request_count": self.request_count,
"success_count": self.success_count,
"success_rate": self.success_count / max(self.request_count, 1),
}
async def process_request(
self,
*,
task: str,
prompt: str,
fallback: Callable[[], Any],
output_format: str = "json",
cache_dir: Optional[Path] = None,
fresh: bool = False,
) -> Any:
self.request_count += 1
cache_path: Optional[Path] = None
if cache_dir is not None:
cache_dir = Path(cache_dir).resolve()
cache_key = hashlib.sha256(f"{task}\n{output_format}\n{prompt}".encode("utf-8", errors="ignore")).hexdigest()
safe_task = "".join([c if (c.isalnum() or c in {"-", "_"}) else "_" for c in str(task)])[:64] or "task"
suffix = ".json" if output_format == "json" else ".txt"
cache_path = (cache_dir / f"{safe_task}_{cache_key}{suffix}").resolve()
if (not fresh) and cache_path.exists():
try:
if output_format == "json":
obj = json.loads(cache_path.read_text(encoding="utf-8", errors="ignore"))
if isinstance(obj, dict):
self.success_count += 1
return obj
elif output_format == "text":
self.success_count += 1
return cache_path.read_text(encoding="utf-8", errors="ignore").strip()
except (OSError, UnicodeError, json.JSONDecodeError, ValueError):
logging.getLogger(__name__).debug("[AIIntegration] ignore broken cache: %s", str(cache_path))
if not self.enable_ai:
self.fallback_mode = True
self._log_fallback(task, reason="AI disabled")
return fallback()
if self.responder is None:
self.fallback_mode = True
self._log_fallback(task, reason="No responder configured")
return fallback()
try:
raw = self.responder(task, prompt, output_format)
if hasattr(raw, "__await__"):
raw = await raw # type: ignore[misc]
if raw is None:
raise ValueError("Empty AI response")
if output_format == "json":
if isinstance(raw, dict):
self.success_count += 1
if cache_path is not None:
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_text(json.dumps(raw, ensure_ascii=False, indent=2), encoding="utf-8")
return raw
parsed = self._parse_json_response(str(raw))
if parsed is None:
raise ValueError("Failed to parse JSON response")
self.success_count += 1
if cache_path is not None:
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_text(json.dumps(parsed, ensure_ascii=False, indent=2), encoding="utf-8")
return parsed
if output_format == "text":
text = str(raw).strip()
self.success_count += 1
if cache_path is not None:
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_text(text, encoding="utf-8")
return text
raise ValueError(f"Unsupported output_format: {output_format}")
except Exception as e:
self.fallback_mode = True
logging.getLogger(__name__).warning(
"[AIIntegration] fallback task=%s reason=%s",
task,
str(e),
exc_info=True,
)
return fallback()
@staticmethod
def _parse_json_response(response_text: str) -> Optional[JsonDict]:
# 1) fenced code block
if "```json" in response_text:
start = response_text.find("```json") + 7
end = response_text.find("```", start)
if end != -1:
candidate = response_text[start:end].strip()
try:
obj = json.loads(candidate)
return obj if isinstance(obj, dict) else None
except json.JSONDecodeError:
return None
# 2) first balanced {...}
start = response_text.find("{")
if start == -1:
return None
depth = 0
for i in range(start, len(response_text)):
ch = response_text[i]
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
candidate = response_text[start : i + 1]
try:
obj = json.loads(candidate)
return obj if isinstance(obj, dict) else None
except json.JSONDecodeError:
return None
return None
@staticmethod
def _log_fallback(task: str, reason: str) -> None:
logger = logging.getLogger(__name__)
if reason in {"AI disabled", "No responder configured"}:
logger.info("[AIIntegration] fallback task=%s reason=%s", task, reason)
else:
logger.warning("[AIIntegration] fallback task=%s reason=%s", task, reason)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True)
class BibFixSuggestion:
missing_bibkeys: List[str]
missing_doi_keys: List[str]
invalid_doi_keys: List[str]
def to_markdown(self, *, project_root: str) -> str:
lines = [
"# 引用核验建议(手动补齐 BibTeX)",
"",
"说明:本工具不会自动联网补齐引用,但会生成一段“可直接复制”的提示词,帮助你(或任意 BibTeX 工具/助手)完成核验与补齐。",
"",
]
if self.missing_bibkeys:
lines += [
"## 缺失的 bibkey(LaTeX 中引用了,但 .bib 没有)",
"",
] + [f"- {k}" for k in self.missing_bibkeys]
if self.missing_doi_keys:
lines += [
"",
"## DOI 缺失的条目(.bib 有该 key,但缺 doi 字段,建议补齐以便可核验)",
"",
] + [f"- {k}" for k in self.missing_doi_keys]
if self.invalid_doi_keys:
lines += [
"",
"## DOI 疑似不合法的条目(.bib 有 doi 字段,但格式看起来不对,建议核验)",
"",
] + [f"- {k}" for k in self.invalid_doi_keys]
lines += [
"",
"## 可直接复制的提示词(用于核验与补齐)",
"```",
"请帮我核验并补齐参考文献条目:",
f"目标项目:{project_root}",
"任务:核验并补齐参考文献条目,确保不出现幻觉引用。",
]
if self.missing_bibkeys:
lines += [
"需要新增/补齐的 bibkey:",
", ".join(self.missing_bibkeys),
"说明:这些 key 在 tex 里被 \\cite{...} 使用,但当前 .bib 未找到。请让我提供 DOI/链接/题录信息后再写入,或提示我补充缺失信息。",
]
if self.missing_doi_keys:
lines += [
"需要补 DOI 的 bibkey:",
", ".join(self.missing_doi_keys),
"说明:这些 key 在 .bib 存在,但缺 doi 字段;请在不杜撰的前提下补齐 doi(如无法确定,请明确提示需要我提供 DOI/链接)。",
]
if self.invalid_doi_keys:
lines += [
"需要核验/修正 DOI 的 bibkey:",
", ".join(self.invalid_doi_keys),
"说明:这些 key 的 doi 字段疑似不合规(例如写成 URL/带多余字符/缺 10.x 前缀等);请核验后修正为标准 DOI 格式(如无法确定,请明确提示需要我提供 DOI/链接)。",
]
lines += [
"输出:更新项目 references/*.bib(或你认为合适的 .bib),并给出每条的题目/作者/年份/期刊/DOI 核验结果;无法核验的条目请标注“待核验”。",
"```",
]
return "\n".join(lines).strip() + "\n"
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import Any, Dict, Optional
from .ai_integration import AIIntegration
from .latex_parser import strip_comments
class BoastfulExpressionAI:
"""
AI 主导的“可能引起评审不适的表述”识别。
说明:
- 不依赖禁词表,而是基于语义判断是否存在无对照/无证据的自我夸大
- AI 不可用时返回空 issues(不阻断),由硬编码高风险示例做快速提示
"""
def __init__(self, ai: AIIntegration) -> None:
self.ai = ai
async def check(
self,
*,
tex_text: str,
max_chars: int,
cache_dir: Optional[Any] = None,
fresh: bool = False,
) -> Dict[str, Any]:
prompt = """
请分析以下立项依据文本,识别“可能引起评审专家不适的表述”。
评审专家通常反感的表述类型:
1) 绝对化表述:无对照的“最/首/唯一/领先/首创”
2) 填补空白式:“填补空白”“开创性”无具体指标
3) 无依据夸大:“重大/重要/关键”无数据/对比支撑
4) 自我定性:用绝对形容词自我评价(无第三方引用)
请注意区分:
- 有数据/引用支撑的结论是 OK 的(如“相比 X 方法提升 30%”“引用自 Y 期刊”)
- 无依据的自我夸大需要标记
要求:
- 只输出 JSON,不要解释
- 原文片段请截取不超过 50 字
- 不要杜撰文献引用与 DOI
返回 JSON:
{
"issues": [
{
"category": "绝对化表述|填补空白|无依据夸大|自我定性",
"text": "原文片段(不超过 50 字)",
"reason": "为何会引起评审不适",
"suggestion": "如何改为可验证/可对照的表述"
}
],
"summary": {
"total_issues": 3,
"by_category": {"绝对化表述": 2, "无依据夸大": 1}
}
}
""".strip()
max_chars = max(int(max_chars), 1000)
cleaned = strip_comments(tex_text or "")[:max_chars]
prompt = prompt + f"\n\n文本内容(去注释后,最多 {max_chars} 字符):\n" + cleaned
def _fallback() -> Dict[str, Any]:
return {"issues": [], "summary": {"total_issues": 0, "by_category": {}}, "_mode": "fallback"}
obj = await self.ai.process_request(
task="boastful_expression_check",
prompt=prompt,
fallback=_fallback,
output_format="json",
cache_dir=cache_dir,
fresh=fresh,
)
if not isinstance(obj, dict):
return _fallback()
obj.setdefault("_mode", "ai" if self.ai.is_available() else "fallback")
return obj
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import Any, Mapping, MutableMapping, Sequence
def get_mapping(cfg: Mapping[str, Any], key: str) -> Mapping[str, Any]:
v = cfg.get(key)
return v if isinstance(v, Mapping) else {}
def get_mutable_mapping(cfg: Mapping[str, Any], key: str) -> MutableMapping[str, Any]:
v = cfg.get(key)
return v if isinstance(v, MutableMapping) else {}
def get_nested_mapping(cfg: Mapping[str, Any], *keys: str) -> Mapping[str, Any]:
cur: Mapping[str, Any] = cfg
for k in keys:
cur = get_mapping(cur, k)
return cur
def get_str(cfg: Mapping[str, Any], key: str, default: str = "") -> str:
v = cfg.get(key, default)
return str(v) if v is not None else str(default)
def get_bool(cfg: Mapping[str, Any], key: str, default: bool = False) -> bool:
v = cfg.get(key, default)
return bool(v)
def get_int(cfg: Mapping[str, Any], key: str, default: int) -> int:
v = cfg.get(key, default)
try:
return int(v)
except (TypeError, ValueError):
return int(default)
def get_seq_str(cfg: Mapping[str, Any], key: str) -> Sequence[str]:
v = cfg.get(key)
if isinstance(v, list):
return [str(x) for x in v if str(x).strip()]
return []
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
import os
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence
# ═══════════════════════════════════════════════════════════════════════
# 配置单一真相来源(Single Source of Truth)说明:
# config.yaml 是权威配置文件,DEFAULT_CONFIG 仅作为"兜底值"
# 修改配置时,请优先编辑 config.yaml,而非此文件
# ═══════════════════════════════════════════════════════════════════════
DEFAULT_CONFIG: Dict[str, Any] = {
# 安全关键项:即使 YAML 缺失也必须生效的默认值(避免"空策略"导致任意写入)
"guardrails": {
"allowed_write_files": ["extraTex/1.1.立项依据.tex"],
"forbidden_write_files": ["main.tex", "extraTex/@config.tex"],
"forbidden_write_globs": ["**/*.cls", "**/*.sty"],
},
}
def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
merged: Dict[str, Any] = dict(base)
for key, value in (override or {}).items():
if isinstance(value, dict) and isinstance(merged.get(key), dict):
merged[key] = _deep_merge(merged[key], value)
else:
merged[key] = value
return merged
def _load_yaml_dict_with_warning(path: Path) -> tuple[Dict[str, Any], str]:
try:
import yaml # type: ignore
except (ModuleNotFoundError, ImportError):
return {}, "未安装 PyYAML,已跳过 YAML 配置加载(建议 `pip install pyyaml`)"
try:
raw = yaml.safe_load(path.read_text(encoding="utf-8", errors="ignore")) or {}
if not isinstance(raw, dict):
return {}, "YAML 顶层不是 mapping(dict),已忽略"
return raw, ""
except (OSError, UnicodeError, ValueError, yaml.YAMLError) as e: # type: ignore[attr-defined]
return {}, f"YAML 解析失败({type(e).__name__}: {e}),已忽略(请检查语法)"
def _default_user_override_path() -> Optional[Path]:
home = Path.home()
candidates = [
home / ".config" / "nsfc-justification-writer" / "override.yaml",
home / ".config" / "nsfc-justification-writer" / "override.yml",
]
return next((p for p in candidates if p.exists() and p.is_file()), None)
def _is_seq_str(x: Any) -> bool:
return isinstance(x, list) and all(isinstance(it, str) for it in x)
def _is_alias_groups(x: Any) -> bool:
if not isinstance(x, dict):
return False
for k, v in x.items():
if not isinstance(k, str):
return False
if not _is_seq_str(v):
return False
return True
def _is_nonempty_seq_str(x: Any) -> bool:
return isinstance(x, list) and all(isinstance(it, str) and str(it).strip() for it in x) and len(x) > 0
def _looks_like_path(s: str) -> bool:
t = (s or "").strip()
if not t:
return False
if "\n" in t or "\r" in t:
return False
if t.endswith((".txt", ".md", ".yaml", ".yml")):
return True
return ("/" in t) or ("\\" in t)
def _resolve_prompt_path(skill_root: Path, value: str) -> Optional[Path]:
v = str(value or "").strip()
if not v or (not _looks_like_path(v)):
return None
p = Path(v).expanduser()
if not p.is_absolute():
p = (Path(skill_root).resolve() / p).resolve()
return p.resolve()
def _collect_config_warnings(*, skill_root: Path, config: Dict[str, Any]) -> List[str]:
warnings: List[str] = []
prompts = config.get("prompts", {})
if isinstance(prompts, dict):
for k, v in prompts.items():
if not isinstance(k, str):
continue
if not isinstance(v, str) or (not v.strip()):
continue
p = _resolve_prompt_path(skill_root, v)
if p is None:
continue
# 仅做风险提示:外部路径内容可能被拼入 prompt(若上层注入 responder)
if Path(v).expanduser().is_absolute():
warnings.append(f"prompts.{k} 使用绝对路径:{v}(注意:该文件内容可能被拼入 prompt)")
try:
p.relative_to(Path(skill_root).resolve())
except ValueError:
warnings.append(f"prompts.{k} 指向 skill_root 之外:{v} -> {p}(注意:该文件内容可能被拼入 prompt)")
return warnings
def _harden_guardrails(*, config: Dict[str, Any], meta: Dict[str, Any]) -> None:
"""
安全关键项加固:
- guardrails 必须存在且为 dict
- allowed_write_files 不允许为空;若无效则回退到 DEFAULT_CONFIG(并给出 warning)
- forbidden_write_files/forbidden_write_globs 若无效也回退到 DEFAULT_CONFIG(避免误放开 .cls/.sty 等)
"""
default_guard = DEFAULT_CONFIG.get("guardrails", {})
if not isinstance(default_guard, dict):
default_guard = {}
guard = config.get("guardrails")
if not isinstance(guard, dict):
config["guardrails"] = dict(default_guard)
meta.setdefault("warnings", []).append("guardrails 非 dict(或被置空),已回退到安全默认值(白名单写入保持启用)")
return
allowed = guard.get("allowed_write_files")
if not _is_nonempty_seq_str(allowed):
guard["allowed_write_files"] = list(default_guard.get("allowed_write_files", ["extraTex/1.1.立项依据.tex"]))
meta.setdefault("warnings", []).append(
"guardrails.allowed_write_files 为空/无效,已回退到安全默认值(避免白名单失效)"
)
forbidden_files = guard.get("forbidden_write_files")
if not _is_seq_str(forbidden_files):
guard["forbidden_write_files"] = list(default_guard.get("forbidden_write_files", []))
meta.setdefault("warnings", []).append("guardrails.forbidden_write_files 无效,已回退到安全默认值")
forbidden_globs = guard.get("forbidden_write_globs")
if not _is_seq_str(forbidden_globs):
guard["forbidden_write_globs"] = list(default_guard.get("forbidden_write_globs", []))
meta.setdefault("warnings", []).append("guardrails.forbidden_write_globs 无效,已回退到安全默认值")
def validate_config(*, skill_root: Path, config: Dict[str, Any]) -> List[str]:
"""
轻量配置校验:只校验关键字段与类型,不阻止用户加入额外键。
返回错误列表(空列表表示通过)。
"""
errors: List[str] = []
def err(msg: str) -> None:
errors.append(msg)
skill_info = config.get("skill_info")
if not isinstance(skill_info, dict):
err("skill_info 必须是 dict")
else:
if not isinstance(skill_info.get("name"), str) or not str(skill_info.get("name")).strip():
err("skill_info.name 必须是非空字符串")
if not isinstance(skill_info.get("version"), str) or not str(skill_info.get("version")).strip():
err("skill_info.version 必须是非空字符串")
style = config.get("style", {})
if style is not None and not isinstance(style, dict):
err("style 必须是 dict")
elif isinstance(style, dict) and style:
mode = str(style.get("mode", "theoretical")).strip().lower()
if mode not in {"theoretical", "mixed", "engineering"}:
err("style.mode 必须是 theoretical|mixed|engineering")
targets = config.get("targets")
if not isinstance(targets, dict):
err("targets 必须是 dict")
else:
if not isinstance(targets.get("justification_tex"), str) or not str(targets.get("justification_tex")).strip():
err("targets.justification_tex 必须是非空字符串")
bib_globs = targets.get("bib_globs", [])
if not _is_seq_str(bib_globs):
err("targets.bib_globs 必须是字符串列表")
structure = config.get("structure")
if not isinstance(structure, dict):
err("structure 必须是 dict")
else:
expected = structure.get("expected_subsubsections", None)
recommended = structure.get("recommended_subsubsections", None)
if (expected is None) and (recommended is None):
err("structure.expected_subsubsections 或 structure.recommended_subsubsections 必须至少存在一个")
else:
seq = recommended if recommended is not None else expected
if not _is_seq_str(seq) or not seq:
err("structure.(expected_subsubsections|recommended_subsubsections) 必须是非空字符串列表")
m = structure.get("min_subsubsection_count")
if not isinstance(m, int) or m <= 0:
err("structure.min_subsubsection_count 必须是正整数")
quality = config.get("quality")
if not isinstance(quality, dict):
err("quality 必须是 dict")
else:
if "forbidden_phrases" in quality and not _is_seq_str(quality.get("forbidden_phrases", [])):
err("quality.forbidden_phrases 必须是字符串列表")
if "high_risk_examples" in quality and not _is_seq_str(quality.get("high_risk_examples", [])):
err("quality.high_risk_examples 必须是字符串列表")
if not _is_seq_str(quality.get("avoid_commands", [])):
err("quality.avoid_commands 必须是字符串列表")
if "strict_mode" in quality and not isinstance(quality.get("strict_mode"), bool):
err("quality.strict_mode 必须是 bool")
if "enable_ai_judgment" in quality and not isinstance(quality.get("enable_ai_judgment"), bool):
err("quality.enable_ai_judgment 必须是 bool")
if "ai_judgment_mode" in quality and not isinstance(quality.get("ai_judgment_mode"), str):
err("quality.ai_judgment_mode 必须是 str")
wc = config.get("word_count", {})
if not isinstance(wc, dict):
err("word_count 必须是 dict")
else:
if not isinstance(wc.get("target", 4000), int):
err("word_count.target 必须是整数")
if not isinstance(wc.get("tolerance", 200), int):
err("word_count.tolerance 必须是整数")
ai = config.get("ai", {})
if not isinstance(ai, dict):
err("ai 必须是 dict")
else:
if not isinstance(ai.get("enabled", True), bool):
err("ai.enabled 必须是 bool")
if "tier2_chunk_size" in ai and not isinstance(ai.get("tier2_chunk_size"), int):
err("ai.tier2_chunk_size 必须是 int")
if "tier2_max_chunks" in ai and not isinstance(ai.get("tier2_max_chunks"), int):
err("ai.tier2_max_chunks 必须是 int")
if "cache_dir" in ai and not isinstance(ai.get("cache_dir"), str):
err("ai.cache_dir 必须是 str")
limits = config.get("limits", {})
if limits is not None and not isinstance(limits, dict):
err("limits 必须是 dict")
elif isinstance(limits, dict):
if "max_file_size_mb" in limits and not isinstance(limits.get("max_file_size_mb"), int):
err("limits.max_file_size_mb 必须是 int")
if "ai_max_input_chars" in limits and not isinstance(limits.get("ai_max_input_chars"), int):
err("limits.ai_max_input_chars 必须是 int")
if "writing_coach_preview_chars" in limits and not isinstance(limits.get("writing_coach_preview_chars"), int):
err("limits.writing_coach_preview_chars 必须是 int")
if "word_target" in limits:
wt = limits.get("word_target")
if wt is not None and not isinstance(wt, dict):
err("limits.word_target 必须是 dict")
elif isinstance(wt, dict):
if "min" in wt and not isinstance(wt.get("min"), int):
err("limits.word_target.min 必须是 int")
if "max" in wt and not isinstance(wt.get("max"), int):
err("limits.word_target.max 必须是 int")
prompts = config.get("prompts", {})
if prompts is not None and not isinstance(prompts, dict):
err("prompts 必须是 dict")
elif isinstance(prompts, dict):
for k, v in prompts.items():
if not isinstance(v, str) or not v.strip():
err(f"prompts.{k} 必须是非空字符串")
continue
# 允许:1) 文件路径;2) 直接写多行 prompt(用于 override/preset)
if _looks_like_path(v):
p = Path(v)
if not p.is_absolute():
maybe = (Path(skill_root).resolve() / p).resolve()
if not maybe.exists():
err(f"prompts.{k} 路径不存在:{v}")
guardrails = config.get("guardrails")
if not isinstance(guardrails, dict):
err("guardrails 必须是 dict(安全关键项)")
else:
if not _is_nonempty_seq_str(guardrails.get("allowed_write_files")):
err("guardrails.allowed_write_files 必须是非空字符串列表(安全关键项:写入白名单不可为空)")
if not _is_seq_str(guardrails.get("forbidden_write_files", [])):
err("guardrails.forbidden_write_files 必须是字符串列表")
if not _is_seq_str(guardrails.get("forbidden_write_globs", [])):
err("guardrails.forbidden_write_globs 必须是字符串列表")
terminology = config.get("terminology", {})
if terminology is not None and not isinstance(terminology, dict):
err("terminology 必须是 dict")
elif isinstance(terminology, dict):
mode = str(terminology.get("mode", "auto")).strip().lower()
if mode not in {"auto", "ai", "legacy", "semantic_only", "legacy_only"}:
err("terminology.mode 必须是 auto|ai|legacy|semantic_only|legacy_only")
if "enable_ai_semantic_check" in terminology and not isinstance(terminology.get("enable_ai_semantic_check"), bool):
err("terminology.enable_ai_semantic_check 必须是 bool")
if "ai_mode" in terminology and not isinstance(terminology.get("ai_mode"), str):
err("terminology.ai_mode 必须是 str")
ai_cfg = terminology.get("ai", {})
if ai_cfg is not None and not isinstance(ai_cfg, dict):
err("terminology.ai 必须是 dict")
elif isinstance(ai_cfg, dict):
if "enabled" in ai_cfg and not isinstance(ai_cfg.get("enabled"), bool):
err("terminology.ai.enabled 必须是 bool")
if "max_chars" in ai_cfg and not isinstance(ai_cfg.get("max_chars"), int):
err("terminology.ai.max_chars 必须是 int")
if "dimensions" in terminology:
dims = terminology.get("dimensions")
if not isinstance(dims, dict):
err("terminology.dimensions 必须是 dict")
elif dims:
for dim_name, groups in dims.items():
if not isinstance(dim_name, str) or not dim_name.strip():
err("terminology.dimensions 的 key 必须是非空字符串")
continue
if not _is_alias_groups(groups):
err(f"terminology.dimensions.{dim_name} 必须是 dict[str, list[str]]")
elif "alias_groups" in terminology:
if not _is_alias_groups(terminology.get("alias_groups")):
err("terminology.alias_groups 必须是 dict[str, list[str]]")
writing_coach = config.get("writing_coach")
if writing_coach is not None and not isinstance(writing_coach, dict):
err("writing_coach 必须是 dict")
elif isinstance(writing_coach, dict):
if "enable_ai_stage_inference" in writing_coach and not isinstance(writing_coach.get("enable_ai_stage_inference"), bool):
err("writing_coach.enable_ai_stage_inference 必须是 bool")
if "ai_inference_mode" in writing_coach and not isinstance(writing_coach.get("ai_inference_mode"), str):
err("writing_coach.ai_inference_mode 必须是 str")
if "fallback_rules" in writing_coach and not isinstance(writing_coach.get("fallback_rules"), dict):
err("writing_coach.fallback_rules 必须是 dict")
# 第三方约束:仅做类型校验(不要求必须存在)
constraints = config.get("constraints")
if constraints is not None and not isinstance(constraints, dict):
err("constraints 必须是 dict")
elif isinstance(constraints, dict):
page = constraints.get("page_limit")
if page is not None and not isinstance(page, dict):
err("constraints.page_limit 必须是 dict")
elif isinstance(page, dict):
for k in ["min", "max", "warning_threshold", "chars_per_page"]:
if k in page and not isinstance(page.get(k), int):
err(f"constraints.page_limit.{k} 必须是 int")
if "recommended" in page:
rec = page.get("recommended")
if not (isinstance(rec, list) and len(rec) >= 2 and all(isinstance(x, int) for x in rec[:2])):
err("constraints.page_limit.recommended 必须是 [int, int]")
wc = constraints.get("word_count")
if wc is not None and not isinstance(wc, dict):
err("constraints.word_count 必须是 dict")
elif isinstance(wc, dict):
for k in ["min", "max"]:
if k in wc and not isinstance(wc.get(k), int):
err(f"constraints.word_count.{k} 必须是 int")
refs = constraints.get("references")
if refs is not None and not isinstance(refs, dict):
err("constraints.references 必须是 dict")
elif isinstance(refs, dict):
for k in ["min", "max"]:
if k in refs and not isinstance(refs.get(k), int):
err(f"constraints.references.{k} 必须是 int")
opening = constraints.get("opening")
if opening is not None and not isinstance(opening, dict):
err("constraints.opening 必须是 dict")
elif isinstance(opening, dict):
if "cjk_chars" in opening and not isinstance(opening.get("cjk_chars"), int):
err("constraints.opening.cjk_chars 必须是 int")
return errors
def load_config(
skill_root: Path,
*,
preset: Optional[str] = None,
override_path: Optional[str] = None,
load_user_override: bool = True,
) -> Dict[str, Any]:
skill_root = Path(skill_root).resolve()
config: Dict[str, Any] = dict(DEFAULT_CONFIG)
meta: Dict[str, Any] = {"yaml_available": True, "loaded_files": [], "warnings": []}
def _merge_yaml(path: Path, *, label: str) -> None:
nonlocal config, meta
if not path.exists():
return
data, warn = _load_yaml_dict_with_warning(path)
if warn:
meta["warnings"].append(f"{label}: {warn} -> {path}")
if "未安装 PyYAML" in warn:
meta["yaml_available"] = False
return
config = _deep_merge(config, data)
meta["loaded_files"].append(str(path))
config_path = (skill_root / "config.yaml").resolve()
_merge_yaml(config_path, label="repo config.yaml")
if preset:
# 新规范:assets/presets/<name>.yaml(优先)
# 兼容旧路径:config/presets/<name>.yaml
preset_name = str(preset).strip()
preset_candidates = [
(skill_root / "assets" / "presets" / f"{preset_name}.yaml").resolve(),
(skill_root / "config" / "presets" / f"{preset_name}.yaml").resolve(),
]
preset_path = next((p for p in preset_candidates if p.exists()), preset_candidates[0])
_merge_yaml(preset_path, label=f"preset={preset_name}")
config["active_preset"] = str(preset)
else:
config["active_preset"] = ""
disable_user_override = str(os.environ.get("NSFC_JUSTIFICATION_WRITER_DISABLE_USER_OVERRIDE", "")).strip().lower() in {
"1",
"true",
"yes",
}
if load_user_override and (not disable_user_override):
env_override = os.environ.get("NSFC_JUSTIFICATION_WRITER_OVERRIDE_PATH")
user_path = Path(env_override).expanduser().resolve() if env_override else _default_user_override_path()
if user_path and user_path.exists():
_merge_yaml(Path(user_path), label="user override")
if override_path:
p = Path(override_path).expanduser()
if not p.is_absolute():
p = (Path.cwd() / p).resolve()
_merge_yaml(p, label="--override")
meta["warnings"].extend(_collect_config_warnings(skill_root=skill_root, config=config))
config["_config_loader"] = meta
disable_validation = str(os.environ.get("NSFC_JUSTIFICATION_WRITER_DISABLE_CONFIG_VALIDATION", "")).strip().lower() in {
"1",
"true",
"yes",
}
# 无 PyYAML:只能运行 DEFAULT_CONFIG(至少保证 guardrails 生效),跳过强校验避免“承诺降级但实际失败”
if not bool(meta.get("yaml_available", True)):
disable_validation = True
meta["warnings"].append("未安装 PyYAML:已跳过配置强校验(当前仅保证 guardrails 等安全兜底生效)")
if not disable_validation:
errs = validate_config(skill_root=skill_root, config=config)
if errs:
raise ValueError("配置校验失败:\n- " + "\n- ".join(errs))
# 即使用户显式关闭校验,也不允许安全关键项被“置空/移除”
_harden_guardrails(config=config, meta=meta)
return config
def get_runs_dir(skill_root: Path, config: Dict[str, Any]) -> Path:
env_override = os.environ.get("NSFC_JUSTIFICATION_WRITER_RUNS_DIR")
if env_override:
p = Path(env_override)
if not p.is_absolute():
p = (Path(skill_root) / p).resolve()
return p.resolve()
workspace = config.get("workspace", {})
workspace = workspace if isinstance(workspace, dict) else {}
runs_dir = workspace.get("runs_dir", "tests/_artifacts/runs")
return (Path(skill_root) / str(runs_dir)).resolve()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Any, Dict, List, Mapping, Sequence, Tuple
from .config_access import get_int, get_mapping
from .latex_parser import strip_comments
from .reference_validator import parse_cite_keys
from .wordcount import count_cjk_chars
@dataclass(frozen=True)
class PageLimit:
min_pages: int
max_pages: int
recommended: Tuple[int, int]
warning_threshold: int
chars_per_page: int
@dataclass(frozen=True)
class RangeLimit:
min_n: int
max_n: int
def _constraints_cfg(cfg: Mapping[str, Any]) -> Mapping[str, Any]:
return get_mapping(cfg, "constraints")
def load_page_limit(cfg: Mapping[str, Any]) -> PageLimit:
page_cfg = get_mapping(_constraints_cfg(cfg), "page_limit")
lo = max(0, get_int(page_cfg, "min", 6))
hi = max(lo, get_int(page_cfg, "max", 10))
rec_raw = page_cfg.get("recommended", [6, 8])
if isinstance(rec_raw, Sequence) and len(rec_raw) >= 2:
try:
rec_a = int(rec_raw[0])
rec_b = int(rec_raw[1])
except (TypeError, ValueError):
rec_a, rec_b = 6, 8
else:
rec_a, rec_b = 6, 8
rec_lo, rec_hi = (rec_a, rec_b) if rec_a <= rec_b else (rec_b, rec_a)
rec_lo = max(lo, rec_lo)
rec_hi = max(rec_lo, min(hi, rec_hi))
warn = get_int(page_cfg, "warning_threshold", 9)
warn = max(lo, min(hi, warn))
cpp = max(100, get_int(page_cfg, "chars_per_page", 1000))
return PageLimit(
min_pages=lo,
max_pages=hi,
recommended=(rec_lo, rec_hi),
warning_threshold=warn,
chars_per_page=cpp,
)
def load_word_count_limit(cfg: Mapping[str, Any]) -> RangeLimit:
wc_cfg = get_mapping(_constraints_cfg(cfg), "word_count")
lo = max(0, get_int(wc_cfg, "min", 8000))
hi = max(lo, get_int(wc_cfg, "max", 10000))
return RangeLimit(min_n=lo, max_n=hi)
def load_reference_limit(cfg: Mapping[str, Any]) -> RangeLimit:
ref_cfg = get_mapping(_constraints_cfg(cfg), "references")
lo = max(0, get_int(ref_cfg, "min", 30))
hi = max(lo, get_int(ref_cfg, "max", 50))
return RangeLimit(min_n=lo, max_n=hi)
def load_opening_limit(cfg: Mapping[str, Any]) -> int:
opening_cfg = get_mapping(_constraints_cfg(cfg), "opening")
return max(50, get_int(opening_cfg, "cjk_chars", 300))
def estimate_pages(tex_text: str, *, chars_per_page: int) -> Tuple[int, float]:
"""
仅用于“预警级”估算:用 cjk_strip_commands 的正文字符数 / chars_per_page 估算页数。
"""
n = count_cjk_chars(tex_text or "", mode="cjk_strip_commands").cjk_count
if chars_per_page <= 0:
return n, 0.0
pages = float(n) / float(chars_per_page)
return n, max(0.0, pages)
def classify_range(n: int, *, lo: int, hi: int) -> str:
if n < lo:
return "too_few"
if n > hi:
return "too_many"
return "within"
def classify_pages(pages: float, *, rule: PageLimit) -> str:
rec_lo, rec_hi = rule.recommended
if pages < float(rule.min_pages):
return "too_short"
if pages <= float(rec_hi):
return "within_recommended"
if pages <= float(rule.warning_threshold):
return "within_limit"
if pages <= float(rule.max_pages):
return "near_limit"
return "exceed"
_CMD_WITH_ARG_RE = re.compile(r"\\[a-zA-Z@]+\\*?\s*(?:\[[^\]]*\]\s*)*\{([^{}]*)\}")
_CMD_BARE_RE = re.compile(r"\\[a-zA-Z@]+\\*?")
_WS_RE = re.compile(r"\s+")
def _latex_to_plain_text(tex_text: str) -> str:
"""
近似 LaTeX -> 纯文本:
- 去注释
- 将 \\cmd{...} 替换为 {...} 内部文本(仅处理“无嵌套花括号”的常见情形)
- 删除剩余 \\cmd
目的:给启发式规则做关键词命中(非严格解析)。
"""
t = strip_comments(tex_text or "")
# 多轮展开:逐步剥离最外层命令,同时保留参数中文
for _ in range(8):
new = _CMD_WITH_ARG_RE.sub(lambda m: m.group(1) or "", t)
if new == t:
break
t = new
t = _CMD_BARE_RE.sub("", t)
t = t.replace("{", "").replace("}", "")
t = t.replace("[", "").replace("]", "")
t = _WS_RE.sub(" ", t)
return t.strip()
def check_opening(tex_text: str, *, cjk_chars: int) -> Dict[str, Any]:
"""
启发式开篇检查:
- 在“开篇 cjk_chars 个中文字符”内同时命中“卡点/局限”与“突破/切入点”两类信号词。
"""
plain = _latex_to_plain_text(tex_text or "")
# 提取前 N 个 CJK 字符(保留原顺序)
cjk = [ch for ch in plain if "\u3400" <= ch <= "\u9fff"]
head = "".join(cjk[: max(0, int(cjk_chars))])
gap_keywords = [
"卡点",
"瓶颈",
"局限",
"不足",
"挑战",
"难点",
"痛点",
"短板",
"难以",
"受限于",
]
breakthrough_keywords = [
"突破",
"切入",
"本项目",
"本研究",
"拟解决",
"提出",
"针对",
"创新",
"关键思路",
"核心思路",
]
hit_gap = any(k in head for k in gap_keywords)
hit_break = any(k in head for k in breakthrough_keywords)
issues: List[str] = []
if not head:
issues.append("开篇内容过短:无法完成 300 字直击核心检查")
if not hit_gap:
issues.append("开篇未明显点出“领域卡点/局限/瓶颈”(建议用 1-2 句明确指出现有方法受限之处)")
if not hit_break:
issues.append("开篇未明显给出“本项目的突破式切入/关键思路”(建议用 1 句指出拟突破的关键切口)")
return {
"cjk_chars": int(cjk_chars),
"head_cjk_len": len(head),
"ok": bool(head) and hit_gap and hit_break,
"hit_gap": hit_gap,
"hit_breakthrough": hit_break,
"issues": issues,
}
def count_unique_citations(tex_text: str) -> int:
return len(set(parse_cite_keys(tex_text or "")))
def describe_constraints_summary(cfg: Mapping[str, Any]) -> str:
"""
用于 CLI/报告的简短说明(避免到处重复 hardcode)。
"""
page = load_page_limit(cfg)
wc = load_word_count_limit(cfg)
refs = load_reference_limit(cfg)
return (
f"页数 {page.min_pages}-{page.max_pages}(推荐 {page.recommended[0]}-{page.recommended[1]})"
f";字数 {wc.min_n}-{wc.max_n}"
f";核心文献 {refs.min_n}-{refs.max_n}"
)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
from .config_access import get_mapping
from .hard_rules import QualityRule, StructureRule, load_quality_rule, load_structure_rule
from .latex_parser import parse_subsubsections, strip_comments
from .reference_validator import CitationCheckResult, check_citations
from .validator import snapshot_third_party_constraints
from .wordcount import WordCountResult, count_cjk_chars
@dataclass(frozen=True)
class Tier1Report:
structure_ok: bool
subsubsection_count: int
missing_subsubsections: List[str]
citation_ok: bool
missing_citation_keys: List[str]
missing_doi_keys: List[str]
invalid_doi_keys: List[str]
word_count: int
forbidden_phrases_hits: List[str]
avoid_commands_hits: List[str]
constraints: Dict[str, Any]
@dataclass
class DiagnosticReport:
tier1: Tier1Report
tier2: Optional[Dict[str, Any]] = None
dimension_coverage: Optional[Dict[str, Any]] = None
boastful_expressions: Optional[Dict[str, Any]] = None
word_target: Optional[Dict[str, Any]] = None
notes: List[str] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
return {
"tier1": {
"structure_ok": self.tier1.structure_ok,
"subsubsection_count": self.tier1.subsubsection_count,
"missing_subsubsections": self.tier1.missing_subsubsections,
"citation_ok": self.tier1.citation_ok,
"missing_citation_keys": self.tier1.missing_citation_keys,
"missing_doi_keys": self.tier1.missing_doi_keys,
"invalid_doi_keys": self.tier1.invalid_doi_keys,
"word_count": self.tier1.word_count,
"forbidden_phrases_hits": self.tier1.forbidden_phrases_hits,
"avoid_commands_hits": self.tier1.avoid_commands_hits,
"constraints": self.tier1.constraints,
},
"tier2": self.tier2,
"dimension_coverage": self.dimension_coverage,
"boastful_expressions": self.boastful_expressions,
"word_target": self.word_target,
"notes": self.notes,
}
def _check_structure(text: str, rule: StructureRule) -> tuple[bool, int, List[str]]:
secs = parse_subsubsections(text)
count = len(secs)
if count < int(rule.min_subsubsection_count):
missing = list(rule.expected_subsubsections) if rule.expected_subsubsections else []
return False, count, missing
if not rule.strict_title_match or not rule.expected_subsubsections:
return True, count, []
titles = {s.title for s in secs}
missing = [t for t in rule.expected_subsubsections if t not in titles]
return (len(missing) == 0), count, missing
def _check_quality(text: str, rule: QualityRule) -> tuple[List[str], List[str]]:
t = strip_comments(text)
forbidden_hits = [p for p in rule.high_risk_examples if p and (p in t)]
cmd_hits = [c for c in rule.avoid_commands if c and (c in t)]
return forbidden_hits, cmd_hits
def run_tier1(
*,
tex_text: str,
project_root: Path,
config: Dict[str, Any],
) -> Tier1Report:
structure_rule = load_structure_rule(config)
quality_rule = load_quality_rule(config)
structure_ok, count, missing_sections = _check_structure(tex_text, structure_rule)
targets = get_mapping(config, "targets")
bib_globs = targets.get("bib_globs", ["references/*.bib"])
cite_result: CitationCheckResult = check_citations(
tex_text=tex_text, project_root=project_root, bib_globs=bib_globs
)
citation_ok = len(cite_result.missing_keys) == 0
wc_cfg = get_mapping(config, "word_count")
mode = str(wc_cfg.get("mode", "cjk_only")).strip() or "cjk_only"
wc: WordCountResult = count_cjk_chars(tex_text, mode=mode)
forbidden_hits, cmd_hits = _check_quality(tex_text, quality_rule)
constraints: Dict[str, Any] = snapshot_third_party_constraints(tex_text=tex_text, config=config)
return Tier1Report(
structure_ok=structure_ok,
subsubsection_count=count,
missing_subsubsections=missing_sections,
citation_ok=citation_ok,
missing_citation_keys=cite_result.missing_keys,
missing_doi_keys=cite_result.missing_doi_keys,
invalid_doi_keys=cite_result.invalid_doi_keys,
word_count=wc.cjk_count,
forbidden_phrases_hits=forbidden_hits,
avoid_commands_hits=cmd_hits,
constraints=constraints,
)
def format_tier1(report: Tier1Report) -> str:
lines: List[str] = []
if report.structure_ok:
lines.append(f"- ✅ 结构完整:subsubsection={report.subsubsection_count}")
else:
missing = "、".join(report.missing_subsubsections) if report.missing_subsubsections else "(未知)"
lines.append(f"- ❌ 结构缺失:subsubsection={report.subsubsection_count},缺少:{missing}")
if report.citation_ok:
lines.append("- ✅ 引用格式:所有 \\cite{...} 均在 .bib 中存在")
else:
lines.append(f"- ❌ 引用缺失:.bib 未找到 keys:{', '.join(report.missing_citation_keys)}")
if report.missing_doi_keys:
lines.append(f"- ⚠️ DOI 缺失:建议补齐(可用 DOI/链接线索补齐 .bib)keys:{', '.join(report.missing_doi_keys[:10])}")
if report.invalid_doi_keys:
lines.append(f"- ⚠️ DOI 格式疑似不合法:建议核验/修正 keys:{', '.join(report.invalid_doi_keys[:10])}")
lines.append(f"- ℹ️ 字数统计(中文字符,不含注释):{report.word_count}")
c = report.constraints or {}
if isinstance(c, dict) and c:
page = c.get("page_limit") if isinstance(c.get("page_limit"), dict) else {}
pages = c.get("estimated_pages")
pstatus = str(c.get("page_status", "") or "")
if isinstance(page, dict) and isinstance(pages, (int, float)):
rec = page.get("recommended", [])
rec_text = f"{rec[0]}-{rec[1]}" if isinstance(rec, list) and len(rec) >= 2 else "6-8"
lim_text = f"{page.get('min', 6)}-{page.get('max', 10)}"
if pstatus in {"near_limit", "exceed"}:
lines.append(f"- ⚠️ 页数预警(经验估算):{pages} 页(目标 {lim_text},推荐 {rec_text})")
else:
lines.append(f"- ℹ️ 预估页数(经验估算):{pages} 页(目标 {lim_text},推荐 {rec_text})")
refs = c.get("references_range") if isinstance(c.get("references_range"), dict) else {}
uniq = c.get("references_unique")
rstatus = str(refs.get("status", "") or "")
if isinstance(refs, dict) and isinstance(uniq, int):
rng = f"{refs.get('min', 30)}-{refs.get('max', 50)}"
prefix = "⚠️" if rstatus in {"too_few", "too_many"} else "ℹ️"
lines.append(f"- {prefix} 核心文献(去重 cite keys):{uniq}(建议 {rng})")
opening = c.get("opening") if isinstance(c.get("opening"), dict) else {}
if isinstance(opening, dict) and "ok" in opening:
ok = bool(opening.get("ok"))
n = int(opening.get("cjk_chars", 300))
if ok:
lines.append(f"- ✅ 开篇 {n} 字:卡点/局限 + 突破/切入点 信号均命中(启发式)")
else:
issues = opening.get("issues", [])
if isinstance(issues, list) and issues:
lines.append(f"- ⚠️ 开篇 {n} 字:{issues[0]}")
if report.forbidden_phrases_hits:
lines.append(f"- ⚠️ 高风险表述(示例命中):{', '.join(report.forbidden_phrases_hits)}")
if report.avoid_commands_hits:
lines.append(f"- ⚠️ 可能破坏模板的命令:{', '.join(report.avoid_commands_hits)}")
return "\n".join(lines).strip() + "\n"
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging
def configure_logging(*, verbose: bool = False) -> None:
"""
统一脚本与核心模块的日志输出口径:
- 默认:仅输出 WARNING 及以上(stderr)
- --verbose:输出 DEBUG(stderr)
"""
level = logging.DEBUG if verbose else logging.WARNING
root = logging.getLogger()
if not root.handlers:
logging.basicConfig(level=level, format="%(message)s")
return
root.setLevel(level)
for h in root.handlers:
h.setLevel(level)
Related skills
FAQ
What sections does nsfc-justification-writer draft?
nsfc-justification-writer drafts NSFC grant justification content covering project necessity, innovation claims, and expected impact in the formal structure and tone required by Chinese National Natural Science Foundation reviewers.
Who should use nsfc-justification-writer?
nsfc-justification-writer suits Chinese academic researchers preparing NSFC proposals who need agent-assisted drafting of justification sections like 立项依据 aligned with funder conventions.