关于 constraints-extractor
constraints-extractor 是一项文档技能,用于从项目资料中提取有证据支持的外部约束,并汇总到规范的 CONSTRAINTS.md。它适合需要追踪合规、安全、托管或利益相关方边界的项目团队。
主要功能
- 01按外部影响判断约束
- 02为条目保留来源证据
- 03标记相互冲突的约束
- 04区分约束与近似项
- 05通过脚本合并约束条目
适用场景
- 01整理项目文档中的安全与合规边界
- 02记录影响技术选择的外部要求
- 03核对现有约束与新发现是否冲突
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ gohypergiant/ agent-skills --skill constraints-extractor安装后在 Claude Code 中输入/constraints-extractor确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
CHANGELOG.mdREADME.mdevals/evals.jsonreferences/category-guide.mdentry-format.mdtemplate.mdtroubleshooting.mdscripts/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.yaml | onboard-openspec 技能 | 团队选择了什么(技术栈、模式) |
AGENTS.md / CLAUDE.md | onboard-agent 技能 | Agent 应如何表现 |
ARCHITECTURE.md | architecture-doc 技能 | 系统当前的架构方式 |
CONSTRAINTS.md | 本技能 | 为什么某些选择不可协商 |
完整的类别定义、详细展开的强制函数测试,以及每个类别的范围内/范围外示例表,都位于 references/category-guide.md —— 在运行逐文档提取之前请先阅读它。
输出位置
在扫描任何内容之前,先确定 CONSTRAINTS.md 应放在哪里,这样 Source Discovery 就知道应将现有文件视为权威,而不是作为扫描素材。
- 在做任何其他事情之前,先在仓库中搜索现有的
CONSTRAINTS.md文件。使用仓库范围的搜索(find、rg或等效工具),而不是只对根目录或docs/做狭窄检查。如果恰好存在一个,那就是目标 —— 使用它,不要询问。 - 如果存在多个
CONSTRAINTS.md,停下来并展示匹配项。询问用户哪一个才是权威,而不是猜测。 - 如果不存在任何文件,默认使用仓库根目录下的
CONSTRAINTS.md,与ARCHITECTURE.md和AGENTS.md放在一起。 - 在继续之前宣布所选路径:"未找到现有的 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:
- 完整读取分配给它的文件。
- 对它发现的每一条规则、限制或边界陈述应用强制函数测试。
- 对每个候选约束,提取:
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或其他位置)。不要猜测;宁可不写。
- 还会标记擦边候选——读起来像约束但未通过强制函数测试的陈述——并附上一行原因。这些只会在写入前预览中呈现(见“写入前预览”),绝不会写入
CONSTRAINTS.md本身。 - 将完整发现以 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 处理——绝不要每个波次 各运行一次。
- 用一次调用读取所有临时文件——
cat /tmp/constraints-extractor.XXXXXX/*.json——而不是用单独的工具调用逐个打开。 - 按类别对候选项分组。
- 关联重复项。 如果两个或更多来源支持同一个底层约束,则合并为
单条发现,将其证据引用(按 file+location 去重,而非累加)合并到一个
evidence列表中。每条引用都保留自己的note——合并发现 不会把 note 相互合并。 - 暴露冲突。 如果两个来源对同一边界做出互不兼容的断言,
不要选择其中一方。构建一个
claims列表——每一方一个条目, 各自带有自己的statement、file和location——将confidence设为CONFLICTING,并完全丢弃该条发现的evidence/impact字段(CONFLICTING发现携带claims而非二者,绝不兼有)。 - 路由近似项。 在逐文档提取期间被标记为近似项的任何内容,
都会带入关联发现文件,供预览步骤显示——它不会在
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 结构、结构容错层,以及合并脚本的确切行为。仅在运行实际失败时才需要。