formwork-kit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- formwork_cli/__init__.py +326 -0
- formwork_cli/kit/COSTS.md +111 -0
- formwork_cli/kit/adapters/claude-code/README.md +53 -0
- formwork_cli/kit/adapters/claude-code/settings.json +46 -0
- formwork_cli/kit/adapters/codex/README.md +43 -0
- formwork_cli/kit/adapters/cursor/README.md +45 -0
- formwork_cli/kit/adapters/gemini-cli/README.md +47 -0
- formwork_cli/kit/build +410 -0
- formwork_cli/kit/check/checks/config-shape +123 -0
- formwork_cli/kit/check/checks/decision-ids +159 -0
- formwork_cli/kit/check/checks/doc-links +133 -0
- formwork_cli/kit/check/checks/generated-current +74 -0
- formwork_cli/kit/check/checks/guard-wired +139 -0
- formwork_cli/kit/check/checks/kit-integrity +199 -0
- formwork_cli/kit/check/checks/predictions-first +127 -0
- formwork_cli/kit/check/checks/role-shape +172 -0
- formwork_cli/kit/check/checks/rule-labels +135 -0
- formwork_cli/kit/check/fixtures/config-shape/must-fail/documents-a-section-that-does-not-exist/.formwork.toml +5 -0
- formwork_cli/kit/check/fixtures/config-shape/must-fail/documents-a-section-that-does-not-exist/formwork/guide.md +13 -0
- formwork_cli/kit/check/fixtures/config-shape/must-fail/rules-as-a-switchboard/.formwork.toml +8 -0
- formwork_cli/kit/check/fixtures/config-shape/must-pass/layers-kept-apart/.formwork.toml +5 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-fail/a-placeholder-shipped/docs/decisions/0003-still-pending.md +7 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-fail/superseded-by-nothing/docs/decisions/0002-old.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-fail/two-decisions-one-number/docs/decisions/0007-first.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-fail/two-decisions-one-number/docs/decisions/0007-second.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0001-the-first.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0002-the-second.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0003-the-third.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-pass/nothing-recorded-yet/docs/decisions/README.md +3 -0
- formwork_cli/kit/check/fixtures/doc-links/must-fail/never-written/index.md +7 -0
- formwork_cli/kit/check/fixtures/doc-links/must-fail/renamed-file/architecture-notes.md +3 -0
- formwork_cli/kit/check/fixtures/doc-links/must-fail/renamed-file/guide.md +8 -0
- formwork_cli/kit/check/fixtures/doc-links/must-pass/links-resolve/architecture-notes.md +1 -0
- formwork_cli/kit/check/fixtures/doc-links/must-pass/links-resolve/guide.md +5 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.claude/agents/sample.md +22 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.codex/agents/sample.toml +22 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.gemini/agents/sample.md +23 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/formwork/build +349 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/formwork/roles/method/sample.md +18 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.claude/agents/sample.md +20 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.codex/agents/sample.toml +22 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.gemini/agents/sample.md +23 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/formwork/build +349 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/formwork/roles/method/sample.md +18 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/nothing-is-generated-here/README.md +3 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-fail/declared-but-no-file/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-fail/file-but-not-wired/.claude/settings.json +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-fail/file-but-not-wired/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-pass/declared-and-wired/.claude/settings.json +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-pass/declared-and-wired/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-pass/nothing-declared/README.md +1 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-check-went-missing/formwork/check/checks/still-here +2 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-check-went-missing/state/fingerprints.txt +2 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-guard-was-altered/formwork/guard/git-boundary +3 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-guard-was-altered/state/fingerprints.txt +1 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-pass/everything-matches/formwork/guard/git-boundary +2 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-pass/everything-matches/state/fingerprints.txt +1 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/architect.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/researcher.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/round.md +4 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/a-round-that-has-not-argued-yet/docs/rounds/0006-not-started/round.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/no-rounds-at-all/docs/README.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/architect.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/predictions.md +4 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/researcher.md +3 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/claims-a-grant-binds-everywhere/formwork/roles/README.md +6 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/claims-a-grant-binds-everywhere/formwork/roles/complete.md +18 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/missing-a-section/formwork/roles/vague.md +16 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/spawn-without-being-lead/formwork/roles/eager.md +18 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/two-roles-one-job/formwork/roles/first.md +18 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/two-roles-one-job/formwork/roles/second.md +18 -0
- formwork_cli/kit/check/fixtures/role-shape/must-pass/well-formed/formwork/roles/complete.md +18 -0
- formwork_cli/kit/check/fixtures/rule-labels/must-fail/claims-enforcement-that-does-not-exist/formwork/rules/core.md +9 -0
- formwork_cli/kit/check/fixtures/rule-labels/must-fail/no-catches/formwork/rules/core.md +9 -0
- formwork_cli/kit/check/fixtures/rule-labels/must-fail/unlabelled/formwork/rules/core.md +7 -0
- formwork_cli/kit/check/fixtures/rule-labels/must-pass/well-formed/formwork/rules/core.md +10 -0
- formwork_cli/kit/check/run +340 -0
- formwork_cli/kit/check/test_gate.py +222 -0
- formwork_cli/kit/first-run.md +204 -0
- formwork_cli/kit/fw +121 -0
- formwork_cli/kit/glossary.md +160 -0
- formwork_cli/kit/guard/git-boundary +627 -0
- formwork_cli/kit/guard/protected-files +748 -0
- formwork_cli/kit/guard/quality-gate +260 -0
- formwork_cli/kit/guard/test_boundary.py +273 -0
- formwork_cli/kit/guard/test_protection.py +254 -0
- formwork_cli/kit/guard/test_quality_gate.py +156 -0
- formwork_cli/kit/install +395 -0
- formwork_cli/kit/limits.md +141 -0
- formwork_cli/kit/loop.md +82 -0
- formwork_cli/kit/roles/HOW-TO-ADD-A-ROLE.md +105 -0
- formwork_cli/kit/roles/TEMPLATE.md +26 -0
- formwork_cli/kit/roles/method/architect.md +269 -0
- formwork_cli/kit/roles/method/challenger.md +243 -0
- formwork_cli/kit/roles/method/lead.md +280 -0
- formwork_cli/kit/roles/method/record-keeper.md +206 -0
- formwork_cli/kit/roles/method/researcher.md +246 -0
- formwork_cli/kit/roles/method/reviewer.md +207 -0
- formwork_cli/kit/roles/packs/accessibility.md +236 -0
- formwork_cli/kit/roles/packs/ai.md +248 -0
- formwork_cli/kit/roles/packs/analyst.md +233 -0
- formwork_cli/kit/roles/packs/backend.md +425 -0
- formwork_cli/kit/roles/packs/brainstormer.md +190 -0
- formwork_cli/kit/roles/packs/data.md +212 -0
- formwork_cli/kit/roles/packs/devops.md +203 -0
- formwork_cli/kit/roles/packs/frontend.md +224 -0
- formwork_cli/kit/roles/packs/integrations.md +215 -0
- formwork_cli/kit/roles/packs/legal.md +251 -0
- formwork_cli/kit/roles/packs/marketing.md +206 -0
- formwork_cli/kit/roles/packs/mobile.md +202 -0
- formwork_cli/kit/roles/packs/performance.md +192 -0
- formwork_cli/kit/roles/packs/product.md +217 -0
- formwork_cli/kit/roles/packs/security.md +267 -0
- formwork_cli/kit/roles/packs/sre.md +203 -0
- formwork_cli/kit/roles/packs/tester.md +246 -0
- formwork_cli/kit/roles/packs/user-researcher.md +218 -0
- formwork_cli/kit/roles/packs/ux.md +205 -0
- formwork_cli/kit/roles/packs/visual.md +199 -0
- formwork_cli/kit/roles/packs/writer.md +198 -0
- formwork_cli/kit/round.md +131 -0
- formwork_cli/kit/rules/core.md +195 -0
- formwork_cli/kit/rules/full.md +493 -0
- formwork_cli/kit/templates/brief.md +68 -0
- formwork_cli/kit/templates/decision.md +93 -0
- formwork_cli/kit/templates/predictions.md +54 -0
- formwork_cli/kit/templates/report.md +52 -0
- formwork_cli/kit/templates/round.md +77 -0
- formwork_cli/kit/test_install.py +165 -0
- formwork_cli/kit/troubleshooting.md +247 -0
- formwork_cli/kit-page/FORMWORK.md +182 -0
- formwork_kit-0.1.0.dist-info/METADATA +308 -0
- formwork_kit-0.1.0.dist-info/RECORD +137 -0
- formwork_kit-0.1.0.dist-info/WHEEL +4 -0
- formwork_kit-0.1.0.dist-info/entry_points.txt +2 -0
- formwork_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check: every role is complete, and no two roles claim the same job.
|
|
3
|
+
|
|
4
|
+
role-shape <directory>
|
|
5
|
+
|
|
6
|
+
Over every role in formwork/roles/:
|
|
7
|
+
|
|
8
|
+
1. it declares name, pack, owns and tools
|
|
9
|
+
2. it has all five sections
|
|
10
|
+
3. no other role claims the same `owns` slug
|
|
11
|
+
4. only the lead may hold `spawn`
|
|
12
|
+
|
|
13
|
+
CATCHES A role with no stated boundary, which wanders into somebody else's
|
|
14
|
+
work and nobody notices because nothing said where the edge was.
|
|
15
|
+
|
|
16
|
+
And two roles owning one thing, which is how two agents produce
|
|
17
|
+
opposite answers with equal confidence.
|
|
18
|
+
|
|
19
|
+
Exit status:
|
|
20
|
+
0 every role complete, every job owned once
|
|
21
|
+
1 at least one problem, named
|
|
22
|
+
2 the check could not run
|
|
23
|
+
"""
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
ROLES_SUBDIR = os.path.join("formwork", "roles")
|
|
29
|
+
SKIP = {"TEMPLATE.md", "HOW-TO-ADD-A-ROLE.md", "README.md"}
|
|
30
|
+
SECTIONS = ("Owns", "Does not own", "Tools", "Stops when", "Would be wrong if")
|
|
31
|
+
FIELDS = ("name", "pack", "owns", "tools")
|
|
32
|
+
MAY_SPAWN = {"lead"}
|
|
33
|
+
|
|
34
|
+
SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv",
|
|
35
|
+
"venv", "build", "dist", "target", "vendor",
|
|
36
|
+
"site-packages", ".tox"}
|
|
37
|
+
# Paths the runner has told this check to stay out of.
|
|
38
|
+
EXCLUDED = [os.path.abspath(p) for p in
|
|
39
|
+
os.environ.get("FORMWORK_EXCLUDE", "").split(os.pathsep) if p]
|
|
40
|
+
|
|
41
|
+
# Which runtimes can actually hold a role to its tool grant, established by
|
|
42
|
+
# reading each publisher's own documentation. Recorded in docs/role-formats.md.
|
|
43
|
+
#
|
|
44
|
+
# Claude Code, Gemini CLI a named list of permitted tools
|
|
45
|
+
# Cursor read-only, or not. One bit, nothing finer
|
|
46
|
+
# Codex a sandbox mode, which is not a tool list at all
|
|
47
|
+
ENFORCES_GRANTS = {"claude-code"}
|
|
48
|
+
# Gemini CLI takes a named list and would enforce it. The documented name of
|
|
49
|
+
# its file-writing tool is not established, so the generator writes no list for
|
|
50
|
+
# any role that writes — which today is 26 of 27. Enforced in principle,
|
|
51
|
+
# advisory in practice, and this check reports the practice.
|
|
52
|
+
ADVISORY_GRANTS = {"cursor", "codex", "gemini-cli (for now)"}
|
|
53
|
+
|
|
54
|
+
# A claim that a grant binds somewhere it cannot. Written as a check rather
|
|
55
|
+
# than a sentence, because a sentence gets deleted by somebody tidying up.
|
|
56
|
+
OVERCLAIM = re.compile(
|
|
57
|
+
r"(?i)(?:tool\s+grants?|grants?)\s+(?:are\s+|is\s+)?enforced\s+(?:on|in|by)"
|
|
58
|
+
r"\s+(?:all\s+(?:four\s+)?runtimes|every\s+runtime|cursor|codex)")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def frontmatter(text):
|
|
62
|
+
m = re.match(r"^---\n(.*?)\n---\n", text, re.S)
|
|
63
|
+
if not m:
|
|
64
|
+
return None
|
|
65
|
+
out = {}
|
|
66
|
+
for line in m.group(1).split("\n"):
|
|
67
|
+
if ":" in line:
|
|
68
|
+
k, v = line.split(":", 1)
|
|
69
|
+
out[k.strip()] = v.strip()
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def role_files(root):
|
|
74
|
+
base = os.path.join(root, ROLES_SUBDIR)
|
|
75
|
+
if not os.path.isdir(base):
|
|
76
|
+
return None
|
|
77
|
+
found = []
|
|
78
|
+
for dirpath, dirnames, filenames in os.walk(base):
|
|
79
|
+
for fn in sorted(filenames):
|
|
80
|
+
if fn.endswith(".md") and fn not in SKIP:
|
|
81
|
+
found.append(os.path.join(dirpath, fn))
|
|
82
|
+
return found
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main(argv):
|
|
86
|
+
if len(argv) < 2:
|
|
87
|
+
print("usage: role-shape <directory>", file=sys.stderr)
|
|
88
|
+
return 2
|
|
89
|
+
root = argv[1]
|
|
90
|
+
if not os.path.isdir(root):
|
|
91
|
+
print("ERROR: not a directory: %s" % root, file=sys.stderr)
|
|
92
|
+
return 2
|
|
93
|
+
|
|
94
|
+
files = role_files(root)
|
|
95
|
+
if files is None:
|
|
96
|
+
print("no roles directory, nothing claimed")
|
|
97
|
+
return 0
|
|
98
|
+
if not files:
|
|
99
|
+
print("ERROR: a roles directory with no roles in it", file=sys.stderr)
|
|
100
|
+
return 2
|
|
101
|
+
|
|
102
|
+
problems = []
|
|
103
|
+
claimed = {}
|
|
104
|
+
for path in files:
|
|
105
|
+
rel = os.path.relpath(path, root)
|
|
106
|
+
try:
|
|
107
|
+
text = open(path, encoding="utf-8", errors="strict").read()
|
|
108
|
+
except (UnicodeDecodeError, OSError) as e:
|
|
109
|
+
print("ERROR: cannot read %s: %s" % (path, e), file=sys.stderr)
|
|
110
|
+
return 2
|
|
111
|
+
|
|
112
|
+
fm = frontmatter(text)
|
|
113
|
+
if fm is None:
|
|
114
|
+
problems.append("%s — no frontmatter" % rel)
|
|
115
|
+
continue
|
|
116
|
+
for field in FIELDS:
|
|
117
|
+
if not fm.get(field):
|
|
118
|
+
problems.append("%s — no '%s' declared" % (rel, field))
|
|
119
|
+
|
|
120
|
+
for section in SECTIONS:
|
|
121
|
+
if not re.search(r"^\*\*%s\.?\*\*" % re.escape(section), text, re.M):
|
|
122
|
+
problems.append("%s — no '%s' section" % (rel, section))
|
|
123
|
+
|
|
124
|
+
owns = fm.get("owns")
|
|
125
|
+
if owns:
|
|
126
|
+
if owns in claimed:
|
|
127
|
+
problems.append("%s — claims '%s', already owned by %s"
|
|
128
|
+
% (rel, owns, claimed[owns]))
|
|
129
|
+
else:
|
|
130
|
+
claimed[owns] = rel
|
|
131
|
+
|
|
132
|
+
if "spawn" in fm.get("tools", "") and fm.get("name") not in MAY_SPAWN:
|
|
133
|
+
problems.append("%s — holds 'spawn', which only the lead may hold"
|
|
134
|
+
% rel)
|
|
135
|
+
|
|
136
|
+
# Nothing in the kit may claim a grant binds where the runtime cannot
|
|
137
|
+
# express it. Two of the four can only approximate one.
|
|
138
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
139
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
140
|
+
here = os.path.abspath(dirpath)
|
|
141
|
+
if any(here == e or here.startswith(e + os.sep) for e in EXCLUDED):
|
|
142
|
+
dirnames[:] = []
|
|
143
|
+
continue
|
|
144
|
+
for fn in sorted(filenames):
|
|
145
|
+
if not fn.endswith(".md"):
|
|
146
|
+
continue
|
|
147
|
+
full = os.path.join(dirpath, fn)
|
|
148
|
+
try:
|
|
149
|
+
text = open(full, encoding="utf-8", errors="ignore").read()
|
|
150
|
+
except OSError:
|
|
151
|
+
continue
|
|
152
|
+
for m in OVERCLAIM.finditer(text):
|
|
153
|
+
line = text[:m.start()].count("\n") + 1
|
|
154
|
+
problems.append("%s:%d — claims a tool grant is enforced where "
|
|
155
|
+
"it cannot be. It binds on %s and is advice on "
|
|
156
|
+
"%s" % (os.path.relpath(full, root), line,
|
|
157
|
+
", ".join(sorted(ENFORCES_GRANTS)),
|
|
158
|
+
", ".join(sorted(ADVISORY_GRANTS))))
|
|
159
|
+
|
|
160
|
+
if problems:
|
|
161
|
+
print("%d problem(s) across %d role(s)" % (len(problems), len(files)))
|
|
162
|
+
for p in problems:
|
|
163
|
+
print(" %s" % p)
|
|
164
|
+
return 1
|
|
165
|
+
|
|
166
|
+
print("%d role(s), each complete, each owning something nobody else does"
|
|
167
|
+
% len(files))
|
|
168
|
+
return 0
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check: every rule is labelled, says what it catches, and does not lie.
|
|
3
|
+
|
|
4
|
+
rule-labels <directory>
|
|
5
|
+
|
|
6
|
+
Three things, over every rule in formwork/rules/:
|
|
7
|
+
|
|
8
|
+
1. it says whether it is Enforced or Advice
|
|
9
|
+
2. it has a "Catches:" line
|
|
10
|
+
3. if it claims to be enforced, the check it names exists
|
|
11
|
+
|
|
12
|
+
CATCHES A rule that says it is enforced when nothing enforces it. Everything
|
|
13
|
+
reads correctly, the rule is in the file, and no program will ever
|
|
14
|
+
refuse anything. That is worse than honest advice, because you stop
|
|
15
|
+
watching for the thing yourself.
|
|
16
|
+
|
|
17
|
+
Also a rule with no "catches" line, which is a rule asking to be
|
|
18
|
+
obeyed without saying what for. Those get deleted in week two, and
|
|
19
|
+
they should be.
|
|
20
|
+
|
|
21
|
+
Exit status:
|
|
22
|
+
0 every rule is labelled, explained, and honest
|
|
23
|
+
1 at least one is not, and it is named
|
|
24
|
+
2 the check could not run
|
|
25
|
+
"""
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
RULES_SUBDIR = os.path.join("formwork", "rules")
|
|
31
|
+
CHECKS_SUBDIR = os.path.join("formwork", "check", "checks")
|
|
32
|
+
GUARD_SUBDIR = os.path.join("formwork", "guard")
|
|
33
|
+
|
|
34
|
+
LABEL = re.compile(r"^\*\*(Enforced|Advice)\b", re.M)
|
|
35
|
+
ENFORCER = re.compile(r"^\*\*Enforced\*\*\s+by\s+`([^`]+)`", re.M)
|
|
36
|
+
CATCHES = re.compile(r"^\*\*Catches:\*\*", re.M)
|
|
37
|
+
|
|
38
|
+
# A Catches line has to name a failure. "nothing" is the word a rule uses when
|
|
39
|
+
# it cannot, and one shipped saying exactly that while this check passed it.
|
|
40
|
+
EMPTY_CATCH = re.compile(
|
|
41
|
+
r"^\*\*Catches:\*\*\s*(nothing|none|n/a|-|tbd|todo)\s*\.?\s*$",
|
|
42
|
+
re.M | re.I)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def rules_in(text):
|
|
46
|
+
"""Split a rules file into (heading, body) at level-three headings."""
|
|
47
|
+
parts = re.split(r"^### (.+)$", text, flags=re.M)
|
|
48
|
+
out = []
|
|
49
|
+
for i in range(1, len(parts), 2):
|
|
50
|
+
out.append((parts[i].strip(), parts[i + 1]))
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def enforcer_exists(root, named):
|
|
55
|
+
"""A rule may name a check or a guard. It must be one that really runs.
|
|
56
|
+
|
|
57
|
+
This used to accept any file of that name anywhere, so `nonexistent/dir/
|
|
58
|
+
doc-links` passed because a file called doc-links exists somewhere. It now
|
|
59
|
+
requires the named thing to sit where the gate and the hooks look for it,
|
|
60
|
+
and to be runnable.
|
|
61
|
+
"""
|
|
62
|
+
base = os.path.basename(named)
|
|
63
|
+
if named not in (base, os.path.join(CHECKS_SUBDIR, base),
|
|
64
|
+
os.path.join(GUARD_SUBDIR, base)):
|
|
65
|
+
return False
|
|
66
|
+
for sub in (CHECKS_SUBDIR, GUARD_SUBDIR):
|
|
67
|
+
full = os.path.join(root, sub, base)
|
|
68
|
+
if os.path.isfile(full) and os.access(full, os.X_OK):
|
|
69
|
+
return True
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main(argv):
|
|
74
|
+
if len(argv) < 2:
|
|
75
|
+
print("usage: rule-labels <directory>", file=sys.stderr)
|
|
76
|
+
return 2
|
|
77
|
+
root = argv[1]
|
|
78
|
+
if not os.path.isdir(root):
|
|
79
|
+
print("ERROR: not a directory: %s" % root, file=sys.stderr)
|
|
80
|
+
return 2
|
|
81
|
+
|
|
82
|
+
rules_dir = os.path.join(root, RULES_SUBDIR)
|
|
83
|
+
if not os.path.isdir(rules_dir):
|
|
84
|
+
print("no rules directory, nothing claimed")
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
problems = []
|
|
88
|
+
counted = 0
|
|
89
|
+
for fn in sorted(os.listdir(rules_dir)):
|
|
90
|
+
if not fn.endswith(".md"):
|
|
91
|
+
continue
|
|
92
|
+
path = os.path.join(rules_dir, fn)
|
|
93
|
+
try:
|
|
94
|
+
text = open(path, encoding="utf-8", errors="strict").read()
|
|
95
|
+
except (UnicodeDecodeError, OSError) as e:
|
|
96
|
+
print("ERROR: cannot read %s: %s" % (path, e), file=sys.stderr)
|
|
97
|
+
return 2
|
|
98
|
+
|
|
99
|
+
for heading, body in rules_in(text):
|
|
100
|
+
counted += 1
|
|
101
|
+
where = "%s: %s" % (fn, heading)
|
|
102
|
+
label = LABEL.search(body)
|
|
103
|
+
if not label:
|
|
104
|
+
problems.append("%s — neither Enforced nor Advice" % where)
|
|
105
|
+
elif label.group(1) == "Enforced":
|
|
106
|
+
named = ENFORCER.search(body)
|
|
107
|
+
if not named:
|
|
108
|
+
problems.append("%s — says Enforced, names no check" % where)
|
|
109
|
+
elif not enforcer_exists(root, named.group(1)):
|
|
110
|
+
problems.append("%s — says Enforced by `%s`, which does not "
|
|
111
|
+
"exist" % (where, named.group(1)))
|
|
112
|
+
if not CATCHES.search(body):
|
|
113
|
+
problems.append("%s — no 'Catches:' line" % where)
|
|
114
|
+
elif EMPTY_CATCH.search(body):
|
|
115
|
+
problems.append("%s — its 'Catches:' line says nothing is "
|
|
116
|
+
"caught. A rule that cannot name a failure is "
|
|
117
|
+
"an opinion" % where)
|
|
118
|
+
|
|
119
|
+
if not counted:
|
|
120
|
+
print("ERROR: a rules directory with no rules in it", file=sys.stderr)
|
|
121
|
+
return 2
|
|
122
|
+
|
|
123
|
+
if problems:
|
|
124
|
+
print("%d problem(s) across %d rule(s)" % (len(problems), counted))
|
|
125
|
+
for p in problems:
|
|
126
|
+
print(" %s" % p)
|
|
127
|
+
return 1
|
|
128
|
+
|
|
129
|
+
print("%d rule(s), each labelled, explained, and honest about enforcement"
|
|
130
|
+
% counted)
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Guide
|
|
2
|
+
|
|
3
|
+
This fixture exists to make the doc-links check fail.
|
|
4
|
+
|
|
5
|
+
The file below was renamed, and this link was not updated. That is the whole
|
|
6
|
+
failure mode: the sentence still reads correctly.
|
|
7
|
+
|
|
8
|
+
See [the architecture notes](architecture.md) for the boundaries.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Architecture notes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sample
|
|
3
|
+
description: One clearly stated thing.
|
|
4
|
+
tools: Read, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- GENERATED FROM formwork/roles/method/sample.md — DO NOT EDIT. -->
|
|
8
|
+
<!-- Change the source and run formwork/build. A hand-edit here fails the gate. -->
|
|
9
|
+
|
|
10
|
+
# Sample
|
|
11
|
+
|
|
12
|
+
**Owns.** One clearly stated thing.
|
|
13
|
+
|
|
14
|
+
**Does not own.** Something else.
|
|
15
|
+
|
|
16
|
+
**Tools.** Reading.
|
|
17
|
+
|
|
18
|
+
**Stops when.** It reaches the edge.
|
|
19
|
+
|
|
20
|
+
**Would be wrong if.** It carried on.
|
|
21
|
+
|
|
22
|
+
<!-- somebody edited this by hand and it will vanish -->
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# GENERATED FROM formwork/roles/method/sample.md — DO NOT EDIT.
|
|
2
|
+
# Change the source and run formwork/build. A hand-edit here fails the gate.
|
|
3
|
+
|
|
4
|
+
name = "sample"
|
|
5
|
+
description = "One clearly stated thing."
|
|
6
|
+
|
|
7
|
+
# This runtime has a sandbox mode rather than a tool list, so
|
|
8
|
+
# the grant read is not expressed here. It is advice on Codex.
|
|
9
|
+
|
|
10
|
+
developer_instructions = """
|
|
11
|
+
# Sample
|
|
12
|
+
|
|
13
|
+
**Owns.** One clearly stated thing.
|
|
14
|
+
|
|
15
|
+
**Does not own.** Something else.
|
|
16
|
+
|
|
17
|
+
**Tools.** Reading.
|
|
18
|
+
|
|
19
|
+
**Stops when.** It reaches the edge.
|
|
20
|
+
|
|
21
|
+
**Would be wrong if.** It carried on.
|
|
22
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
runtime = "claude-code"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sample
|
|
3
|
+
description: One clearly stated thing.
|
|
4
|
+
kind: local
|
|
5
|
+
tools:
|
|
6
|
+
- read_file
|
|
7
|
+
- grep_search
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<!-- GENERATED FROM formwork/roles/method/sample.md — DO NOT EDIT. -->
|
|
11
|
+
<!-- Change the source and run formwork/build. A hand-edit here fails the gate. -->
|
|
12
|
+
|
|
13
|
+
# Sample
|
|
14
|
+
|
|
15
|
+
**Owns.** One clearly stated thing.
|
|
16
|
+
|
|
17
|
+
**Does not own.** Something else.
|
|
18
|
+
|
|
19
|
+
**Tools.** Reading.
|
|
20
|
+
|
|
21
|
+
**Stops when.** It reaches the edge.
|
|
22
|
+
|
|
23
|
+
**Would be wrong if.** It carried on.
|