workflow-toolkit 1.0.1

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 (142) hide show
  1. package/.agents/skills/ponytail/SKILL.md +111 -0
  2. package/.agents/skills/ponytail-audit/SKILL.md +37 -0
  3. package/.agents/skills/ponytail-debt/SKILL.md +41 -0
  4. package/.agents/skills/ponytail-gain/SKILL.md +47 -0
  5. package/.agents/skills/ponytail-help/SKILL.md +70 -0
  6. package/.agents/skills/ponytail-review/SKILL.md +52 -0
  7. package/.agents/skills/prompt-review/SKILL.md +60 -0
  8. package/.agents/skills/wtk/SKILL.md +64 -0
  9. package/.agents/skills/wtk/references/artifacts.md +82 -0
  10. package/.agents/skills/wtk/references/evidence.md +68 -0
  11. package/.agents/skills/wtk/references/git.md +75 -0
  12. package/.agents/skills/wtk/references/test-contract.md +76 -0
  13. package/.agents/skills/wtk/references/validation.md +127 -0
  14. package/.agents/skills/wtk-config/SKILL.md +102 -0
  15. package/.agents/skills/wtk-config/assets/agents/claude/deep-reviewer.md +40 -0
  16. package/.agents/skills/wtk-config/assets/agents/claude/designer.md +57 -0
  17. package/.agents/skills/wtk-config/assets/agents/claude/explorer.md +41 -0
  18. package/.agents/skills/wtk-config/assets/agents/claude/implementer.md +57 -0
  19. package/.agents/skills/wtk-config/assets/agents/claude/planner.md +68 -0
  20. package/.agents/skills/wtk-config/assets/agents/claude/verifier.md +80 -0
  21. package/.agents/skills/wtk-config/assets/agents/codex/deep-reviewer.toml +41 -0
  22. package/.agents/skills/wtk-config/assets/agents/codex/designer.toml +55 -0
  23. package/.agents/skills/wtk-config/assets/agents/codex/explorer.toml +35 -0
  24. package/.agents/skills/wtk-config/assets/agents/codex/implementer.toml +52 -0
  25. package/.agents/skills/wtk-config/assets/agents/codex/planner.toml +66 -0
  26. package/.agents/skills/wtk-config/assets/agents/codex/verifier.toml +78 -0
  27. package/.agents/skills/wtk-config/assets/agents/cursor/deep-reviewer.md +38 -0
  28. package/.agents/skills/wtk-config/assets/agents/cursor/designer.md +55 -0
  29. package/.agents/skills/wtk-config/assets/agents/cursor/explorer.md +39 -0
  30. package/.agents/skills/wtk-config/assets/agents/cursor/implementer.md +55 -0
  31. package/.agents/skills/wtk-config/assets/agents/cursor/planner.md +66 -0
  32. package/.agents/skills/wtk-config/assets/agents/cursor/verifier.md +79 -0
  33. package/.agents/skills/wtk-config/scripts/ad-index.py +123 -0
  34. package/.agents/skills/wtk-config/scripts/repository_intelligence.py +671 -0
  35. package/.agents/skills/wtk-config/scripts/workflow_config.py +972 -0
  36. package/.agents/skills/wtk-deep-review/SKILL.md +164 -0
  37. package/.agents/skills/wtk-deep-review/assets/PROMPT.md +56 -0
  38. package/.agents/skills/wtk-deep-review/assets/REVIEW_UI.html +1340 -0
  39. package/.agents/skills/wtk-deep-review/assets/findings.schema.json +113 -0
  40. package/.agents/skills/wtk-deep-review/references/context-pack.md +88 -0
  41. package/.agents/skills/wtk-deep-review/references/orchestration.md +164 -0
  42. package/.agents/skills/wtk-deep-review/references/output-contracts.md +149 -0
  43. package/.agents/skills/wtk-deep-review/references/publish-github.md +83 -0
  44. package/.agents/skills/wtk-deep-review/references/state-and-learnings.md +64 -0
  45. package/.agents/skills/wtk-deep-review/references/subagent-runtimes.md +33 -0
  46. package/.agents/skills/wtk-deep-review/references/taxonomy.md +70 -0
  47. package/.agents/skills/wtk-deep-review/scripts/_common.py +371 -0
  48. package/.agents/skills/wtk-deep-review/scripts/build_jobs.py +561 -0
  49. package/.agents/skills/wtk-deep-review/scripts/build_knowledge.py +349 -0
  50. package/.agents/skills/wtk-deep-review/scripts/build_manifest.py +479 -0
  51. package/.agents/skills/wtk-deep-review/scripts/graft_context.py +160 -0
  52. package/.agents/skills/wtk-deep-review/scripts/graphify_context.py +75 -0
  53. package/.agents/skills/wtk-deep-review/scripts/merge_findings.py +313 -0
  54. package/.agents/skills/wtk-deep-review/scripts/render_html.py +240 -0
  55. package/.agents/skills/wtk-deep-review/scripts/render_review.py +282 -0
  56. package/.agents/skills/wtk-deep-review/scripts/run_jobs.py +436 -0
  57. package/.agents/skills/wtk-deep-review/scripts/token_metrics.py +369 -0
  58. package/.agents/skills/wtk-discover/SKILL.md +270 -0
  59. package/.agents/skills/wtk-discover/references/document-format.md +149 -0
  60. package/.agents/skills/wtk-implement/SKILL.md +95 -0
  61. package/.agents/skills/wtk-implement/references/checklist-format.md +116 -0
  62. package/.agents/skills/wtk-implement/references/screens.md +129 -0
  63. package/.agents/skills/wtk-implement/references/test-policy.md +152 -0
  64. package/.agents/skills/wtk-implement/references/verify.md +297 -0
  65. package/.agents/skills/wtk-knowledge-check/SKILL.md +16 -0
  66. package/.agents/skills/wtk-knowledge-check/scripts/check.ts +502 -0
  67. package/.agents/skills/wtk-knowledge-check/scripts/cli.ts +32 -0
  68. package/.agents/skills/wtk-knowledge-check/scripts/frontmatter.ts +44 -0
  69. package/.agents/skills/wtk-lean/.skill-meta.json +6 -0
  70. package/.agents/skills/wtk-lean/NOTICE.md +18 -0
  71. package/.agents/skills/wtk-lean/SKILL.md +137 -0
  72. package/.agents/skills/wtk-lean/references/build.md +168 -0
  73. package/.agents/skills/wtk-lean/references/checks.md +253 -0
  74. package/.agents/skills/wtk-lean/references/memory.md +156 -0
  75. package/.agents/skills/wtk-lean/references/plan.md +514 -0
  76. package/.agents/skills/wtk-lean/references/verify.md +336 -0
  77. package/.agents/skills/wtk-lean/scripts/check_commit.py +121 -0
  78. package/.agents/skills/wtk-lean/scripts/fixtures/checks.md +98 -0
  79. package/.agents/skills/wtk-lean/scripts/fixtures/plan.md +119 -0
  80. package/.agents/skills/wtk-lean/scripts/fixtures/verification.md +41 -0
  81. package/.agents/skills/wtk-lean/scripts/lessons.py +412 -0
  82. package/.agents/skills/wtk-lean/scripts/selftest.py +401 -0
  83. package/.agents/skills/wtk-lean/scripts/validate_checks.py +417 -0
  84. package/.agents/skills/wtk-lean/scripts/validate_plan.py +580 -0
  85. package/.agents/skills/wtk-lean/scripts/validate_verification.py +357 -0
  86. package/.agents/skills/wtk-plan/SKILL.md +102 -0
  87. package/.agents/skills/wtk-plan/references/document-format.md +222 -0
  88. package/.agents/skills/wtk-qa/SKILL.md +14 -0
  89. package/.agents/skills/wtk-qa-execute/SKILL.md +115 -0
  90. package/.agents/skills/wtk-qa-execute/references/fix-loop.md +23 -0
  91. package/.agents/skills/wtk-qa-execute/references/session-protocol.md +25 -0
  92. package/.agents/skills/wtk-qa-plan/SKILL.md +111 -0
  93. package/.agents/skills/wtk-qa-plan/references/profile.md +35 -0
  94. package/.agents/skills/wtk-ship/SKILL.md +49 -0
  95. package/.agents/skills/wtk-ship/remediation.py +170 -0
  96. package/.agents/skills/wtk-ship/scripts/close_feature.py +83 -0
  97. package/.agents/skills/wtk-ship/scripts/review_convergence.py +373 -0
  98. package/.wtk.toml.example +93 -0
  99. package/AGENTS.md +129 -0
  100. package/NOTICE.md +12 -0
  101. package/README.md +440 -0
  102. package/bin/wtk.js +25 -0
  103. package/docs/toolkit/README.md +56 -0
  104. package/docs/toolkit/decisions.md +61 -0
  105. package/docs/toolkit/guidelines/CONTEXT-BUDGET.md +64 -0
  106. package/docs/toolkit/guidelines/DX.md +67 -0
  107. package/docs/toolkit/guidelines/FRONTEND.md +73 -0
  108. package/docs/toolkit/guidelines/KNOWLEDGE-WIKI.md +128 -0
  109. package/docs/toolkit/guidelines/MODELING.md +134 -0
  110. package/docs/toolkit/guidelines/QA-EXECUTION.md +46 -0
  111. package/docs/toolkit/guidelines/QA-SCENARIOS.md +165 -0
  112. package/docs/toolkit/guidelines/REVIEW-ROUNDS.md +160 -0
  113. package/docs/toolkit/guidelines/SECURITY.md +160 -0
  114. package/docs/toolkit/guidelines/UI-UX.md +117 -0
  115. package/docs/toolkit/guidelines/WORKFLOW-MEMORY.md +73 -0
  116. package/docs/toolkit/guidelines.md +50 -0
  117. package/docs/toolkit/loop.md +77 -0
  118. package/docs/toolkit/purpose.md +51 -0
  119. package/docs/toolkit/repository-intelligence.md +53 -0
  120. package/docs/toolkit/reviews.md +77 -0
  121. package/knowledge/AGENTS.md +316 -0
  122. package/knowledge/raw/README.md +17 -0
  123. package/package.json +74 -0
  124. package/scripts/install_security_skills.py +816 -0
  125. package/scripts/installer/engine.js +254 -0
  126. package/scripts/installer/knowledge.js +28 -0
  127. package/scripts/installer/packets.js +77 -0
  128. package/scripts/installer/terminal.js +98 -0
  129. package/scripts/installer/transaction.js +75 -0
  130. package/skills-lock.json +92 -0
  131. package/templates/adoption/agents/core.md +23 -0
  132. package/templates/adoption/agents/quality.md +3 -0
  133. package/templates/adoption/knowledge/wiki/architecture/index.md +3 -0
  134. package/templates/adoption/knowledge/wiki/decisions/index.md +3 -0
  135. package/templates/adoption/knowledge/wiki/design/index.md +3 -0
  136. package/templates/adoption/knowledge/wiki/domain/index.md +3 -0
  137. package/templates/adoption/knowledge/wiki/index.md +3 -0
  138. package/templates/adoption/knowledge/wiki/log.md +3 -0
  139. package/templates/adoption/knowledge/wiki/open-questions/index.md +3 -0
  140. package/templates/adoption/knowledge/wiki/product/index.md +3 -0
  141. package/templates/adoption/knowledge/wiki/research/index.md +3 -0
  142. package/templates/adoption/product/AGENT-CONTEXT.md +25 -0
@@ -0,0 +1,417 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ validate_checks.py - deterministic gate for a feature checks.md, run before any code.
4
+
5
+ This is the omission catcher. The failure it exists to prevent is not a vague claim
6
+ (a human notices those) but a set member that was named in prose and never assigned a
7
+ proof: "dispatches over paused, updated, deleted and trial_will_end" reads perfectly
8
+ with one of the four missing, so nobody sees it, and that branch ships unproven.
9
+
10
+ The Coverage join makes that failure structural - every member is its own token beside
11
+ the check that proves it - and this script makes the join checkable: a row declaring a
12
+ set of 5 with 4 members assigned fails, loudly, before a line of code is written.
13
+
14
+ Pure standard library, zero dependencies. Operates only on the checks.md markdown
15
+ artifact - never on the target codebase - so it stays stack-agnostic.
16
+
17
+ The requirements and the solution's shape are not this file's business: both were
18
+ reviewed in plan.md and are gated by validate_plan.py. What this script owns is the
19
+ derivation from them, which is where omissions surface.
20
+
21
+ What it checks:
22
+ ERROR - no `Profile:` line, or an unknown profile
23
+ ERROR - a required section is missing (Checks, Coverage, Swept)
24
+ ERROR - a check with no `Proof:` line
25
+ ERROR - a duplicate check id
26
+ ERROR - no checks parsed at all
27
+ ERROR - a Coverage row with an empty member cell
28
+ ERROR - a Coverage row whose declared set size exceeds the members assigned
29
+ ERROR - a Coverage row with a non-empty `Unproven` cell
30
+ ERROR - a swept dimension missing, or its landing left blank
31
+ ERROR - an `n/a` swept landing with no reason after it
32
+ ERROR - a check id referenced in Coverage or Swept that is not defined
33
+ WARN - a route in plan.md's Surface that nothing here mentions
34
+ WARN - a proof that names no test selector (a whole suite settles no single claim)
35
+ WARN - a claim carrying a vague word instead of a concrete value
36
+ WARN - a Coverage row that declares no set size
37
+ WARN - a swept landing that is neither a check, `existing`, `n/a`, nor `Unresolved`
38
+ WARN - a blocking unresolved question still open
39
+ WARN - a `Test policy` section is absent under profile standard/ui
40
+
41
+ Usage:
42
+ python3 <skill-dir>/scripts/validate_checks.py [target] [--root DIR] [--strict]
43
+
44
+ Invoke from the skill directory that ships this script (not the project root).
45
+ target Path to a checks.md, a feature directory, a feature name, or a project root.
46
+ Omitted -> auto-detect the single feature under <root>/.specs/features/.
47
+ --root Project root that contains .specs/ (default: current dir).
48
+ --strict Treat warnings as errors.
49
+
50
+ Exit codes: 0 pass, 1 errors found (or warnings under --strict), 2 usage error.
51
+ """
52
+
53
+ import argparse
54
+ import os
55
+ import re
56
+ import sys
57
+
58
+ PROFILES = ("light", "standard", "ui")
59
+ REQUIRED_SECTIONS = ["Checks", "Coverage", "Swept"]
60
+
61
+ PROFILE_RE = re.compile(r"^\**Profile\**\s*:\s*`?(\w+)`?", re.IGNORECASE)
62
+ CHECK_RE = re.compile(r"^\**\s*(C\d+)\s*\**\s*[-–—:]\s*(.+)$")
63
+ PROOF_RE = re.compile(r"^\**\s*Proof\s*\**\s*:\s*(.+)$", re.IGNORECASE)
64
+ CID_RE = re.compile(r"\bC\d+\b")
65
+ PLACEHOLDER_RE = re.compile(r"^\s*[\[<].+[\]>]\s*$")
66
+ SIZE_RE = re.compile(r"\((\d+)\s*(?:[a-z\- ]+)?\)")
67
+ ROUTE_RE = re.compile(r"(/[A-Za-z0-9_\-/:{}.]*)")
68
+ TABLE_DRIVEN_RE = re.compile(r"table[- ]driven", re.IGNORECASE)
69
+
70
+ SELECTOR_TOKENS = (
71
+ "-n ", "-k ", "-t ", "-e ", "--name", "--only", "--example", "--filter",
72
+ "--testnamepattern", "--test-name", "--run", "-dtest=", "-run ", "::", "#",
73
+ "--grep", "-g ", "--spec",
74
+ )
75
+
76
+ VAGUE_RE = re.compile(
77
+ r"\b(gracefully|properly|correctly|quickly|fast|slow|efficiently|reasonably|"
78
+ r"appropriately|as expected|user-friendly|robust|works)\b",
79
+ re.IGNORECASE,
80
+ )
81
+
82
+ # canonical name -> alternative spellings accepted in the Swept list
83
+ DIMENSIONS = {
84
+ "validation": ("validation", "validation and bounds", "input validation"),
85
+ "failure modes": ("failure modes", "failure", "failure and partial failure", "partial failure"),
86
+ "idempotency": ("idempotency", "idempotency and retry", "idempotency, retry, duplicates", "retry"),
87
+ "authorization": ("authorization", "authorisation", "auth", "authorization and rate limits"),
88
+ "concurrency": ("concurrency", "concurrency and ordering", "ordering"),
89
+ "data lifecycle": ("data lifecycle", "lifecycle", "data retention"),
90
+ "dependency failure": (
91
+ "dependency failure", "external-dependency failure", "external dependency failure",
92
+ "external dependency",
93
+ ),
94
+ "state transitions": ("state transitions", "transitions", "state-transition integrity"),
95
+ "observability": ("observability", "logging and metrics", "telemetry"),
96
+ }
97
+
98
+
99
+ def resolve_checks(target, root):
100
+ if target:
101
+ if os.path.isfile(target):
102
+ return target
103
+ if os.path.isdir(target):
104
+ cand = os.path.join(target, "checks.md")
105
+ if os.path.isfile(cand):
106
+ return cand
107
+ return _autodetect(target)
108
+ cand = os.path.join(root, ".specs", "features", target, "checks.md")
109
+ if os.path.isfile(cand):
110
+ return cand
111
+ return None
112
+ return _autodetect(root)
113
+
114
+
115
+ def _autodetect(root):
116
+ base = os.path.join(root, ".specs", "features")
117
+ if not os.path.isdir(base):
118
+ return None
119
+ features = [
120
+ d for d in sorted(os.listdir(base))
121
+ if os.path.isfile(os.path.join(base, d, "checks.md"))
122
+ ]
123
+ if len(features) == 1:
124
+ return os.path.join(base, features[0], "checks.md")
125
+ if len(features) == 0:
126
+ return None
127
+ raise SystemExit(
128
+ "validate_checks: multiple features found; pass one explicitly:\n "
129
+ + "\n ".join(os.path.join(base, f, "checks.md") for f in features)
130
+ )
131
+
132
+
133
+ def split_row(line):
134
+ return [c.strip() for c in line.strip().strip("|").split("|")]
135
+
136
+
137
+ def is_separator(line):
138
+ return bool(re.match(r"^\s*\|?[\s:|-]+\|?\s*$", line)) and "-" in line
139
+
140
+
141
+ def section_bounds(lines, name):
142
+ pattern = re.compile(r"^#{1,4}\s+" + re.escape(name) + r"\b.*$", re.IGNORECASE)
143
+ start = None
144
+ for i, ln in enumerate(lines):
145
+ if pattern.match(ln.strip()):
146
+ start = i + 1
147
+ break
148
+ if start is None:
149
+ return None
150
+ end = len(lines)
151
+ for j in range(start, len(lines)):
152
+ if re.match(r"^#{1,2}\s+\S", lines[j]): # only a top-level heading closes it
153
+ end = j
154
+ break
155
+ return (start, end)
156
+
157
+
158
+ def table_rows(lines, bounds):
159
+ """Data rows of the first contiguous markdown table inside a section."""
160
+ if not bounds:
161
+ return []
162
+ rows, started = [], False
163
+ for i in range(*bounds):
164
+ stripped = lines[i].strip()
165
+ if stripped.startswith("|"):
166
+ started = True
167
+ rows.append(stripped)
168
+ elif started and stripped == "":
169
+ continue
170
+ elif started:
171
+ break
172
+ data = [r for r in rows if not is_separator(r)]
173
+ return data[1:] if data else []
174
+
175
+
176
+ def strip_fences(lines):
177
+ """Drop fenced code blocks so a template example inside the file is not parsed."""
178
+ out, in_fence = [], False
179
+ for ln in lines:
180
+ if ln.strip().startswith("```"):
181
+ in_fence = not in_fence
182
+ out.append("")
183
+ continue
184
+ out.append("" if in_fence else ln)
185
+ return out
186
+
187
+
188
+ def parse_checks(lines):
189
+ """Return (checks: dict id -> {'claim', 'proofs', 'line'}, duplicates: list)."""
190
+ checks, duplicates = {}, []
191
+ current = None
192
+ for i, ln in enumerate(lines, start=1):
193
+ stripped = ln.strip()
194
+ m = CHECK_RE.match(stripped)
195
+ if m and not stripped.startswith("|"):
196
+ cid = m.group(1).upper()
197
+ if cid in checks:
198
+ duplicates.append((cid, i))
199
+ current = None
200
+ continue
201
+ checks[cid] = {"claim": m.group(2).strip(), "proofs": [], "line": i}
202
+ current = cid
203
+ continue
204
+ if re.match(r"^#{1,2}\s+\S", ln):
205
+ current = None
206
+ if current:
207
+ pm = PROOF_RE.match(stripped)
208
+ if pm:
209
+ checks[current]["proofs"].append(pm.group(1).strip())
210
+ return checks, duplicates
211
+
212
+
213
+ def count_members(cell):
214
+ """Count member tokens that carry a check id. Members are separated by '·' or ';'."""
215
+ parts = re.split(r"[·;]", cell)
216
+ return sum(1 for p in parts if CID_RE.search(p))
217
+
218
+
219
+ def plan_routes(checks_path):
220
+ """Route paths named in the sibling plan.md's Surface table, if there is one."""
221
+ plan = os.path.join(os.path.dirname(os.path.abspath(checks_path)), "plan.md")
222
+ if not os.path.isfile(plan):
223
+ return []
224
+ with open(plan, "r", encoding="utf-8") as f:
225
+ lines = strip_fences(f.read().splitlines())
226
+ bounds = section_bounds(lines, "Surface")
227
+ routes = []
228
+ for r in table_rows(lines, bounds):
229
+ cells = split_row(r)
230
+ if not cells or PLACEHOLDER_RE.match(cells[0]):
231
+ continue
232
+ m = ROUTE_RE.search(cells[0])
233
+ if m:
234
+ routes.append(m.group(1))
235
+ return routes
236
+
237
+
238
+ def check_file(path):
239
+ with open(path, "r", encoding="utf-8") as f:
240
+ raw = f.read().splitlines()
241
+ lines = strip_fences(raw)
242
+ errors, warnings = [], []
243
+
244
+ # Profile.
245
+ profile = None
246
+ for ln in lines:
247
+ m = PROFILE_RE.match(ln.strip())
248
+ if m:
249
+ profile = m.group(1).lower()
250
+ break
251
+ if profile is None:
252
+ errors.append("no `Profile:` line - the verification report has to name the profile in force")
253
+ elif profile not in PROFILES:
254
+ errors.append(f"unknown profile '{profile}' (expected one of: {', '.join(PROFILES)})")
255
+
256
+ # Sections.
257
+ for name in REQUIRED_SECTIONS:
258
+ if section_bounds(lines, name) is None:
259
+ errors.append(f"missing required section: ## {name}")
260
+ if profile in ("standard", "ui") and section_bounds(lines, "Test policy") is None:
261
+ warnings.append(
262
+ f"profile is {profile} but there is no ## Test policy section - confirm the repo already "
263
+ "answers which level proves each layer and how deeply"
264
+ )
265
+
266
+ # Checks.
267
+ checks, duplicates = parse_checks(lines)
268
+ for cid, line_no in duplicates:
269
+ errors.append(f"L{line_no}: duplicate check id {cid} - ids are referenced downstream and must be unique")
270
+ if not checks:
271
+ errors.append("no checks parsed - expected lines shaped `**C1** - <claim>` followed by `Proof: <command>`")
272
+ return errors, warnings, profile
273
+
274
+ for cid, c in sorted(checks.items(), key=lambda kv: int(kv[0][1:])):
275
+ if not c["proofs"]:
276
+ errors.append(f"{cid} has no `Proof:` line - no proof, no check (L{c['line']})")
277
+ for pr in c["proofs"]:
278
+ low = pr.lower()
279
+ if not any(tok in low for tok in SELECTOR_TOKENS):
280
+ warnings.append(
281
+ f"{cid}: proof names no test selector, so it may be a whole suite - "
282
+ f"a suite going green settles no single claim: {pr[:60]}"
283
+ )
284
+ vague = VAGUE_RE.search(c["claim"])
285
+ if vague:
286
+ warnings.append(f"{cid}: claim uses '{vague.group(0)}' instead of a concrete value")
287
+
288
+ referenced = set()
289
+
290
+ # Coverage join.
291
+ cov = section_bounds(lines, "Coverage")
292
+ rows = table_rows(lines, cov)
293
+ if cov and not rows:
294
+ warnings.append("Coverage section has no rows - state 'no set rows' explicitly if nothing enumerates")
295
+ for r in rows:
296
+ cells = split_row(r)
297
+ if len(cells) < 2:
298
+ continue
299
+ set_cell, member_cell = cells[0], cells[1]
300
+ unproven = cells[2] if len(cells) > 2 else ""
301
+ if not set_cell or PLACEHOLDER_RE.match(set_cell):
302
+ continue
303
+ label = set_cell[:48]
304
+ if not member_cell or PLACEHOLDER_RE.match(member_cell) or member_cell == "-":
305
+ errors.append(f"Coverage '{label}': empty member cell - every member needs its own token and a check")
306
+ continue
307
+ referenced |= {c.upper() for c in CID_RE.findall(member_cell)}
308
+ sm = SIZE_RE.search(set_cell)
309
+ if not sm:
310
+ warnings.append(f"Coverage '{label}': declares no set size - write it as '(N)' so the join is checkable")
311
+ else:
312
+ declared = int(sm.group(1))
313
+ if TABLE_DRIVEN_RE.search(member_cell) and str(declared) in member_cell:
314
+ pass # the enumeration lives in the test
315
+ else:
316
+ assigned = count_members(member_cell)
317
+ if assigned < declared:
318
+ errors.append(
319
+ f"Coverage '{label}': declares {declared} members but only {assigned} carry a check "
320
+ f"- the unassigned member is the one that ships unproven"
321
+ )
322
+ if unproven and unproven not in ("-", "—") and not PLACEHOLDER_RE.match(unproven):
323
+ errors.append(f"Coverage '{label}': Unproven is '{unproven[:40]}' - a member with no proof is a gap")
324
+
325
+ # Swept.
326
+ swept = section_bounds(lines, "Swept")
327
+ if swept:
328
+ found = {}
329
+ for i in range(*swept):
330
+ m = re.match(r"^\s*[-*]\s*\**([^:*]+?)\**\s*:\s*(.*)$", lines[i])
331
+ if not m:
332
+ continue
333
+ key = m.group(1).strip().lower()
334
+ landing = m.group(2).strip()
335
+ for canon, aliases in DIMENSIONS.items():
336
+ if key in aliases:
337
+ found[canon] = (landing, i + 1)
338
+ break
339
+ for canon in DIMENSIONS:
340
+ if canon not in found:
341
+ errors.append(f"Swept: dimension '{canon}' has no line - all nine, every time")
342
+ continue
343
+ landing, line_no = found[canon]
344
+ if not landing or PLACEHOLDER_RE.match(landing):
345
+ errors.append(f"Swept '{canon}' (L{line_no}): landing is blank - a criterion, `existing`, or `n/a - <reason>`")
346
+ continue
347
+ low = landing.lower()
348
+ if CID_RE.search(landing):
349
+ referenced |= {c.upper() for c in CID_RE.findall(landing)}
350
+ elif low.startswith("n/a") or low.startswith("not in scope"):
351
+ rest = re.sub(r"^(n/a|not in scope)\b", "", low).strip(" -–—:")
352
+ if len(rest) < 3:
353
+ errors.append(f"Swept '{canon}' (L{line_no}): `n/a` with no reason - say why it does not apply")
354
+ elif low.startswith("existing"):
355
+ rest = landing[len("existing"):].strip(" -–—:")
356
+ if len(rest) < 3:
357
+ errors.append(f"Swept '{canon}' (L{line_no}): `existing` with no named guard or constraint")
358
+ elif low.startswith("unresolved") or low.startswith("open"):
359
+ warnings.append(f"Swept '{canon}' (L{line_no}): still Unresolved - confirm it does not block")
360
+ else:
361
+ warnings.append(
362
+ f"Swept '{canon}' (L{line_no}): landing '{landing[:40]}' is not a check, `existing`, "
363
+ f"`n/a`, or `Unresolved`"
364
+ )
365
+
366
+ # Dangling references.
367
+ for cid in sorted(referenced - set(checks), key=lambda c: int(c[1:])):
368
+ errors.append(f"{cid} is referenced in Coverage or Swept but is not defined in ## Checks")
369
+
370
+ # The derivation. checks.md is derived from plan.md, so a route reviewed there with
371
+ # nothing pointing back at it here is either dead or unproven - and the plan naming it
372
+ # is what makes it look covered.
373
+ for route in plan_routes(path):
374
+ if route not in "\n".join(lines):
375
+ warnings.append(
376
+ f"plan.md's Surface names `{route}` but nothing here mentions it - its statuses "
377
+ f"owe a Coverage row, or the route is dead"
378
+ )
379
+
380
+ # Blocking questions.
381
+ for i, ln in enumerate(lines, start=1):
382
+ if ln.strip().startswith("|") and re.search(r"\|\s*blocks\s*\|", ln, re.IGNORECASE):
383
+ warnings.append(f"L{i}: an unresolved question is marked `blocks` - it cannot be settled while building")
384
+
385
+ return errors, warnings, profile
386
+
387
+
388
+ def main(argv=None):
389
+ p = argparse.ArgumentParser(
390
+ prog="validate_checks.py",
391
+ description="Pre-build gate for a feature checks.md: proofs, the coverage join, the sweep.",
392
+ )
393
+ p.add_argument("target", nargs="?", default=None)
394
+ p.add_argument("--root", default=".")
395
+ p.add_argument("--strict", action="store_true")
396
+ args = p.parse_args(argv)
397
+
398
+ path = resolve_checks(args.target, args.root)
399
+ if not path:
400
+ print(
401
+ "validate_checks: could not locate a checks.md. Pass a path or run from the project root.",
402
+ file=sys.stderr,
403
+ )
404
+ return 2
405
+
406
+ errors, warnings, profile = check_file(path)
407
+ for w in warnings:
408
+ print(f" WARN {w}")
409
+ for e in errors:
410
+ print(f" ERROR {e}")
411
+ fail = errors or (warnings and args.strict)
412
+ print(f"\nvalidate_checks: {len(errors)} error(s), {len(warnings)} warning(s) in {path} [profile: {profile or 'none'}]")
413
+ return 1 if fail else 0
414
+
415
+
416
+ if __name__ == "__main__":
417
+ raise SystemExit(main())