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.
- package/.agents/skills/ponytail/SKILL.md +111 -0
- package/.agents/skills/ponytail-audit/SKILL.md +37 -0
- package/.agents/skills/ponytail-debt/SKILL.md +41 -0
- package/.agents/skills/ponytail-gain/SKILL.md +47 -0
- package/.agents/skills/ponytail-help/SKILL.md +70 -0
- package/.agents/skills/ponytail-review/SKILL.md +52 -0
- package/.agents/skills/prompt-review/SKILL.md +60 -0
- package/.agents/skills/wtk/SKILL.md +64 -0
- package/.agents/skills/wtk/references/artifacts.md +82 -0
- package/.agents/skills/wtk/references/evidence.md +68 -0
- package/.agents/skills/wtk/references/git.md +75 -0
- package/.agents/skills/wtk/references/test-contract.md +76 -0
- package/.agents/skills/wtk/references/validation.md +127 -0
- package/.agents/skills/wtk-config/SKILL.md +102 -0
- package/.agents/skills/wtk-config/assets/agents/claude/deep-reviewer.md +40 -0
- package/.agents/skills/wtk-config/assets/agents/claude/designer.md +57 -0
- package/.agents/skills/wtk-config/assets/agents/claude/explorer.md +41 -0
- package/.agents/skills/wtk-config/assets/agents/claude/implementer.md +57 -0
- package/.agents/skills/wtk-config/assets/agents/claude/planner.md +68 -0
- package/.agents/skills/wtk-config/assets/agents/claude/verifier.md +80 -0
- package/.agents/skills/wtk-config/assets/agents/codex/deep-reviewer.toml +41 -0
- package/.agents/skills/wtk-config/assets/agents/codex/designer.toml +55 -0
- package/.agents/skills/wtk-config/assets/agents/codex/explorer.toml +35 -0
- package/.agents/skills/wtk-config/assets/agents/codex/implementer.toml +52 -0
- package/.agents/skills/wtk-config/assets/agents/codex/planner.toml +66 -0
- package/.agents/skills/wtk-config/assets/agents/codex/verifier.toml +78 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/deep-reviewer.md +38 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/designer.md +55 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/explorer.md +39 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/implementer.md +55 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/planner.md +66 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/verifier.md +79 -0
- package/.agents/skills/wtk-config/scripts/ad-index.py +123 -0
- package/.agents/skills/wtk-config/scripts/repository_intelligence.py +671 -0
- package/.agents/skills/wtk-config/scripts/workflow_config.py +972 -0
- package/.agents/skills/wtk-deep-review/SKILL.md +164 -0
- package/.agents/skills/wtk-deep-review/assets/PROMPT.md +56 -0
- package/.agents/skills/wtk-deep-review/assets/REVIEW_UI.html +1340 -0
- package/.agents/skills/wtk-deep-review/assets/findings.schema.json +113 -0
- package/.agents/skills/wtk-deep-review/references/context-pack.md +88 -0
- package/.agents/skills/wtk-deep-review/references/orchestration.md +164 -0
- package/.agents/skills/wtk-deep-review/references/output-contracts.md +149 -0
- package/.agents/skills/wtk-deep-review/references/publish-github.md +83 -0
- package/.agents/skills/wtk-deep-review/references/state-and-learnings.md +64 -0
- package/.agents/skills/wtk-deep-review/references/subagent-runtimes.md +33 -0
- package/.agents/skills/wtk-deep-review/references/taxonomy.md +70 -0
- package/.agents/skills/wtk-deep-review/scripts/_common.py +371 -0
- package/.agents/skills/wtk-deep-review/scripts/build_jobs.py +561 -0
- package/.agents/skills/wtk-deep-review/scripts/build_knowledge.py +349 -0
- package/.agents/skills/wtk-deep-review/scripts/build_manifest.py +479 -0
- package/.agents/skills/wtk-deep-review/scripts/graft_context.py +160 -0
- package/.agents/skills/wtk-deep-review/scripts/graphify_context.py +75 -0
- package/.agents/skills/wtk-deep-review/scripts/merge_findings.py +313 -0
- package/.agents/skills/wtk-deep-review/scripts/render_html.py +240 -0
- package/.agents/skills/wtk-deep-review/scripts/render_review.py +282 -0
- package/.agents/skills/wtk-deep-review/scripts/run_jobs.py +436 -0
- package/.agents/skills/wtk-deep-review/scripts/token_metrics.py +369 -0
- package/.agents/skills/wtk-discover/SKILL.md +270 -0
- package/.agents/skills/wtk-discover/references/document-format.md +149 -0
- package/.agents/skills/wtk-implement/SKILL.md +95 -0
- package/.agents/skills/wtk-implement/references/checklist-format.md +116 -0
- package/.agents/skills/wtk-implement/references/screens.md +129 -0
- package/.agents/skills/wtk-implement/references/test-policy.md +152 -0
- package/.agents/skills/wtk-implement/references/verify.md +297 -0
- package/.agents/skills/wtk-knowledge-check/SKILL.md +16 -0
- package/.agents/skills/wtk-knowledge-check/scripts/check.ts +502 -0
- package/.agents/skills/wtk-knowledge-check/scripts/cli.ts +32 -0
- package/.agents/skills/wtk-knowledge-check/scripts/frontmatter.ts +44 -0
- package/.agents/skills/wtk-lean/.skill-meta.json +6 -0
- package/.agents/skills/wtk-lean/NOTICE.md +18 -0
- package/.agents/skills/wtk-lean/SKILL.md +137 -0
- package/.agents/skills/wtk-lean/references/build.md +168 -0
- package/.agents/skills/wtk-lean/references/checks.md +253 -0
- package/.agents/skills/wtk-lean/references/memory.md +156 -0
- package/.agents/skills/wtk-lean/references/plan.md +514 -0
- package/.agents/skills/wtk-lean/references/verify.md +336 -0
- package/.agents/skills/wtk-lean/scripts/check_commit.py +121 -0
- package/.agents/skills/wtk-lean/scripts/fixtures/checks.md +98 -0
- package/.agents/skills/wtk-lean/scripts/fixtures/plan.md +119 -0
- package/.agents/skills/wtk-lean/scripts/fixtures/verification.md +41 -0
- package/.agents/skills/wtk-lean/scripts/lessons.py +412 -0
- package/.agents/skills/wtk-lean/scripts/selftest.py +401 -0
- package/.agents/skills/wtk-lean/scripts/validate_checks.py +417 -0
- package/.agents/skills/wtk-lean/scripts/validate_plan.py +580 -0
- package/.agents/skills/wtk-lean/scripts/validate_verification.py +357 -0
- package/.agents/skills/wtk-plan/SKILL.md +102 -0
- package/.agents/skills/wtk-plan/references/document-format.md +222 -0
- package/.agents/skills/wtk-qa/SKILL.md +14 -0
- package/.agents/skills/wtk-qa-execute/SKILL.md +115 -0
- package/.agents/skills/wtk-qa-execute/references/fix-loop.md +23 -0
- package/.agents/skills/wtk-qa-execute/references/session-protocol.md +25 -0
- package/.agents/skills/wtk-qa-plan/SKILL.md +111 -0
- package/.agents/skills/wtk-qa-plan/references/profile.md +35 -0
- package/.agents/skills/wtk-ship/SKILL.md +49 -0
- package/.agents/skills/wtk-ship/remediation.py +170 -0
- package/.agents/skills/wtk-ship/scripts/close_feature.py +83 -0
- package/.agents/skills/wtk-ship/scripts/review_convergence.py +373 -0
- package/.wtk.toml.example +93 -0
- package/AGENTS.md +129 -0
- package/NOTICE.md +12 -0
- package/README.md +440 -0
- package/bin/wtk.js +25 -0
- package/docs/toolkit/README.md +56 -0
- package/docs/toolkit/decisions.md +61 -0
- package/docs/toolkit/guidelines/CONTEXT-BUDGET.md +64 -0
- package/docs/toolkit/guidelines/DX.md +67 -0
- package/docs/toolkit/guidelines/FRONTEND.md +73 -0
- package/docs/toolkit/guidelines/KNOWLEDGE-WIKI.md +128 -0
- package/docs/toolkit/guidelines/MODELING.md +134 -0
- package/docs/toolkit/guidelines/QA-EXECUTION.md +46 -0
- package/docs/toolkit/guidelines/QA-SCENARIOS.md +165 -0
- package/docs/toolkit/guidelines/REVIEW-ROUNDS.md +160 -0
- package/docs/toolkit/guidelines/SECURITY.md +160 -0
- package/docs/toolkit/guidelines/UI-UX.md +117 -0
- package/docs/toolkit/guidelines/WORKFLOW-MEMORY.md +73 -0
- package/docs/toolkit/guidelines.md +50 -0
- package/docs/toolkit/loop.md +77 -0
- package/docs/toolkit/purpose.md +51 -0
- package/docs/toolkit/repository-intelligence.md +53 -0
- package/docs/toolkit/reviews.md +77 -0
- package/knowledge/AGENTS.md +316 -0
- package/knowledge/raw/README.md +17 -0
- package/package.json +74 -0
- package/scripts/install_security_skills.py +816 -0
- package/scripts/installer/engine.js +254 -0
- package/scripts/installer/knowledge.js +28 -0
- package/scripts/installer/packets.js +77 -0
- package/scripts/installer/terminal.js +98 -0
- package/scripts/installer/transaction.js +75 -0
- package/skills-lock.json +92 -0
- package/templates/adoption/agents/core.md +23 -0
- package/templates/adoption/agents/quality.md +3 -0
- package/templates/adoption/knowledge/wiki/architecture/index.md +3 -0
- package/templates/adoption/knowledge/wiki/decisions/index.md +3 -0
- package/templates/adoption/knowledge/wiki/design/index.md +3 -0
- package/templates/adoption/knowledge/wiki/domain/index.md +3 -0
- package/templates/adoption/knowledge/wiki/index.md +3 -0
- package/templates/adoption/knowledge/wiki/log.md +3 -0
- package/templates/adoption/knowledge/wiki/open-questions/index.md +3 -0
- package/templates/adoption/knowledge/wiki/product/index.md +3 -0
- package/templates/adoption/knowledge/wiki/research/index.md +3 -0
- 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())
|