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.
Files changed (137) hide show
  1. formwork_cli/__init__.py +326 -0
  2. formwork_cli/kit/COSTS.md +111 -0
  3. formwork_cli/kit/adapters/claude-code/README.md +53 -0
  4. formwork_cli/kit/adapters/claude-code/settings.json +46 -0
  5. formwork_cli/kit/adapters/codex/README.md +43 -0
  6. formwork_cli/kit/adapters/cursor/README.md +45 -0
  7. formwork_cli/kit/adapters/gemini-cli/README.md +47 -0
  8. formwork_cli/kit/build +410 -0
  9. formwork_cli/kit/check/checks/config-shape +123 -0
  10. formwork_cli/kit/check/checks/decision-ids +159 -0
  11. formwork_cli/kit/check/checks/doc-links +133 -0
  12. formwork_cli/kit/check/checks/generated-current +74 -0
  13. formwork_cli/kit/check/checks/guard-wired +139 -0
  14. formwork_cli/kit/check/checks/kit-integrity +199 -0
  15. formwork_cli/kit/check/checks/predictions-first +127 -0
  16. formwork_cli/kit/check/checks/role-shape +172 -0
  17. formwork_cli/kit/check/checks/rule-labels +135 -0
  18. formwork_cli/kit/check/fixtures/config-shape/must-fail/documents-a-section-that-does-not-exist/.formwork.toml +5 -0
  19. formwork_cli/kit/check/fixtures/config-shape/must-fail/documents-a-section-that-does-not-exist/formwork/guide.md +13 -0
  20. formwork_cli/kit/check/fixtures/config-shape/must-fail/rules-as-a-switchboard/.formwork.toml +8 -0
  21. formwork_cli/kit/check/fixtures/config-shape/must-pass/layers-kept-apart/.formwork.toml +5 -0
  22. formwork_cli/kit/check/fixtures/decision-ids/must-fail/a-placeholder-shipped/docs/decisions/0003-still-pending.md +7 -0
  23. formwork_cli/kit/check/fixtures/decision-ids/must-fail/superseded-by-nothing/docs/decisions/0002-old.md +6 -0
  24. formwork_cli/kit/check/fixtures/decision-ids/must-fail/two-decisions-one-number/docs/decisions/0007-first.md +6 -0
  25. formwork_cli/kit/check/fixtures/decision-ids/must-fail/two-decisions-one-number/docs/decisions/0007-second.md +6 -0
  26. formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0001-the-first.md +6 -0
  27. formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0002-the-second.md +6 -0
  28. formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0003-the-third.md +6 -0
  29. formwork_cli/kit/check/fixtures/decision-ids/must-pass/nothing-recorded-yet/docs/decisions/README.md +3 -0
  30. formwork_cli/kit/check/fixtures/doc-links/must-fail/never-written/index.md +7 -0
  31. formwork_cli/kit/check/fixtures/doc-links/must-fail/renamed-file/architecture-notes.md +3 -0
  32. formwork_cli/kit/check/fixtures/doc-links/must-fail/renamed-file/guide.md +8 -0
  33. formwork_cli/kit/check/fixtures/doc-links/must-pass/links-resolve/architecture-notes.md +1 -0
  34. formwork_cli/kit/check/fixtures/doc-links/must-pass/links-resolve/guide.md +5 -0
  35. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.claude/agents/sample.md +22 -0
  36. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.codex/agents/sample.toml +22 -0
  37. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.formwork.toml +1 -0
  38. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.gemini/agents/sample.md +23 -0
  39. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/formwork/build +349 -0
  40. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/formwork/roles/method/sample.md +18 -0
  41. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.claude/agents/sample.md +20 -0
  42. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.codex/agents/sample.toml +22 -0
  43. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.formwork.toml +1 -0
  44. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.gemini/agents/sample.md +23 -0
  45. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/formwork/build +349 -0
  46. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/formwork/roles/method/sample.md +18 -0
  47. formwork_cli/kit/check/fixtures/generated-current/must-pass/nothing-is-generated-here/README.md +3 -0
  48. formwork_cli/kit/check/fixtures/guard-wired/must-fail/declared-but-no-file/.formwork.toml +1 -0
  49. formwork_cli/kit/check/fixtures/guard-wired/must-fail/file-but-not-wired/.claude/settings.json +1 -0
  50. formwork_cli/kit/check/fixtures/guard-wired/must-fail/file-but-not-wired/.formwork.toml +1 -0
  51. formwork_cli/kit/check/fixtures/guard-wired/must-pass/declared-and-wired/.claude/settings.json +1 -0
  52. formwork_cli/kit/check/fixtures/guard-wired/must-pass/declared-and-wired/.formwork.toml +1 -0
  53. formwork_cli/kit/check/fixtures/guard-wired/must-pass/nothing-declared/README.md +1 -0
  54. formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-check-went-missing/formwork/check/checks/still-here +2 -0
  55. formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-check-went-missing/state/fingerprints.txt +2 -0
  56. formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-guard-was-altered/formwork/guard/git-boundary +3 -0
  57. formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-guard-was-altered/state/fingerprints.txt +1 -0
  58. formwork_cli/kit/check/fixtures/kit-integrity/must-pass/everything-matches/formwork/guard/git-boundary +2 -0
  59. formwork_cli/kit/check/fixtures/kit-integrity/must-pass/everything-matches/state/fingerprints.txt +1 -0
  60. formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/architect.md +3 -0
  61. formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/researcher.md +3 -0
  62. formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/round.md +4 -0
  63. 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
  64. formwork_cli/kit/check/fixtures/predictions-first/must-pass/no-rounds-at-all/docs/README.md +3 -0
  65. formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/architect.md +3 -0
  66. formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/predictions.md +4 -0
  67. formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/researcher.md +3 -0
  68. formwork_cli/kit/check/fixtures/role-shape/must-fail/claims-a-grant-binds-everywhere/formwork/roles/README.md +6 -0
  69. formwork_cli/kit/check/fixtures/role-shape/must-fail/claims-a-grant-binds-everywhere/formwork/roles/complete.md +18 -0
  70. formwork_cli/kit/check/fixtures/role-shape/must-fail/missing-a-section/formwork/roles/vague.md +16 -0
  71. formwork_cli/kit/check/fixtures/role-shape/must-fail/spawn-without-being-lead/formwork/roles/eager.md +18 -0
  72. formwork_cli/kit/check/fixtures/role-shape/must-fail/two-roles-one-job/formwork/roles/first.md +18 -0
  73. formwork_cli/kit/check/fixtures/role-shape/must-fail/two-roles-one-job/formwork/roles/second.md +18 -0
  74. formwork_cli/kit/check/fixtures/role-shape/must-pass/well-formed/formwork/roles/complete.md +18 -0
  75. formwork_cli/kit/check/fixtures/rule-labels/must-fail/claims-enforcement-that-does-not-exist/formwork/rules/core.md +9 -0
  76. formwork_cli/kit/check/fixtures/rule-labels/must-fail/no-catches/formwork/rules/core.md +9 -0
  77. formwork_cli/kit/check/fixtures/rule-labels/must-fail/unlabelled/formwork/rules/core.md +7 -0
  78. formwork_cli/kit/check/fixtures/rule-labels/must-pass/well-formed/formwork/rules/core.md +10 -0
  79. formwork_cli/kit/check/run +340 -0
  80. formwork_cli/kit/check/test_gate.py +222 -0
  81. formwork_cli/kit/first-run.md +204 -0
  82. formwork_cli/kit/fw +121 -0
  83. formwork_cli/kit/glossary.md +160 -0
  84. formwork_cli/kit/guard/git-boundary +627 -0
  85. formwork_cli/kit/guard/protected-files +748 -0
  86. formwork_cli/kit/guard/quality-gate +260 -0
  87. formwork_cli/kit/guard/test_boundary.py +273 -0
  88. formwork_cli/kit/guard/test_protection.py +254 -0
  89. formwork_cli/kit/guard/test_quality_gate.py +156 -0
  90. formwork_cli/kit/install +395 -0
  91. formwork_cli/kit/limits.md +141 -0
  92. formwork_cli/kit/loop.md +82 -0
  93. formwork_cli/kit/roles/HOW-TO-ADD-A-ROLE.md +105 -0
  94. formwork_cli/kit/roles/TEMPLATE.md +26 -0
  95. formwork_cli/kit/roles/method/architect.md +269 -0
  96. formwork_cli/kit/roles/method/challenger.md +243 -0
  97. formwork_cli/kit/roles/method/lead.md +280 -0
  98. formwork_cli/kit/roles/method/record-keeper.md +206 -0
  99. formwork_cli/kit/roles/method/researcher.md +246 -0
  100. formwork_cli/kit/roles/method/reviewer.md +207 -0
  101. formwork_cli/kit/roles/packs/accessibility.md +236 -0
  102. formwork_cli/kit/roles/packs/ai.md +248 -0
  103. formwork_cli/kit/roles/packs/analyst.md +233 -0
  104. formwork_cli/kit/roles/packs/backend.md +425 -0
  105. formwork_cli/kit/roles/packs/brainstormer.md +190 -0
  106. formwork_cli/kit/roles/packs/data.md +212 -0
  107. formwork_cli/kit/roles/packs/devops.md +203 -0
  108. formwork_cli/kit/roles/packs/frontend.md +224 -0
  109. formwork_cli/kit/roles/packs/integrations.md +215 -0
  110. formwork_cli/kit/roles/packs/legal.md +251 -0
  111. formwork_cli/kit/roles/packs/marketing.md +206 -0
  112. formwork_cli/kit/roles/packs/mobile.md +202 -0
  113. formwork_cli/kit/roles/packs/performance.md +192 -0
  114. formwork_cli/kit/roles/packs/product.md +217 -0
  115. formwork_cli/kit/roles/packs/security.md +267 -0
  116. formwork_cli/kit/roles/packs/sre.md +203 -0
  117. formwork_cli/kit/roles/packs/tester.md +246 -0
  118. formwork_cli/kit/roles/packs/user-researcher.md +218 -0
  119. formwork_cli/kit/roles/packs/ux.md +205 -0
  120. formwork_cli/kit/roles/packs/visual.md +199 -0
  121. formwork_cli/kit/roles/packs/writer.md +198 -0
  122. formwork_cli/kit/round.md +131 -0
  123. formwork_cli/kit/rules/core.md +195 -0
  124. formwork_cli/kit/rules/full.md +493 -0
  125. formwork_cli/kit/templates/brief.md +68 -0
  126. formwork_cli/kit/templates/decision.md +93 -0
  127. formwork_cli/kit/templates/predictions.md +54 -0
  128. formwork_cli/kit/templates/report.md +52 -0
  129. formwork_cli/kit/templates/round.md +77 -0
  130. formwork_cli/kit/test_install.py +165 -0
  131. formwork_cli/kit/troubleshooting.md +247 -0
  132. formwork_cli/kit-page/FORMWORK.md +182 -0
  133. formwork_kit-0.1.0.dist-info/METADATA +308 -0
  134. formwork_kit-0.1.0.dist-info/RECORD +137 -0
  135. formwork_kit-0.1.0.dist-info/WHEEL +4 -0
  136. formwork_kit-0.1.0.dist-info/entry_points.txt +2 -0
  137. 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,5 @@
1
+ [bindings]
2
+ runtime = "claude-code"
3
+
4
+ [strength]
5
+ git_boundary = "block"
@@ -0,0 +1,13 @@
1
+ # Guide
2
+
3
+ Switch role packs on here:
4
+
5
+ ```toml
6
+ [bindings]
7
+ runtime = "claude-code"
8
+
9
+ [roles]
10
+ packs = ["method"]
11
+ ```
12
+
13
+ There is no `[roles]` section in the real file, so this instruction is fiction.
@@ -0,0 +1,8 @@
1
+ [bindings]
2
+ runtime = "claude-code"
3
+
4
+ [strength]
5
+ git_boundary = "block"
6
+
7
+ [rules]
8
+ one_unit_at_a_time = false
@@ -0,0 +1,5 @@
1
+ [bindings]
2
+ runtime = "claude-code"
3
+
4
+ [strength]
5
+ git_boundary = "block"
@@ -0,0 +1,7 @@
1
+ ---
2
+ status: accepted
3
+ ---
4
+ # A decision carrying a number nobody assigned
5
+
6
+ Status says accepted; the number is still DEC-PENDING, and everything
7
+ downstream will now cite it.
@@ -0,0 +1,6 @@
1
+ ---
2
+ status: superseded by 0099
3
+ ---
4
+ # 0002. Overtaken by something that does not exist
5
+
6
+ The cross-reference breaks quietly.
@@ -0,0 +1,6 @@
1
+ ---
2
+ status: accepted
3
+ ---
4
+ # 0007. The first one
5
+
6
+ Two threads were open and each read a different stale state.
@@ -0,0 +1,6 @@
1
+ ---
2
+ status: accepted
3
+ ---
4
+ # 0007. The second one
5
+
6
+ Both were confident. Nothing anywhere noticed.
@@ -0,0 +1,6 @@
1
+ ---
2
+ status: accepted
3
+ ---
4
+ # 0001. The first decision
5
+
6
+ Numbered from the directory.
@@ -0,0 +1,6 @@
1
+ ---
2
+ status: superseded by 0003
3
+ ---
4
+ # 0002. Overtaken later
5
+
6
+ Kept, with its reasoning, because that is the useful part.
@@ -0,0 +1,6 @@
1
+ ---
2
+ status: accepted
3
+ ---
4
+ # 0003. What replaced it
5
+
6
+ The one that holds now.
@@ -0,0 +1,3 @@
1
+ # Decisions
2
+
3
+ A project that has recorded none yet.
@@ -0,0 +1,7 @@
1
+ # Index
2
+
3
+ This fixture exists to make the doc-links check fail.
4
+
5
+ A link written to a document somebody intended to write, and did not.
6
+
7
+ Start with [the first run](first-run.md).
@@ -0,0 +1,3 @@
1
+ # Architecture notes
2
+
3
+ The file the guide is looking for, under its new name.
@@ -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,5 @@
1
+ # Guide
2
+
3
+ Every link here points at something real.
4
+
5
+ See [the architecture notes](architecture-notes.md) for the boundaries.
@@ -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,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.