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,357 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
validate_verification.py - deterministic completion gate for a feature.
|
|
4
|
+
|
|
5
|
+
The skill's strongest invariant is "a feature is done when an independent Verifier's
|
|
6
|
+
report accounts for every check." That is prose the model has to remember, and prose
|
|
7
|
+
is what a long trajectory quietly drops. This turns it into a checkable pass/fail run
|
|
8
|
+
as the closing step, so declaring a feature done without a real report fails loudly.
|
|
9
|
+
|
|
10
|
+
It does NOT merely check that verification.md exists. A report that exists but holds
|
|
11
|
+
the template placeholder, cites no evidence, or records a surviving mutant next to a
|
|
12
|
+
PASS verdict would sail through a shallow existence check while proving nothing. The
|
|
13
|
+
gate reads the report's own tables and refuses a verdict its rows contradict.
|
|
14
|
+
|
|
15
|
+
Pure standard library, zero dependencies. Operates only on the .specs/ markdown
|
|
16
|
+
artifacts, so it stays stack-agnostic.
|
|
17
|
+
|
|
18
|
+
What it checks:
|
|
19
|
+
ERROR - no verification.md (Verify is the closing step of the flow, not an option)
|
|
20
|
+
ERROR - the verdict is FAIL, missing, or still the '[PASS | FAIL]' placeholder
|
|
21
|
+
ERROR - PASS with no file:line evidence anywhere (evidence-or-zero)
|
|
22
|
+
ERROR - profile standard/ui with no fault rows, or with no recomputed Coverage section
|
|
23
|
+
ERROR - PASS while a fault row says the mutant survived
|
|
24
|
+
ERROR - PASS while a Coverage row leaves a member Unproven
|
|
25
|
+
ERROR - PASS while a binding source leaves an element Uncovered
|
|
26
|
+
ERROR - PASS while a check row's Result is not PASS
|
|
27
|
+
ERROR - PASS while a Test policy row's expectation is not met
|
|
28
|
+
ERROR - the report's profile does not match the one checks.md was approved under
|
|
29
|
+
ERROR - profile ui, but the report carries no binding-sources section (step 1 vanished)
|
|
30
|
+
ERROR - profile standard/ui with Test policy rows in checks.md, but no verdicts in the report
|
|
31
|
+
WARN - the report says self-verified (author == verifier: degraded gate)
|
|
32
|
+
WARN - no `Profile:` line, so a skipped step cannot be told from a forgotten one
|
|
33
|
+
WARN - no `Round:` line
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
python3 <skill-dir>/scripts/validate_verification.py [feature] [--root DIR] [--strict]
|
|
37
|
+
|
|
38
|
+
Invoke from the skill directory that ships this script (not the project root).
|
|
39
|
+
Pass --root when cwd is not the project that contains .specs/.
|
|
40
|
+
|
|
41
|
+
Exit codes: 0 ok, 1 the feature is not done, 2 usage error.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
import argparse
|
|
45
|
+
import os
|
|
46
|
+
import re
|
|
47
|
+
import sys
|
|
48
|
+
|
|
49
|
+
EVIDENCE_RE = re.compile(r"[\w./-]+\.[A-Za-z0-9]+:\d+")
|
|
50
|
+
PROFILE_RE = re.compile(r"^\**Profile\**\s*:\s*`?(\w+)`?", re.IGNORECASE | re.MULTILINE)
|
|
51
|
+
EMPTY_CELL = {"", "-", "—", "–", "none", "n/a", "na", "nothing"}
|
|
52
|
+
SURVIVED_RE = re.compile(r"\b(no|survived|alive|not killed)\b", re.IGNORECASE)
|
|
53
|
+
UNMET_RE = re.compile(r"\b(no|not met|unmet|fail|failed|gap)\b", re.IGNORECASE)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _feature_dirs(root):
|
|
57
|
+
base = os.path.join(root, ".specs", "features")
|
|
58
|
+
if not os.path.isdir(base):
|
|
59
|
+
return base, []
|
|
60
|
+
return base, [d for d in sorted(os.listdir(base)) if os.path.isdir(os.path.join(base, d))]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def split_row(line):
|
|
64
|
+
return [c.strip() for c in line.strip().strip("|").split("|")]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_separator(line):
|
|
68
|
+
return bool(re.match(r"^\s*\|?[\s:|-]+\|?\s*$", line)) and "-" in line
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def strip_fences(text):
|
|
72
|
+
"""Drop fenced blocks: a report that pasted the template must not be read as data."""
|
|
73
|
+
out, in_fence = [], False
|
|
74
|
+
for ln in text.splitlines():
|
|
75
|
+
if ln.strip().startswith("```"):
|
|
76
|
+
in_fence = not in_fence
|
|
77
|
+
continue
|
|
78
|
+
if not in_fence:
|
|
79
|
+
out.append(ln)
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def find_tables(lines):
|
|
84
|
+
"""Return [(header_cells_lowercased, [data_row_cells, ...]), ...] for every table."""
|
|
85
|
+
tables, buf = [], []
|
|
86
|
+
for ln in lines + [""]:
|
|
87
|
+
stripped = ln.strip()
|
|
88
|
+
if stripped.startswith("|"):
|
|
89
|
+
buf.append(stripped)
|
|
90
|
+
continue
|
|
91
|
+
if buf:
|
|
92
|
+
rows = [split_row(r) for r in buf if not is_separator(r)]
|
|
93
|
+
if len(rows) >= 2:
|
|
94
|
+
tables.append(([c.lower() for c in rows[0]], rows[1:]))
|
|
95
|
+
buf = []
|
|
96
|
+
return tables
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def column(header, *names):
|
|
100
|
+
for i, cell in enumerate(header):
|
|
101
|
+
for n in names:
|
|
102
|
+
if n in cell:
|
|
103
|
+
return i
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def has_heading(lines, *names):
|
|
108
|
+
for ln in lines:
|
|
109
|
+
s = ln.strip()
|
|
110
|
+
for n in names:
|
|
111
|
+
if re.match(r"^#{1,4}\s+" + re.escape(n) + r"\b", s, re.IGNORECASE):
|
|
112
|
+
return True
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _profile(text):
|
|
117
|
+
m = PROFILE_RE.search(text)
|
|
118
|
+
return m.group(1).lower() if m else None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _checks_context(fdir):
|
|
122
|
+
"""What checks.md was approved under: (profile, has a Test policy section)."""
|
|
123
|
+
path = os.path.join(fdir, "checks.md")
|
|
124
|
+
if not os.path.exists(path):
|
|
125
|
+
return None, False
|
|
126
|
+
body = open(path, encoding="utf-8", errors="replace").read()
|
|
127
|
+
lines = strip_fences(body)
|
|
128
|
+
return _profile("\n".join(lines)), has_heading(lines, "Test policy")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _verdict(lines):
|
|
132
|
+
"""Return 'pass', 'fail', 'unfilled', or None."""
|
|
133
|
+
candidates = [
|
|
134
|
+
ln for ln in lines
|
|
135
|
+
if re.search(r"^\**verdict\**\s*:", ln.strip(), re.IGNORECASE)
|
|
136
|
+
or re.search(r"^#{1,4}\s*verification\b", ln.strip(), re.IGNORECASE)
|
|
137
|
+
]
|
|
138
|
+
hay = " ".join(candidates) if candidates else "\n".join(lines)
|
|
139
|
+
has_pass = re.search(r"\bPASS\b", hay) is not None
|
|
140
|
+
has_fail = re.search(r"\bFAIL\b", hay) is not None
|
|
141
|
+
if has_pass and has_fail:
|
|
142
|
+
return "unfilled"
|
|
143
|
+
if has_pass:
|
|
144
|
+
return "pass"
|
|
145
|
+
if has_fail:
|
|
146
|
+
return "fail"
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _check_feature(fdir, name):
|
|
151
|
+
errors, warnings = [], []
|
|
152
|
+
vpath = os.path.join(fdir, "verification.md")
|
|
153
|
+
if not os.path.exists(vpath):
|
|
154
|
+
errors.append(
|
|
155
|
+
f"{name}: no verification.md - the feature is not done until a fresh Verifier "
|
|
156
|
+
f"(author != verifier) writes it over <feature base>..HEAD with every check"
|
|
157
|
+
)
|
|
158
|
+
return errors, warnings
|
|
159
|
+
|
|
160
|
+
text = open(vpath, encoding="utf-8", errors="replace").read()
|
|
161
|
+
lines = strip_fences(text)
|
|
162
|
+
body = "\n".join(lines)
|
|
163
|
+
|
|
164
|
+
verdict = _verdict(lines)
|
|
165
|
+
if verdict is None:
|
|
166
|
+
errors.append(f"{name}: no PASS/FAIL verdict (a prose-only report does not count)")
|
|
167
|
+
elif verdict == "unfilled":
|
|
168
|
+
errors.append(f"{name}: verdict is still the template placeholder '[PASS | FAIL]'")
|
|
169
|
+
elif verdict == "fail":
|
|
170
|
+
errors.append(f"{name}: verdict is FAIL - route the ranked gaps back as fixes, then re-verify")
|
|
171
|
+
|
|
172
|
+
# The profile decides which steps run, so a report that quietly declares a cheaper one
|
|
173
|
+
# than the feature was approved under makes an entire step disappear without a trace.
|
|
174
|
+
report_profile = _profile(body)
|
|
175
|
+
approved_profile, policy_rows_exist = _checks_context(fdir)
|
|
176
|
+
if report_profile is None:
|
|
177
|
+
warnings.append(f"{name}: no `Profile:` line - a step that did not run is indistinguishable from one forgotten")
|
|
178
|
+
elif approved_profile and report_profile != approved_profile:
|
|
179
|
+
errors.append(
|
|
180
|
+
f"{name}: report says profile '{report_profile}' but checks.md was approved under "
|
|
181
|
+
f"'{approved_profile}' - the profile decides which steps run, so a mismatch silently drops one"
|
|
182
|
+
)
|
|
183
|
+
effective = approved_profile or report_profile
|
|
184
|
+
if not re.search(r"^\**round\**\s*:", body, re.IGNORECASE | re.MULTILINE):
|
|
185
|
+
warnings.append(f"{name}: no `Round:` line - a scoped re-verification must say what was carried forward")
|
|
186
|
+
if re.search(r"self[- ]verified", body, re.IGNORECASE):
|
|
187
|
+
warnings.append(
|
|
188
|
+
f"{name}: report is self-verified (author == verifier) - a degraded gate, "
|
|
189
|
+
f"since a self-check reproduces the author's own blind spot"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if verdict != "pass":
|
|
193
|
+
return errors, warnings
|
|
194
|
+
|
|
195
|
+
if not EVIDENCE_RE.search(body):
|
|
196
|
+
errors.append(f"{name}: PASS but cites no file:line evidence - evidence-or-zero not satisfied")
|
|
197
|
+
|
|
198
|
+
# A profile-scoped step that produced no section did not run. Requiring the section is what
|
|
199
|
+
# makes "skipped" distinguishable from "forgotten" - otherwise that is only a sentence.
|
|
200
|
+
if effective in ("standard", "ui") and not has_heading(lines, "Coverage"):
|
|
201
|
+
errors.append(
|
|
202
|
+
f"{name}: profile is {effective} but there is no `## Coverage` section - the join has to "
|
|
203
|
+
f"be recomputed from the authority over each set, not read back from the author's table"
|
|
204
|
+
)
|
|
205
|
+
if effective == "ui" and not has_heading(lines, "Binding sources", "Binding source"):
|
|
206
|
+
errors.append(
|
|
207
|
+
f"{name}: profile is ui but there is no `## Binding sources` section - step 1 is the "
|
|
208
|
+
f"only step that can catch a check contradicting the design, and no later step can"
|
|
209
|
+
)
|
|
210
|
+
if effective in ("standard", "ui") and policy_rows_exist and not has_heading(lines, "Test policy"):
|
|
211
|
+
errors.append(
|
|
212
|
+
f"{name}: checks.md carries Test policy rows but the report gives no verdict on them - "
|
|
213
|
+
f"those rows are the bar the author built under, so an unmet one is a finding"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
tables = find_tables(lines)
|
|
217
|
+
saw_faults = False
|
|
218
|
+
|
|
219
|
+
for header, rows in tables:
|
|
220
|
+
killed = column(header, "killed")
|
|
221
|
+
if killed is not None:
|
|
222
|
+
saw_faults = True
|
|
223
|
+
for r in rows:
|
|
224
|
+
if killed >= len(r):
|
|
225
|
+
continue
|
|
226
|
+
cell = r[killed]
|
|
227
|
+
if cell.lower() in EMPTY_CELL and cell.lower() != "no":
|
|
228
|
+
continue
|
|
229
|
+
if SURVIVED_RE.match(cell) or "survived" in cell.lower():
|
|
230
|
+
errors.append(
|
|
231
|
+
f"{name}: PASS but a mutant survived ({r[0][:50]}) - the assertion would "
|
|
232
|
+
f"pass under a plausible wrong implementation"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
unproven = column(header, "unproven")
|
|
236
|
+
if unproven is not None:
|
|
237
|
+
for r in rows:
|
|
238
|
+
if unproven < len(r) and r[unproven].lower() not in EMPTY_CELL:
|
|
239
|
+
errors.append(f"{name}: PASS but Coverage leaves '{r[unproven][:50]}' unproven ({r[0][:40]})")
|
|
240
|
+
|
|
241
|
+
uncovered = column(header, "uncovered")
|
|
242
|
+
if uncovered is not None:
|
|
243
|
+
for r in rows:
|
|
244
|
+
if uncovered < len(r) and r[uncovered].lower() not in EMPTY_CELL:
|
|
245
|
+
errors.append(
|
|
246
|
+
f"{name}: PASS but a binding source leaves '{r[uncovered][:50]}' uncovered ({r[0][:40]})"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
result = column(header, "result")
|
|
250
|
+
if result is not None and column(header, "check", "claim") is not None:
|
|
251
|
+
for r in rows:
|
|
252
|
+
if result < len(r) and r[result] and "pass" not in r[result].lower():
|
|
253
|
+
errors.append(f"{name}: PASS but check {r[0][:20]} reports '{r[result][:30]}'")
|
|
254
|
+
|
|
255
|
+
met = column(header, "expectation met", "met")
|
|
256
|
+
if met is not None:
|
|
257
|
+
for r in rows:
|
|
258
|
+
if met < len(r) and r[met] and UNMET_RE.search(r[met]):
|
|
259
|
+
errors.append(
|
|
260
|
+
f"{name}: PASS but a Test policy row is unmet ({r[0][:40]}: '{r[met][:40]}') "
|
|
261
|
+
f"- the rows priced work the checks do not name"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Fault injection is profile-scoped (standard, ui), like tlc-implement. Under `light` a
|
|
265
|
+
# report with no fault rows is correct, and the profile line is what says so.
|
|
266
|
+
if effective in ("standard", "ui") and not saw_faults:
|
|
267
|
+
errors.append(
|
|
268
|
+
f"{name}: profile is {effective} but there are no fault rows - a green suite proves the "
|
|
269
|
+
f"tests execute, only a killed mutant proves they can fail"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return errors, warnings
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _appears_complete(fdir):
|
|
276
|
+
if os.path.exists(os.path.join(fdir, "verification.md")):
|
|
277
|
+
return True
|
|
278
|
+
checks = os.path.join(fdir, "checks.md")
|
|
279
|
+
if not os.path.exists(checks):
|
|
280
|
+
return False
|
|
281
|
+
body = open(checks, encoding="utf-8", errors="replace").read()
|
|
282
|
+
if not re.search(r"^\**\s*C\d+\s*\**\s*[-–—:]", body, re.MULTILINE):
|
|
283
|
+
return False
|
|
284
|
+
if re.search(r"^\s*-\s*\[\s\]", body, re.MULTILINE):
|
|
285
|
+
return False
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _resolve(root, feature):
|
|
290
|
+
base, dirs = _feature_dirs(root)
|
|
291
|
+
# An explicit argument is answered on its own terms: "that feature is not here" beats
|
|
292
|
+
# "there is no features directory", which is true but not what was asked.
|
|
293
|
+
if feature:
|
|
294
|
+
fdir = feature if os.path.isdir(feature) else os.path.join(base, feature)
|
|
295
|
+
if not os.path.isdir(fdir):
|
|
296
|
+
print(f"validate_verification: feature not found: {feature}", file=sys.stderr)
|
|
297
|
+
raise SystemExit(2)
|
|
298
|
+
return [(fdir, os.path.basename(fdir.rstrip("/")))]
|
|
299
|
+
if not os.path.isdir(base):
|
|
300
|
+
print(f"validate_verification: no {base} directory - nothing to check.")
|
|
301
|
+
return []
|
|
302
|
+
if len(dirs) == 1:
|
|
303
|
+
return [(os.path.join(base, dirs[0]), dirs[0])]
|
|
304
|
+
if not dirs:
|
|
305
|
+
print("validate_verification: no features under .specs/features/ - nothing to check.")
|
|
306
|
+
return []
|
|
307
|
+
picked = [(os.path.join(base, d), d) for d in dirs if _appears_complete(os.path.join(base, d))]
|
|
308
|
+
if not picked:
|
|
309
|
+
print("validate_verification: no completed feature detected (all in progress) - nothing to gate.")
|
|
310
|
+
return picked
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def main(argv=None):
|
|
314
|
+
p = argparse.ArgumentParser(
|
|
315
|
+
prog="validate_verification.py",
|
|
316
|
+
description="Completion gate: a done feature needs a real PASS report its own rows do not contradict.",
|
|
317
|
+
)
|
|
318
|
+
p.add_argument("feature", nargs="?", default=None, help="Feature dir or name")
|
|
319
|
+
p.add_argument("--root", default=".", help="Project root containing .specs/ (default: current dir)")
|
|
320
|
+
p.add_argument("--strict", action="store_true", help="Treat warnings as errors")
|
|
321
|
+
p.add_argument("--allow-empty", action="store_true",
|
|
322
|
+
help="Exit 0 when there is no completed feature to gate (repo-wide sweeps)")
|
|
323
|
+
args = p.parse_args(argv)
|
|
324
|
+
root = os.path.abspath(args.root)
|
|
325
|
+
|
|
326
|
+
targets = _resolve(root, args.feature)
|
|
327
|
+
if not targets and not args.allow_empty:
|
|
328
|
+
# Gating nothing is not a pass. This is the completion gate for a feature that was just
|
|
329
|
+
# built, so an empty run means the report could not be found - which reads as green to
|
|
330
|
+
# anything checking only the exit code. Exit 2 says "could not gate", like the other
|
|
331
|
+
# validators do, and leaves 0 to mean a report was read and held up.
|
|
332
|
+
print(
|
|
333
|
+
"validate_verification: gated nothing - this is NOT a pass. Point it at the feature "
|
|
334
|
+
"(validate_verification.py <feature> --root <project>), or pass --allow-empty if you "
|
|
335
|
+
"meant to sweep a repo that has no completed feature yet.",
|
|
336
|
+
file=sys.stderr,
|
|
337
|
+
)
|
|
338
|
+
return 2
|
|
339
|
+
|
|
340
|
+
all_errors, all_warnings = [], []
|
|
341
|
+
for fdir, name in targets:
|
|
342
|
+
e, w = _check_feature(fdir, name)
|
|
343
|
+
all_errors += e
|
|
344
|
+
all_warnings += w
|
|
345
|
+
|
|
346
|
+
for w in all_warnings:
|
|
347
|
+
print(f" WARN {w}")
|
|
348
|
+
for e in all_errors:
|
|
349
|
+
print(f" ERROR {e}")
|
|
350
|
+
checked = ", ".join(name for _, name in targets) or "(none)"
|
|
351
|
+
fail = all_errors or (all_warnings and args.strict)
|
|
352
|
+
print(f"\nvalidate_verification: {len(all_errors)} error(s), {len(all_warnings)} warning(s) across [{checked}]")
|
|
353
|
+
return 1 if fail else 0
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
if __name__ == "__main__":
|
|
357
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wtk-plan
|
|
3
|
+
description: 'Turn decided sources into modular tasks with observable criteria and explicit scope. Use when writing tasks from a PRD, design, RFC, or ticket; not discovery or implementation.'
|
|
4
|
+
license: CC-BY-4.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: Tech Leads Club - github.com/tech-leads-club
|
|
7
|
+
version: 0.2.0
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TLC Plan
|
|
11
|
+
|
|
12
|
+
Turn decided work into `.tasks/<name>.md` with observable criteria with concrete values, grounded
|
|
13
|
+
in the repository. A one-line ticket can be a decision; an unshaped wish belongs in discovery.
|
|
14
|
+
This skill plans modular work and does not implement it.
|
|
15
|
+
|
|
16
|
+
## Source and scope
|
|
17
|
+
|
|
18
|
+
Read the source and the code relevant to its claims. Resolve factual questions from existing code,
|
|
19
|
+
conventions and documentation. Correct factual source errors within the authorized scope; a conflict
|
|
20
|
+
with an approved product decision needs clarification, not a silent rewrite.
|
|
21
|
+
|
|
22
|
+
Each criterion must describe an observable outcome with a concrete field, status, bound or other
|
|
23
|
+
value and an explicit boundary. It must be possible to name an execution that settles the claim.
|
|
24
|
+
Separate behavior from distributional service targets such as p95 latency or uptime; record how the
|
|
25
|
+
target is measured under observability instead of letting a behavior test claim to prove it.
|
|
26
|
+
|
|
27
|
+
For a guarantee that something will not happen, identify the mechanism preventing it, either in
|
|
28
|
+
existing code or a `Decided` row. Check relevant failure paths: a retry after remote success and
|
|
29
|
+
local failure is different from a retry after nothing happened. An absent mechanism is a decision
|
|
30
|
+
gap, not a guaranteed outcome.
|
|
31
|
+
|
|
32
|
+
Keep new capabilities outside the task unless the user authorizes them. Preserve already-settled
|
|
33
|
+
intent and use the task as the decision record; linked sources retain the reasoning. If they later
|
|
34
|
+
diverge on a binding decision, resolve that conflict before building.
|
|
35
|
+
|
|
36
|
+
## Slice and task boundaries
|
|
37
|
+
|
|
38
|
+
A slice is one observable outcome, not a layer such as schema or endpoints. Enumerate the source's
|
|
39
|
+
slices, then default to one task for the whole source. Split only for a demonstrated order constraint,
|
|
40
|
+
an answer only another party can supply, or different team ownership.
|
|
41
|
+
|
|
42
|
+
Use project conventions or reachable issue-tracker evidence for task size; git history describes
|
|
43
|
+
pull-request size, not task size. For a large task, show its irreversible decisions and actual seams
|
|
44
|
+
so the user can choose a meaningful cut. Do not impose a fixed slice, file or agent count.
|
|
45
|
+
|
|
46
|
+
Ground what the change disturbs: existing terms, data and callers, plus choices that introduce a new
|
|
47
|
+
pattern. `Decided` records only hard-to-reverse choices, their literal shape and rejected alternative.
|
|
48
|
+
Reversible placement, helpers and class structure belong to the builder and the diff.
|
|
49
|
+
|
|
50
|
+
## Surface and requirement coverage
|
|
51
|
+
|
|
52
|
+
Walk only the surface kinds the source exposes. Record each applicable item's landing in
|
|
53
|
+
`## Observable`: an existing criterion, `existing - <what>`, `n/a - <reason>`, or `Unresolved <n>`.
|
|
54
|
+
Use `None - no user-facing surface` when appropriate. A missing decision becomes a question; the
|
|
55
|
+
walk never creates a criterion just to fill a row.
|
|
56
|
+
|
|
57
|
+
| Surface | Decisions to account for |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| Screen/view | Empty, loading, error and unauthorized states; density, ordering and destructive confirmation |
|
|
60
|
+
| API/webhook | Response and error shapes/codes, caller authority, versioning and rate-limit behavior |
|
|
61
|
+
| Command/job | Output, verbosity, flags/defaults, exit codes and partial failure |
|
|
62
|
+
| Document/copy | Structure, tone, depth and the reader's next action |
|
|
63
|
+
| Organized collection | Grouping, naming, ordering, duplicates and exceptions |
|
|
64
|
+
|
|
65
|
+
Record the landing for each of the nine implicit dimensions in `Swept`: validation, failure modes,
|
|
66
|
+
idempotency and retry, authorization, concurrency and ordering, data lifecycle, external-dependency
|
|
67
|
+
failure, state transitions, and observability. Each lands on an existing criterion, existing behavior,
|
|
68
|
+
`n/a` with a reason, or `Unresolved`; these dimensions do not authorize extra scope.
|
|
69
|
+
|
|
70
|
+
A cited criterion must observe that dimension. For example, rejecting an existing duplicate does
|
|
71
|
+
not prove concurrent writes are safe, and event deduplication does not prove ordering. Keep the
|
|
72
|
+
landings explicit so `wtk-implement` can derive proofs from them without repeating the discovery.
|
|
73
|
+
|
|
74
|
+
## Decisions and unresolved questions
|
|
75
|
+
|
|
76
|
+
Resolve routine facts and reversible choices from the repository and existing authorization. Ask
|
|
77
|
+
only when a material choice belongs to the user and remains unsettled; continue independent work
|
|
78
|
+
while waiting. Give concrete options and a recommendation when useful, without manufacturing a
|
|
79
|
+
quota of questions or requiring a particular answer format.
|
|
80
|
+
|
|
81
|
+
Use safe defaults for non-blocking ambiguity, marking `Kind: open` and the default in `Until answered`.
|
|
82
|
+
Mark missing decisions that prevent a satisfiable criterion as `blocks`; prerequisites
|
|
83
|
+
that only prevent real-world activation use `blocks go-live`. Preserve the output contract's literal
|
|
84
|
+
fields and values. A proposed default is not approval; explicit delegation such as “you decide” is,
|
|
85
|
+
and is recorded as `user delegated` on the Sources line.
|
|
86
|
+
|
|
87
|
+
## Deliverable
|
|
88
|
+
|
|
89
|
+
When writing the task, read [document-format.md](references/document-format.md). Keep its literal
|
|
90
|
+
headings and required fields, including `Observable`, `Swept`, `Decided` and `Unresolved`.
|
|
91
|
+
Use existing source material to complete the artifact without asking the user to repeat it.
|
|
92
|
+
|
|
93
|
+
Present the task or justified split, followed by unresolved decisions with blockers first. Complete
|
|
94
|
+
the requested planning artifact and its applicable checks; do not begin implementation unless it
|
|
95
|
+
was also authorized. A source with unresolved product intent remains provisional.
|
|
96
|
+
|
|
97
|
+
Tasks are verification units; pull requests are review units. Preparation may belong in a commit
|
|
98
|
+
or PR without being its own task. If review needs smaller PRs, propose that separately using the
|
|
99
|
+
project's review conventions; preserve order constraints needed for safe intermediate states.
|
|
100
|
+
|
|
101
|
+
For library-specific facts not established locally, use the project's documentation tools and
|
|
102
|
+
current official documentation. Record uncertainty rather than inventing an API or behavior.
|