mcpField
C
constraints-extractor

constraints-extractor

github.com/gohypergiant文档处理2577 次安装2 周前更新
代码仓库

关于 constraints-extractor

constraints-extractor 是一项文档技能,用于从项目资料中提取有证据支持的外部约束,并汇总到规范的 CONSTRAINTS.md。它适合需要追踪合规、安全、托管或利益相关方边界的项目团队。

主要功能

  1. 01按外部影响判断约束
  2. 02为条目保留来源证据
  3. 03标记相互冲突的约束
  4. 04区分约束与近似项
  5. 05通过脚本合并约束条目

适用场景

  1. 01整理项目文档中的安全与合规边界
  2. 02记录影响技术选择的外部要求
  3. 03核对现有约束与新发现是否冲突

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/gohypergiant/agent-skills --skill constraints-extractor

安装后在 Claude Code 中输入/constraints-extractor确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

CHANGELOG.md
README.md
evals/
evals.json
references/
category-guide.md
entry-format.md
template.md
troubleshooting.md
scripts/
merge_constraints.py

详细介绍

约束提取器

从项目文档中提取显式和隐式约束,并将其综合为一份规范的 CONSTRAINTS.md —— 一份持续更新的记录,记录那些外部施加的边界(合规、安全、托管、工具、利益相关方、范围、依赖),这些边界决定了项目能够构建什么以及如何构建。

与其他持续更新文档的关系

本技能刻意保持狭窄。如果某段内容不是明确的约束,它就不会进入本文件。在写下任何内容之前,先应用强制函数测试:

Forcing-function test: if this were ignored, would something break OUTSIDE
the codebase — a contract, a law, a security posture, an executive decision,
a vendor relationship? If yes, it's a constraint.

If the honest answer is "the team would just be inconsistent" or "it would
look different," it's a pattern or a procedure, not a constraint — it
belongs in config.yaml or AGENTS.md instead.
文档生成者记录内容
openspec/config.yamlonboard-openspec 技能团队选择了什么(技术栈、模式)
AGENTS.md / CLAUDE.mdonboard-agent 技能Agent 应如何表现
ARCHITECTURE.mdarchitecture-doc 技能系统当前的架构方式
CONSTRAINTS.md本技能为什么某些选择不可协商

完整的类别定义、详细展开的强制函数测试,以及每个类别的范围内/范围外示例表,都位于 references/category-guide.md —— 在运行逐文档提取之前请先阅读它。

输出位置

在扫描任何内容之前,先确定 CONSTRAINTS.md 应放在哪里,这样 Source Discovery 就知道应将现有文件视为权威,而不是作为扫描素材。

  1. 在做任何其他事情之前,先在仓库中搜索现有的 CONSTRAINTS.md 文件。使用仓库范围的搜索(find、rg 或等效工具),而不是只对根目录或 docs/ 做狭窄检查。如果恰好存在一个,那就是目标 —— 使用它,不要询问。
  2. 如果存在多个 CONSTRAINTS.md,停下来并展示匹配项。询问用户哪一个才是权威,而不是猜测。
  3. 如果不存在任何文件,默认使用仓库根目录下的 CONSTRAINTS.md,与 ARCHITECTURE.md 和 AGENTS.md 放在一起。
  4. 在继续之前宣布所选路径:"未找到现有的 CONSTRAINTS.md —— 我将写入 ./CONSTRAINTS.md。"

提取约束时绝不要做的事

  • 绝不要编造源文档中没有证据支持的约束。 如果某个候选陈述背后没有真实证据,它就不符合 CONFIRMED 的标准——应将其视为擦边项(参见下文“写入前预览”),而不是低置信度条目。对于证据薄弱的条目,没有第三层级可以退而求其次。
  • 绝不要因为来源冲突就悄悄丢弃某个约束。 记录双方,将该条目标记为 CONFLICTING,并分别引用每个来源。
  • 绝不要写入没有任何证据引用的约束(文件加上章节或行号引用)。没有来源的约束读起来和幻觉出来的没有区别。
  • 绝不要将措辞自信的团队偏好当作约束。 应用强制函数检验。如果没有明显的外部强制力,就在写入前预览中将其作为擦边项呈现,并排除在文件之外——参见下文“写入前预览”。
  • 绝不要在 CONSTRAINTS.md 中写入“已审查,未收录”章节或任何类似章节。 擦边项属于对话输出,不是文件内容——参见“写入前预览”。
  • 绝不要向 CONSTRAINTS.md 添加“相关文档”章节。 链接是单向的:AGENTS.md 或 CLAUDE.md 通过交叉链接步骤指向此文件,而不是反过来。反向指回那些已经引用此文件的文件纯粹是噪音。
  • 绝不要在正在扫描的仓库内的任何位置写入草稿或发现结果 JSON 文件,也绝不要使用相对文件名。 每个中间文件——每个子 Agent 的草稿输出和唯一的关联发现结果文件——都位于绝对路径 /tmp/constraints-extractor.XXXXXX/ 下。相对文件名会相对于 Agent 的工作目录解析,而工作目录是正在扫描的仓库,不是 /tmp。
  • 绝不要手写或手工格式化 CONSTRAINTS.md 中的条目。 每个新增或已确认的条目——包括单个新条目——都要经过 scripts/merge_constraints.py。手工格式化正是文件中出现不一致结构(临时符号、额外字段、参差不齐的空格)的原因,而且它破坏了脚本在下次运行时解析回该文件的能力。
  • 绝不要指望 scripts/merge_constraints.py 自行移除过时条目或替换错误引用。 该脚本在设计上是增量的——它插入新发现,并通过合并证据来确认已有条目,但它没有删除路径,即使新一轮扫描直接与现有的 CONFIRMED 条目矛盾(这种情况会变成一个 CONFLICTING 发现,它仍然是增加声明,而不是移除原有条目)。移除过时条目或修正错误引用,是直接、经确认地编辑 CONSTRAINTS.md 唯一正确的情形——具体流程参见下文“移除过时条目和错误引用”。这并不是上述规则的漏洞:它是在用户明确确认下修正已有的、格式本已正确的行,绝不是发明新的临时结构。
  • 绝不要自动爬取 openspec/changes/archive/——它会增长得过大,无法进行全语料扫描,并会让已解决变更的噪音淹没真正的信号。仅当用户明确指名时,才读取特定的归档文件。
  • 在有子 Agent 可用时,绝不要串行运行子 Agent 提取——并行派发,最多 5 个并发子 Agent。超过 5 个源文档意味着多轮执行,而不是无限并发。
  • 绝不要在未先阅读的情况下覆盖现有的 CONSTRAINTS.md——可能有人手工修正过置信度评级,或解决过新一轮扫描无法恢复的歧义。

来源发现

在生成任何子 Agent 之前,先汇集要扫描的源文档集合。 在派发子 Agent 之前,向用户公布最终列表。

第 1 步 — 自动发现常规位置。 检查以下位置,如果存在则纳入:

  • docs/、documents/、doc/(递归 — markdown 和纯文本)
  • README.md、CONTRIBUTING.md、SECURITY.md、COMPLIANCE.md、LICENSE*
  • ARCHITECTURE.md、AGENTS.md / CLAUDE.md、openspec/config.yaml — 这些是决策输出,但它们的叙述有时带有被埋没的理据,从未被提升为一等约束
  • 决策记录风格的文件夹(如果存在):adr/、decisions/、rfd/、 rfcs/、notes/、spikes/ — 任何存放 NOTE/SPIKE/RFC 风格文档的文件夹都是高信号,因为已解决决策的理据恰恰是强制函数往往被明确写下的地方
  • 在"输出位置"中确定的位置上已存在的 CONSTRAINTS.md(如果存在)(完整阅读它 — 见下文"定位或创建 CONSTRAINTS.md",不要把它仅仅当作另一个扫描目标)

将上述每一项检查作为一条合并的 bash 命令运行,而不是每个路径一次单独的工具调用 — 这些都是快速的、不涉及推理的存在性检查,因此批量执行只需一次往返,而不是十几次:

for p in docs documents doc README.md CONTRIBUTING.md SECURITY.md \
         COMPLIANCE.md ARCHITECTURE.md AGENTS.md CLAUDE.md \
         openspec/config.yaml adr decisions rfd rfcs notes spikes; do
  [ -e "$p" ] && echo "FOUND: $p"
done
ls LICENSE* 2>/dev/null

第 2 步 — 接受用户显式提供的列表。 如果用户指定了具体的文件或路径 — 包括 openspec/changes/archive/ 内的文件 — 将它们加入集合。这是对第 1 步的补充,绝不是对它的替代。

第 3 步 — 去重并公布。

Merge (Step 1 ∪ Step 2) by resolved path, then tell the user:

"Scanning N documents across ceil(N/5) waves of up to 5 subagents each:
[list]. This excludes openspec/changes/archive/ by default since it's
typically too large for a full pass — name specific files there if you
want them included."

如果合并后的集合为空,停止并询问用户的文档存放在哪里,而不是猜测路径。


逐文档提取(并行子 Agent)

为每个源文档生成一个子 Agent — 并行,绝不串行,最多 5 个并发子 Agent。如果发现的集合中有超过 5 个文档,则以每批最多 5 个的方式派发:等待一批完成后再开始下一批。

批次只是派发批处理层面的问题。它们不会改变下文中的 单次关联遍历 — 该遍历仍然只运行一次,在所有批次完成后运行,绝不按批次运行。

每个子 Agent:

  1. 完整读取分配给它的文件。
  2. 对它发现的每一条规则、限制或边界陈述应用强制函数测试。
  3. 对每个候选约束,提取:
    • statement — 用简明英文表述的约束,自包含(必须在不打开源文件的情况下也能正确阅读)
    • impact — 一到三句话说明实际影响:这约束了什么,它应影响哪些决策,若被忽视会有什么后果。只要包含某条约束就必须提供。如果 subagent 无法清晰表述这一点,通常说明该候选只是擦边,而不是真正的约束。
    • category — 精确的 slug,逐字使用:compliance-governance、 security-privacy-ip-cui、hosting-infrastructure、 tooling-approved-path、workflow-sequencing、 stakeholder-executive、scope-prioritization-delivery 或 external-dependencies。完整定义见 references/category-guide.md。使用 slug 本身,而不是它的缩写或展示形式——merge_constraints.py 会自动纠正少数常见变体,但对任何无法确信解析的内容会硬失败,而这里的任何不匹配都会导致某个类别的发现悄无声息地丢失。
    • confidence — 在此阶段始终为 CONFIRMED。不存在 “弱证据”层级——如果该陈述没有真实引用作为支撑,它就是擦边,而不是较低置信度的约束。 CONFLICTING 只会在下面的单次关联遍历中分配,即当两条已提取的发现相互矛盾时;单个文档的 subagent 自己无法看到这一点。
    • evidence — 引用列表,每项包含 file(从项目根目录起的相对路径,不是绝对路径)、location(在源有真实行号时使用行号或行号范围,否则使用简短的章节锚点)以及 note(必须用一句话说明该具体引用展示了什么——这会成为该引用在条目中的 Evidence notes 项目符号,因此它需要有自己的价值,而不只是重述 statement)。
    • affects — 可选列表,包含裸 token(规则 ID、其他约束 ID、系统名称),即该约束的执行会触及的内容。不要猜测源中并未实际命名的 token;宁可不写或留空。
    • enforced_by — 可选列表,包含裸 token,命名由该约束驱动的某个具体下游规则(位于 AGENTS.md、ARCHITECTURE.md、config.yaml 或其他位置)。不要猜测;宁可不写。
  4. 还会标记擦边候选——读起来像约束但未通过强制函数测试的陈述——并附上一行原因。这些只会在写入前预览中呈现(见“写入前预览”),绝不会写入 CONSTRAINTS.md 本身。
  5. 将完整发现以 JSON 写入 mktemp -d "/tmp/constraints-extractor.XXXXXX" 下的一个绝对路径临时文件(每个 subagent 一个文件——绝不要使用相对文件名,绝不要放在正在扫描的仓库内任何位置),并且只向 orchestrator 返回一行确认,例如 Extracted 4 constraints, 2 near-misses from docs/SECURITY.md。让大量发现远离 orchestrator 的上下文——orchestrator 会在单次关联遍历期间直接读取临时文件;它不会以内联方式接收发现。

在此阶段或关联阶段,绝不要分配条目的 CONSTR-<CATEGORY>-<NNN> ID——ID 分配只发生一次,在 merge_constraints.py 内部,按类别限定范围。提取和关联始终只产生发现;该脚本是身份的唯一所有者。


单次关联处理

在所有子代理完成后,只运行一次 reduce 处理——绝不要每个波次 各运行一次。

  1. 用一次调用读取所有临时文件——cat /tmp/constraints-extractor.XXXXXX/*.json——而不是用单独的工具调用逐个打开。
  2. 按类别对候选项分组。
  3. 关联重复项。 如果两个或更多来源支持同一个底层约束,则合并为 单条发现,将其证据引用(按 file+location 去重,而非累加)合并到一个 evidence 列表中。每条引用都保留自己的 note——合并发现 不会把 note 相互合并。
  4. 暴露冲突。 如果两个来源对同一边界做出互不兼容的断言, 不要选择其中一方。构建一个 claims 列表——每一方一个条目, 各自带有自己的 statement、file 和 location——将 confidence 设为 CONFLICTING,并完全丢弃该条发现的 evidence/impact 字段(CONFLICTING 发现携带 claims 而非二者,绝不兼有)。
  5. 路由近似项。 在逐文档提取期间被标记为近似项的任何内容, 都会带入关联发现文件,供预览步骤显示——它不会在 CONSTRAINTS.md 中获得一个章节。

将关联结果写入 <scratch-dir>/correlated-findings.json—— 一个与临时文件位于同一 /tmp 目录中的绝对路径,绝不 使用裸相对文件名。顶层结构是一个扁平列表,而不是按类别分组:

{"constraints": [ {"category": "workflow-sequencing", "title": "...", ...}, ... ], "near_misses": [...]}

而不是 {"workflow-sequencing": [...], "compliance-governance": [...]}。 合并脚本读取 constraints 数组并读取每一项自己的 category 字段——它不知道如何解释用作顶层 JSON 键的类别名称。 它会显式检查 constraints 键,如果缺失则拒绝运行,并列出它实际找到的 键,因此这会表现为一个清晰的错误,而不是静默的零结果写入。 有关合并脚本期望的完整形态,请参见 references/troubleshooting.md(仅在 排查合并步骤问题时需要;日常使用不需要阅读它)。


定位或创建 CONSTRAINTS.md

使用上文“输出位置”中确定的路径。

  • 如果它存在: 先完整读取它。将任何手工编辑的条目视为 比同一约束的新扫描版本更具权威性,除非新证据直接与之矛盾—— 在这种情况下,在预览步骤中标记该冲突,而不是静默覆盖。将新 发现就地合并到正确的类别章节中。
  • 如果它不存在: 从 references/template.md 全新创建它。

所有用于添加和佐证的机械式文件操作——ID 分配、类别插入、 去重合并、排序——都由 scripts/merge_constraints.py 处理。绝不 用临时的字符串追加来手工编辑文件结构;朴素的文件末尾追加会破坏 类别分组。该脚本是新条目或佐证条目如何被插入的唯一事实来源。 它无法移除条目或引用——有关直接编辑文件才是正确做法的唯一情形, 请参见下文“移除过时条目和错误引用”:

python3 scripts/merge_constraints.py \
  --target <absolute path to CONSTRAINTS.md> \
  --findings <absolute path to correlated-findings.json under /tmp>

两个参数都必须是绝对路径。该脚本在 --findings 路径为相对路径时会 拒绝运行,正是因为相对路径会相对于被扫描的仓库解析,而不是 /tmp。

该脚本是幂等的——用同一发现文件重新运行它不会产生任何差异。

未通过验证的发现(错误的类别、缺失证据、无法强制转换的未识别 形态)会被单独跳过——它不会阻止批次中其余内容被写入。每次运行后 检查脚本的标准输出:它会报告已写入的内容和被跳过的内容, 并给出每次跳过的具体原因。将跳过的发现转达给相关人员,而不是 把成功退出当作“本次运行的所有内容都已写入”——部分写入仍然是一次 成功的运行。


删除过期条目和错误引用

merge_constraints.py 仅做增量处理:它会插入新发现,并通过合并证据来佐证已有发现,但它没有删除路径。它永远不会删除条目、丢弃引用,或替换已经失效的引用,即使新一轮扫描直接与现有的 CONFIRMED 条目相矛盾——这种情况会变成一个 CONFLICTING 发现,仍然是 添加 声明,而不是删除原始条目。如果实际需要的是删除或修复引用,那么带着新发现再次运行脚本并不会产生这种结果。

有两种情况需要这样做,而不是进行一次正常的合并运行:

  • 过期条目。 该约束不再成立——源文档被重写、策略发生变化、供应商关系终止——而新的发现文件中没有任何内容能够取代它,因为脚本只按类别 + 标题进行匹配,对于「这不再适用」没有任何可匹配的依据。
  • 错误引用。 现有条目上的 file:location 不再指向它曾经指向的文本(文件移动了、章节被重写了、行号偏移了),因此该条目的证据悄然不再支撑它自己的陈述。

处理这两种情况的方式与对文件进行的任何其他更改相同:在下面的「写入前预览」中连同原因一起呈现,获得用户确认,然后手动进行编辑——先运行 merge_constraints.py,以处理同一轮产生的任何新增或佐证内容,之后再手动进行清理编辑,这样脚本的解析并渲染流程就不会在编辑过程中重新解析文件。

「手动」是指按照 references/entry-format.md 修正或删除已有的、格式已经正确的行——绝不是自由形式的重新格式化,也绝不是趁你已经在文件里就手写一个新条目的借口:

  • 完全删除一个条目: 删除整个 ### CONSTR-<ID> · <title> 块,包括其末尾的空行。如果这导致其类别部分没有任何条目,请恢复 *No entries yet.* 标记,以免该部分留空。绝不要回收已删除的 ID——未来的 merge_constraints.py 运行会分配下一个空闲编号,而不管中间缺少了什么。
  • 修复某个条目上一条错误引用,而该条目在其他方面仍然成立: 只编辑 Evidence: 行中该引用的片段(以及如果该引用是被丢弃而非修正,则编辑其 (N refs) 计数),并编辑 Evidence notes: 下对应的列表项——根据 references/entry-format.md 中的字段规则,两者在两个列表中的位置相同。该条目上的其他所有字段保持不变。

这是本 skill 中唯一一处直接编辑 CONSTRAINTS.md 是正确做法、而非验证变通方法的地方:上面的「绝不手写条目」规则的存在是为了阻止临时结构混入,而不是为了让文件冻结,阻止一个脚本自身无法做出的、经人工确认的修正。


写入前先预览

在改动文件系统之前,先向用户展示一份摘要:

  • 新发现的约束,按类别分组
  • 由于来自多个来源的相互印证而使证据引用计数增加的约束
  • 所有 CONFLICTING 条目,并排展示每一条主张
  • 发现的近似匹配项,附上其一行的原因——只在这里展示;它们永远不会被写入 CONSTRAINTS.md
  • 本次运行中发现的过期条目或错误引用,标记为待删除或待更正,并附上各自的原因——参见上文“Removing Stale Entries and Bad Citations”。这些需要与这里其他所有内容相同的明确确认,因为 merge_constraints.py 无法对它们执行操作,唯一的途径是直接、手动确认的编辑。

询问:“这样看起来对吗?在我写入之前,有什么需要重新分类、合并或删除的吗?”

错误的合规边界比其他地方错误的参考文档条目更糟——即使是小的差异,也不要跳过这一步。如果一次运行产生零条新约束,就直说,而不是为了让文件看起来有产出而堆砌内容。


从 Agent 行为文件建立交叉链接

如果项目根目录存在 AGENTS.md 或 CLAUDE.md,检查目标文件是否已经有引用 CONSTRAINTS.md 的“Related Documentation”部分。如果没有,提议追加一条引用。

只有指向约束文件的链接存在时,这些约束对未来的 Agent 运行才有用。CONSTRAINTS.md 本身应保持为纯粹的列表,它不应引用 Agent 行为文件,但反过来应成立:AGENTS.md 或 CLAUDE.md(如果存在其一)应引用约束文件,这样之后在本项目中工作的 Agent 就会将其作为上下文拾取。

按顺序检查项目根目录下是否存在 ./AGENTS.md 或 ./CLAUDE.md。如果 ./CLAUDE.md 存在,但其内容只是指向 AGENTS.md 的导入指针(例如,该文件的内容本质上只是 @AGENTS.md),并且 ./AGENTS.md 也存在,则把 AGENTS.md 当作真正的目标,而不是编辑那个指针文件。如果两个文件都不存在,跳过这一步;不要从零创建一个,那超出了本 skill 的范围。

如果找到了目标文件,先完整读取它,然后:

  • 如果它已经有 ## Related Documentation 部分,检查约束文件是否已经列在那里(搜索其路径,或 CONSTRAINTS.md)。如果已列出,就不要动这个文件。如果没有,就在该部分添加一个条目,完整保留每一个现有条目。
  • 如果不存在这样的部分,就在文件末尾添加一个。

无论哪种方式,条目都像这样,用约束文件的实际路径替换 CONSTRAINTS.md:

- **CONSTRAINTS.md** — Externally-imposed boundaries (compliance,
  security, hosting, stakeholder) that shape what this project can build
  and how.
  _(Check this before scoping any change with compliance,
  security, hosting, or stakeholder-priority implications — it is not
  optional background reading)_

用纯文本编辑来完成,不要用配套脚本。这是每个文件一次性的存在性检查,不是大批量的排序或去重——这里没有持续的机械性工作来证明脚本的合理性。


参考文件

  • references/category-guide.md —— 八个约束类别、用示例完整推演过的强制函数测试,以及每个类别的范围内与范围外表。在逐文档提取之前阅读。
  • references/template.md —— 用于创建新文件的裸 CONSTRAINTS.md 骨架。除了八个类别标题外什么都没有——没有示例内容,因此首次解析时它绝不会被误认为是真实条目。
  • references/entry-format.md —— merge_constraints.py 渲染和解析的 CONSTRAINTS.md 条目语法。每次运行读一次。
  • references/troubleshooting.md —— 提取与关联产生的 JSON 结构、结构容错层,以及合并脚本的确切行为。仅在运行实际失败时才需要。

评论

支持 Markdown