视觉画布设计
canvas-design
基于设计原则创作静态视觉作品,用于海报与品牌图形。
constraints-extractor【文档与办公】Extract explicit and implicit constraints (compliance, security, hosting, tooling, stakeholder, scope, and external-dependency boundaries) from a project's documentation, then build or update a canonical CONSTRAINTS.md. Spawns one subagent per source document to flag externally-imposed boundaries with evidence citations and confidence ratings, then c…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Extract explicit and implicit constraints from project documentation and
synthesize them into a canonical CONSTRAINTS.md — a living record of the
externally-imposed boundaries (compliance, security, hosting, tooling,
stakeholder, scope, dependency) that shape what a project can build and how.
This skill is deliberately narrow. If a piece of content isn't clearly a constraint, it stays out of this file. Apply the forcing-function test before writing anything down:
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.
| Document | Produced by | Captures |
|---|---|---|
openspec/config.yaml | onboard-openspec skill | WHAT the team chose (stack, patterns) |
AGENTS.md / CLAUDE.md | onboard-agent skill | HOW the agent should behave |
ARCHITECTURE.md | architecture-doc skill | HOW the system is structured today |
CONSTRAINTS.md | this skill | WHY certain choices are non-negotiable |
Full category definitions, the forcing-function test worked through in
detail, and an in-scope/out-of-scope example table per category live in
references/category-guide.md — read it before running per-document extraction.
Determine where CONSTRAINTS.md belongs before scanning anything, so
Source Discovery knows to treat an existing file as authoritative rather
than as scan fodder.
CONSTRAINTS.md file before doing
anything else. Use a repo-wide search (find, rg, or equivalent), not a
narrow check of only the root or docs/. If exactly one exists, that is the
target — use it, don't ask.CONSTRAINTS.md exists, stop and show the matches. Ask the
user which one is authoritative rather than guessing.CONSTRAINTS.md at the repo root, alongside
ARCHITECTURE.md and AGENTS.md.CONFIRMED — treat it as a near-miss instead
(see "Preview Before Writing" below), not a low-confidence entry. There
is no third tier to fall back on for weak evidence.CONFLICTING, and cite each source.CONSTRAINTS.md. Near-misses are conversation output, not
file content — see "Preview Before Writing."CONSTRAINTS.md. The
linking is one-directional: AGENTS.md or CLAUDE.md point to this file via the cross-link step, not the other
way around. A reverse pointer back to files that already reference this
one is pure noise./tmp/constraints-extractor.XXXXXX/ path. A relative filename resolves
against the agent's working directory, which is the repo being scanned,
not /tmp.CONSTRAINTS.md. Every
new or corroborated entry — including a single new entry — goes
through scripts/merge_constraints.py. Hand-formatting is how
inconsistent structure (ad hoc symbols, extra fields, uneven spacing)
ends up in the file, and it breaks the script's ability to parse the
file back in on the next run.scripts/merge_constraints.py to remove a stale entry
or replace a bad citation on its own. The script is additive by
design — it inserts new findings and corroborates existing ones by
merging evidence, but it has no delete path, even when a fresh scan
directly contradicts an existing CONFIRMED entry (that case becomes a
CONFLICTING finding, which still adds claims rather than removing
the original). Removing a stale entry or fixing a bad citation is the
one case where a direct, confirmed edit to CONSTRAINTS.md is
correct — see "Removing Stale Entries and Bad Citations" below for the
exact procedure. That is not a loophole in the rule above: it's
correcting existing, already-correctly-formatted lines under explicit
user confirmation, never inventing new ad hoc structure.openspec/changes/archive/ automatically — it grows too
large for a full-corpus scan and drowns real signal in resolved-change
noise. Only read specific archive files if the user names them explicitly.CONSTRAINTS.md without reading it first
— a human may have hand-corrected a confidence rating or resolved an
ambiguity that a fresh scan can't recover.Before spawning any subagents, assemble the set of source documents to scan. Announce the final list to the user before dispatching subagents.
Step 1 — Auto-discover conventional locations. Check for, and if present, include:
docs/, documents/, doc/ (recursively — markdown and plain text)README.md, CONTRIBUTING.md, SECURITY.md, COMPLIANCE.md, LICENSE*ARCHITECTURE.md, AGENTS.md / CLAUDE.md, openspec/config.yaml —
these are decision outputs, but their prose sometimes carries buried
rationale that never got promoted to a first-class constraintadr/, decisions/, rfd/,
rfcs/, notes/, spikes/ — any folder holding NOTE/SPIKE/RFC-style
documents is high-signal, since rationale for a resolved decision is
exactly where a forcing function tends to get written down explicitlyCONSTRAINTS.md at the location determined in "Output
Location" above, if one exists (read it fully — see "Locate or Create
CONSTRAINTS.md" below, do not treat it as just another scan target)Run every check above as one consolidated bash command, not a separate tool call per path — these are all fast existence checks with no reasoning involved, so batching them costs one round trip instead of a dozen:
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
Step 2 — Accept an explicit user-provided list. If the user names
specific files or paths — including files inside
openspec/changes/archive/ — add them to the set. This is additive to
Step 1, never a replacement for it.
Step 3 — Deduplicate and announce.
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."
If the merged set is empty, stop and ask the user where their documentation lives rather than guessing at paths.
Spawn one subagent per source document — parallel, never serial, capped at 5 concurrent subagents. With more than 5 documents in the discovered set, dispatch in waves of up to 5: wait for a wave to finish before starting the next.
Waves are a dispatch-batching concern only. They do not change the Single Correlation Pass below — that still runs exactly once, after every wave has finished, never once per wave.
Each subagent:
statement — the constraint in plain English, self-contained (must
read correctly without the source file open)impact — one to three sentences on practical impact: what this
constrains, what decisions it should shape, what breaks if ignored.
Required whenever a constraint is included. If a subagent can't
articulate this, that's usually a sign the candidate is a near-miss,
not a real constraint.category — the exact slug, verbatim: compliance-governance,
security-privacy-ip-cui, hosting-infrastructure,
tooling-approved-path, workflow-sequencing,
stakeholder-executive, scope-prioritization-delivery, or
external-dependencies. Full definitions in
references/category-guide.md. Use the slug itself, not a
shortened or display form of it — merge_constraints.py will
auto-correct a few common variants but hard-fails on anything it
can't confidently resolve, and a mismatch here is how a category's
findings silently go missing.confidence — always CONFIRMED at this stage. There is no
"weak evidence" tier — if the statement isn't backed by a real
citation, it's a near-miss, not a lower-confidence constraint.
CONFLICTING only gets assigned during the Single Correlation Pass
below, when two extracted findings disagree; a single-document
subagent can't see that on its own.evidence — a list of citations, each with file (relative path
from the project root, not an absolute path), location (a line
number or range where the source has real line numbers, a short
section anchor otherwise), and note (one required sentence on
what that specific citation shows — this becomes the entry's
Evidence notes bullet for that citation, so it needs to earn its
own line, not just restate the statement).affects — optional list of bare tokens (rule IDs, other
constraint IDs, system names) this constraint's enforcement touches.
Omit or leave empty rather than guessing at a token that isn't
actually named in the source.enforced_by — optional list of bare tokens naming a specific
downstream rule (in AGENTS.md, ARCHITECTURE.md, config.yaml,
or elsewhere) that this constraint drives. Omit rather than
guessing.CONSTRAINTS.md itself.mktemp -d "/tmp/constraints-extractor.XXXXXX" (one file per
subagent — never a relative filename, never anywhere inside the
repository being scanned) and returns only a one-line confirmation to
the orchestrator, e.g. Extracted 4 constraints, 2 near-misses from docs/SECURITY.md. Keep bulk findings out of the orchestrator's context
— the orchestrator reads scratch files directly during the single
correlation pass; it does not receive findings inline.Never assign an entry's CONSTR-<CATEGORY>-<NNN> ID at this stage, or at
correlation — ID assignment happens once, inside merge_constraints.py,
scoped per category. Extraction and correlation only ever produce
findings; the script is the sole owner of identity.
After all subagents complete, run exactly one reduce pass — never one per wave.
cat /tmp/constraints-extractor.XXXXXX/*.json — rather than opening each
one with a separate tool call.evidence list. Every citation keeps its own note — merging findings
doesn't merge notes into one another.claims list — one entry
per side, each with its own statement, file, and location — set
confidence to CONFLICTING, and drop the evidence/impact fields
for that finding entirely (a CONFLICTING finding carries claims
instead, never both).CONSTRAINTS.md.Write the correlated result to <scratch-dir>/correlated-findings.json —
an absolute path in the same /tmp directory as the scratch files, never
a bare relative filename. The top-level shape is a flat list, not a
grouping by category:
{"constraints": [ {"category": "workflow-sequencing", "title": "...", ...}, ... ], "near_misses": [...]}
Not {"workflow-sequencing": [...], "compliance-governance": [...]}. The
merge script reads the constraints array and reads each item's own
category field — it does not know how to interpret category names used
as top-level JSON keys. It checks for the constraints key explicitly
and refuses to run if it's missing, naming the keys it actually found, so
this shows up as a clear error rather than a silent zero-result write.
See references/troubleshooting.md for the full shape the merge script
expects (only needed if you're troubleshooting the merge step; day-to-day
use doesn't require reading it).
Use the path determined in "Output Location" above.
references/template.md.All mechanical file operations for additions and corroboration — ID
assignment, category insertion, dedup merge, ordering — are handled by
scripts/merge_constraints.py. Never hand-edit the file's structure
with ad hoc string appends; naive end-of-file appending corrupts
category grouping. The script is the single source of truth for how new
or corroborated entries get inserted. It cannot remove an entry or a
citation — see "Removing Stale Entries and Bad Citations" below for the
one case where editing the file directly is correct instead:
python3 scripts/merge_constraints.py \
--target <absolute path to CONSTRAINTS.md> \
--findings <absolute path to correlated-findings.json under /tmp>
Both arguments must be absolute paths. The script refuses to run with a
relative --findings path, precisely because a relative path resolves
against the repo being scanned rather than /tmp.
The script is idempotent — re-running it with the same findings file produces no diff.
A finding that fails validation (bad category, missing evidence, an unrecognized shape it can't coerce) is skipped individually — it does not block the rest of the batch from being written. Check the script's stdout after every run: it reports both what was written and what was skipped, with the specific reason for each skip. Relay skipped findings back to the person rather than treating a successful exit as "everything from this run made it in" — a partial write is still a successful run.
merge_constraints.py is additive only: it inserts new findings and
corroborates existing ones by merging evidence, but it has no delete
path. It will never remove an entry, drop a citation, or replace one
that's gone bad, even when a fresh scan directly contradicts an existing
CONFIRMED entry — that case becomes a CONFLICTING finding, which
still adds claims rather than removing the original. If removal or a
citation fix is what's actually needed, running the script again with
new findings will not produce it.
Two situations call for this instead of a normal merge run:
file:location on an existing entry no longer
points to the text it once did (the file moved, the section was
rewritten, the line numbers shifted), so the entry's evidence silently
stops backing its own statement.Handle either the same way as any other change to the file: surface it
in "Preview Before Writing" below with the reason, get the user's
confirmation, then make the edit by hand — run merge_constraints.py
first for whatever additions or corroboration the same pass produced,
and make the manual cleanup edit afterward, so the script's
parse-and-render pass isn't reparsing a file mid-edit.
"By hand" means correcting or deleting existing, already-correctly-
formatted lines per references/entry-format.md — never freeform
reformatting, and never an excuse to hand-write a new entry while you're
already in the file:
Evidence: line (and its
(N refs) count if the citation is being dropped rather than
corrected) and the matching bullet under Evidence notes: — same
position in both lists, per the field rules in
references/entry-format.md. Leave every other field on the entry
untouched.This is the one place in this skill where editing CONSTRAINTS.md
directly is correct instead of a validation workaround: the "never
hand-write an entry" rule above exists to stop ad hoc structure from
creeping in, not to freeze the file against a human-confirmed correction
the script has no mechanism to make itself.
Show the user a summary before touching the filesystem:
CONFLICTING entries, with every claim shown side by sideCONSTRAINTS.mdmerge_constraints.py
cannot act on them and the only path is a direct, hand-confirmed edit.Ask: "Does this look right? Anything to reclassify, merge, or drop before I write it?"
A wrong compliance boundary is worse than a wrong reference doc entry elsewhere — do not skip this step even for small diffs. If a run produces zero new constraints, say so plainly rather than padding the file to look productive.
If AGENTS.md or CLAUDE.md exist at the project root, check whether the
target already has a "Related Documentation" section referencing
CONSTRAINTS.md. If not, offer to append a reference.
The constraints are only useful to future agent runs if something points them at it. CONSTRAINTS.md itself should stay a pure list, it should not reference agent behavior files, but the reverse should hold: AGENTS.md or CLAUDE.md, if either exists, should reference the constraints file so a later agent working in this project picks it up as context.
Check, in order, whether ./AGENTS.md or ./CLAUDE.md exists at the project root. If ./CLAUDE.md exists but its content is just an import pointer to AGENTS.md (for example, the file's content is essentially just @AGENTS.md), and ./AGENTS.md exists too, treat AGENTS.md as the real target instead of editing the pointer file. If neither file exists, skip this step; do not create one from scratch, that is out of scope for this skill.
If a target file was found, read it fully first, then:
## Related Documentation section, check whether the constraints file is already listed there (search for its path, or CONSTRAINTS.md). If it is, leave the file alone. If not, add one entry to that section, preserving every existing entry exactly as it was.Either way, the entry looks like this, with the constraints's actual path in place of 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)_
Do this with plain prose edits, not a companion script. This is a single one-time presence check per file, not high-volume sorting or deduplication — there's no ongoing mechanical work here that justifies a script.
references/category-guide.md — the eight constraint categories, the
forcing-function test worked through with examples, and an in-scope vs.
out-of-scope table per category. Read before per-document extraction.references/template.md — the bare CONSTRAINTS.md skeleton used for
new-file creation. Nothing but the eight category headings — no example
content, so it can never be mistaken for real entries on first parse.references/entry-format.md — the CONSTRAINTS.md entry grammar that
merge_constraints.py renders and parses. Read once per run.references/troubleshooting.md — the JSON shapes extraction and
correlation produce, the shape-tolerance layer, and the merge script's
exact behavior. Only needed if a run actually fails.