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,401 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ selftest.py - fault injection against this skill's own gates, plus a smoke run of its tooling.
4
+
5
+ The gates in validate_plan.py, validate_checks.py and validate_verification.py are only
6
+ worth their invocation if they FAIL when the artifact is wrong. A validator that exits 0
7
+ on everything is decoration, and nothing about running it would reveal that. So the same
8
+ discipline the skill demands of a feature applies to the skill: mutate the artifact, run
9
+ the gate, and require the mutant to be killed.
10
+
11
+ Run this after editing any validator, and after editing the templates in references/ -
12
+ a template change that drifts from what a validator parses shows up here as a broken
13
+ baseline rather than as a silent pass in six months.
14
+
15
+ Fixtures live in scripts/fixtures/ as a complete, filled-in worked example (a real feature
16
+ shape, not placeholders). They double as the reference for what a passing artifact looks like.
17
+
18
+ Usage:
19
+ python3 <skill-dir>/scripts/selftest.py [-v]
20
+
21
+ Exit codes: 0 all mutants killed and the baseline is clean, 1 otherwise.
22
+ """
23
+
24
+ import os
25
+ import re
26
+ import shutil
27
+ import subprocess
28
+ import sys
29
+ import tempfile
30
+
31
+ HERE = os.path.dirname(os.path.abspath(__file__))
32
+ FIXTURES = os.path.join(HERE, "fixtures")
33
+ FEATURE = "billing"
34
+
35
+ # (validator, artifact, description, mutation, expected exit, expected message fragment)
36
+ CASES = [
37
+ ("validate_checks", "checks.md", "drop one member from a size-5 set",
38
+ lambda s: s.replace(" · `trial_will_end` C10 | - |", " | - |", 1),
39
+ 1, "declares 5 members but only 4"),
40
+ ("validate_checks", "checks.md", "remove a check's Proof line",
41
+ lambda s: re.sub(r"\nProof: `bin/rails test test/billing/access_test\.rb[^\n]*\n", "\n", s),
42
+ 1, "C4 has no `Proof:` line"),
43
+ ("validate_checks", "checks.md", "delete the concurrency sweep line",
44
+ lambda s: s.replace("- concurrency: C7\n", ""),
45
+ 1, "dimension 'concurrency' has no line"),
46
+ ("validate_checks", "checks.md", "bare n/a with no reason",
47
+ lambda s: re.sub(r"- data lifecycle: n/a[^\n]*", "- data lifecycle: n/a", s),
48
+ 1, "`n/a` with no reason"),
49
+ ("validate_checks", "checks.md", "remove the Profile line",
50
+ lambda s: s.replace("Profile: standard\n", ""),
51
+ 1, "no `Profile:` line"),
52
+ ("validate_checks", "checks.md", "reference an undefined check",
53
+ lambda s: s.replace("into it C1 ·", "into it C99 ·"),
54
+ 1, "C99 is referenced in Coverage"),
55
+ ("validate_checks", "checks.md", "duplicate check id",
56
+ lambda s: s.replace("**C6** - A `Suspended` subscription returns", "**C5** - A `Suspended` subscription returns"),
57
+ 1, "duplicate check id C5"),
58
+ ("validate_checks", "checks.md", "non-empty Unproven cell",
59
+ lambda s: s.replace("| C2, table-driven over all 9 | - |", "| C2, table-driven over all 9 | `paused` |"),
60
+ 1, "Unproven is '`paused`'"),
61
+ ("validate_checks", "checks.md", "proof with no test selector (warn)",
62
+ lambda s: s.replace('Proof: `bin/rails test test/billing/access_test.rb -n "/suspended_denies_paid_groups/"`',
63
+ "Proof: `bin/rails test`"),
64
+ 0, "names no test selector"),
65
+ ("validate_checks", "checks.md", "vague claim (warn)",
66
+ lambda s: s.replace("denies access to paid access groups", "handles access gracefully"),
67
+ 0, "instead of a concrete value"),
68
+
69
+ ("validate_plan", "plan.md", "missing Observable section",
70
+ lambda s: s.replace("## Observable", "## User-facing notes"),
71
+ 1, "missing required section: ## Observable"),
72
+ ("validate_plan", "plan.md", "empty Observable section",
73
+ lambda s: re.sub(r"## Observable\n.*?\n## Flow", "## Observable\n\n## Flow", s, flags=re.S),
74
+ 1, "Observable section is empty"),
75
+ ("validate_plan", "plan.md", "an Observable row with a blank landing",
76
+ lambda s: s.replace("| error shape and codes | AC 6 |", "| error shape and codes | |"),
77
+ 1, "landing is blank"),
78
+ ("validate_plan", "plan.md", "an Observable n/a with no reason",
79
+ lambda s: s.replace("| versioning | n/a - the provider pins the payload version in the envelope |",
80
+ "| versioning | n/a |"),
81
+ 1, "`n/a` with no reason"),
82
+
83
+ ("validate_plan", "plan.md", "missing Flow section",
84
+ lambda s: s.replace("## Flow", "## How it hangs together"),
85
+ 1, "missing required section: ## Flow"),
86
+ ("validate_plan", "plan.md", "empty Flow section",
87
+ lambda s: re.sub(r"## Flow\n.*?\n## Relations", "## Flow\n\n## Relations", s, flags=re.S),
88
+ 1, "Flow section is empty"),
89
+ ("validate_plan", "plan.md", "missing Relations section",
90
+ lambda s: s.replace("## Relations", "## Data model"),
91
+ 1, "missing required section: ## Relations"),
92
+ ("validate_plan", "plan.md", "empty Surface section",
93
+ lambda s: re.sub(r"## Surface\n.*?\n## Landing", "## Surface\n\n## Landing", s, flags=re.S),
94
+ 1, "Surface section is empty"),
95
+ ("validate_plan", "plan.md", "empty Landing section",
96
+ lambda s: re.sub(r"## Landing\n.*?\n## Impact", "## Landing\n\n## Impact", s, flags=re.S),
97
+ 1, "Landing section is empty"),
98
+ ("validate_plan", "plan.md", "Landing door with no rejected alternative",
99
+ lambda s: s.replace("| a boolean `is_suspended` - cannot express the next state |", "| |"),
100
+ 1, "no rejected alternative"),
101
+ ("validate_plan", "plan.md", "Landing door with no literal shape",
102
+ lambda s: s.replace("| enum value, not null, existing rows backfilled to `active` |", "| |"),
103
+ 1, "no literal shape"),
104
+ ("validate_plan", "plan.md", "columns and types creep into Relations",
105
+ lambda s: s.replace(
106
+ " Subscription ||--o{ WebhookDelivery : \"reported by\"",
107
+ " Subscription {\n string status\n datetime suspended_at\n }\n"
108
+ " Subscription ||--o{ WebhookDelivery : \"reported by\""),
109
+ 1, "carries an attribute block"),
110
+ ("validate_plan", "plan.md", "a Surface route with no statuses",
111
+ lambda s: s.replace("| `200`, `409`, `422` |", "| |"),
112
+ 1, "names no status code"),
113
+ ("validate_plan", "plan.md", "check ids written into Surface before checks exist",
114
+ lambda s: s.replace("| `200`, `409`, `422` |", "| `200`, `409`, `422` | C7, C8, C9 |"),
115
+ 1, "does not exist yet"),
116
+ ("validate_plan", "plan.md", "empty Impact section",
117
+ lambda s: re.sub(r"## Impact\n.*", "## Impact\n", s, flags=re.S),
118
+ 1, "Impact section is empty"),
119
+ ("validate_plan", "plan.md", "a Flow hop naming a module that neither exists nor is a door (warn)",
120
+ lambda s: s.replace(
121
+ "3. `Billing::StatusMap` (new, no door - placement per conventions) - provider status -> local status",
122
+ "3. `Billing::StatusMapFactory` - provider status -> local status"),
123
+ 0, "without marking it as existing or as a"),
124
+ ("validate_plan", "plan.md", "a source marked binding under profile standard (warn)",
125
+ lambda s: s.replace("- provider webhook reference - the 9 statuses",
126
+ "- design `03` - **binding for the interface** - the 9 statuses"),
127
+ 0, "declares profile standard, so nobody"),
128
+
129
+ ("validate_checks", "plan.md", "a route reviewed in the plan that no check mentions (warn)",
130
+ lambda s: s.replace("`POST /webhooks/provider`", "`POST /webhooks/provider/v2`"),
131
+ 0, "owe a Coverage row, or the route is dead"),
132
+
133
+ ("validate_plan", "plan.md", "acceptance criterion with no SHALL",
134
+ lambda s: s.replace("THEN the system SHALL set the subscription status", "THEN we set the subscription status"),
135
+ 1, "has no SHALL"),
136
+ ("validate_plan", "plan.md", "assumption with no rationale",
137
+ lambda s: s.replace("| the provider already retried 3 times before it reports a failure |", "| |"),
138
+ 1, "empty 'Rationale'"),
139
+ ("validate_plan", "plan.md", "missing required section",
140
+ lambda s: s.replace("## Traceability", "## Requirements Map"),
141
+ 1, "missing required section: ## Traceability"),
142
+ ("validate_plan", "plan.md", "out hop naming a slug in backticks is not an unresolved module",
143
+ lambda s: s.replace(
144
+ "5. out: `200` `{}`, and `AccessPolicy` (exists) reads `status` on the next request - no call from here",
145
+ "5. out: `200` `{}`; `completo` stays"),
146
+ 0, "0 warning"),
147
+ ("validate_plan", "plan.md", "flowchart node neither existing nor a door (warn)",
148
+ lambda s: s.replace(
149
+ "3. `Billing::StatusMap` (new, no door - placement per conventions) - provider status -> local status",
150
+ "```mermaid\nflowchart TD\n A[\"Billing::StatusMap\"] --> B[\"Billing::Ledger\"]\n```"),
151
+ 0, "belongs in the diff"),
152
+ ("validate_plan", "plan.md", "open question left unresolved (warn)",
153
+ lambda s: s.replace("**Open questions:** none - all resolved or logged above.",
154
+ "**Open questions:** what happens on a chargeback?"),
155
+ 0, "do not read as resolved"),
156
+
157
+ ("validate_verification", "verification.md", "surviving mutant next to PASS",
158
+ lambda s: s.replace("| `app/webhooks/ingest.rb:31` | yes |", "| `app/webhooks/ingest.rb:31` | no |"),
159
+ 1, "a mutant survived"),
160
+ ("validate_verification", "verification.md", "PASS with an unproven coverage member",
161
+ lambda s: s.replace("`trial_will_end` C10 | - |", "`trial_will_end` C10 | `paused` |"),
162
+ 1, "unproven"),
163
+ ("validate_verification", "verification.md", "profile standard with no fault rows",
164
+ lambda s: re.sub(r"## Faults injected\n.*?\n## Gate", "## Gate", s, flags=re.S),
165
+ 1, "no fault rows"),
166
+ ("validate_verification", "verification.md", "PASS with no file:line evidence",
167
+ lambda s: re.sub(r"`[\w./-]+\.rb:\d+`", "`the test file`", s),
168
+ 1, "cites no file:line evidence"),
169
+ ("validate_verification", "verification.md", "FAIL verdict",
170
+ lambda s: s.replace("**Verdict**: PASS", "**Verdict**: FAIL"),
171
+ 1, "verdict is FAIL"),
172
+ ("validate_verification", "verification.md", "unfilled template verdict",
173
+ lambda s: s.replace("**Verdict**: PASS", "**Verdict**: [PASS | FAIL]"),
174
+ 1, "template placeholder"),
175
+ ("validate_verification", "verification.md", "a check row that is not PASS",
176
+ lambda s: s.replace("`assert_equal 1, WebhookDelivery.count` | PASS |",
177
+ "`assert_equal 1, WebhookDelivery.count` | not run |"),
178
+ 1, "reports 'not run'"),
179
+ ("validate_verification", "verification.md", "self-verified report (warn)",
180
+ lambda s: s.replace("independent sub-agent (author != verifier)", "self-verified (degraded - no sub-agent)"),
181
+ 0, "self-verified"),
182
+ ("validate_verification", "verification.md", "report downgrades the approved profile",
183
+ lambda s: s.replace("**Profile**: standard", "**Profile**: light"),
184
+ 1, "checks.md was approved under 'standard'"),
185
+
186
+ # Profile-scoped steps: raising the profile in checks.md must make the report owe a section.
187
+ ("validate_verification", "checks.md", "profile ui but no binding-sources section in the report",
188
+ lambda s: s.replace("Profile: standard", "Profile: ui"),
189
+ 1, "no `## Binding sources` section"),
190
+ ("validate_verification", "verification.md", "profile standard with no recomputed Coverage",
191
+ lambda s: re.sub(r"## Coverage\n.*?\n## Test policy rows", "## Test policy rows", s, flags=re.S),
192
+ 1, "no `## Coverage` section"),
193
+ ("validate_verification", "verification.md", "an unmet Test policy row next to PASS",
194
+ lambda s: s.replace("| own layer C2 | yes |", "| own layer C2 | no |"),
195
+ 1, "Test policy row is unmet"),
196
+ ]
197
+
198
+ # Negative controls: under `light` the profile-scoped steps must NOT be demanded. A gate that
199
+ # fires at every profile is the same bug as one that never fires - it just fails loudly instead
200
+ # of silently, and it would make `light` unusable.
201
+ NEGATIVE_CONTROLS = [
202
+ ("validate_verification", "a light report with no faults and no coverage is accepted",
203
+ {"checks.md": lambda s: s.replace("Profile: standard", "Profile: light"),
204
+ "verification.md": lambda s: re.sub(
205
+ r"## Coverage\n.*?\n## Faults injected\n.*?\n## Gate", "## Gate",
206
+ s.replace("**Profile**: standard", "**Profile**: light"), flags=re.S)}),
207
+ ]
208
+
209
+
210
+ def scaffold():
211
+ root = tempfile.mkdtemp(prefix="tlc-spec-lean-selftest-")
212
+ fdir = os.path.join(root, ".specs", "features", FEATURE)
213
+ os.makedirs(fdir)
214
+ for name in ("plan.md", "checks.md", "verification.md"):
215
+ shutil.copyfile(os.path.join(FIXTURES, name), os.path.join(fdir, name))
216
+ return root
217
+
218
+
219
+ def run(validator, root):
220
+ p = subprocess.run(
221
+ [sys.executable, os.path.join(HERE, f"{validator}.py"), FEATURE, "--root", root],
222
+ capture_output=True, text=True,
223
+ )
224
+ return p.returncode, p.stdout + p.stderr
225
+
226
+
227
+ def run_script(name, *args, cwd=None):
228
+ """Run a shipped script directly and return (exit code, combined output)."""
229
+ proc = subprocess.run(
230
+ [sys.executable, os.path.join(HERE, name), *args],
231
+ capture_output=True, text=True, cwd=cwd,
232
+ )
233
+ return proc.returncode, proc.stdout + proc.stderr
234
+
235
+
236
+ # (script, description, argv, expected exit, expected output fragment)
237
+ TOOLING = [
238
+ ("check_commit.py", "a conventional message passes",
239
+ ["feat: suspend the subscription on a failed charge"], 0, "OK"),
240
+ ("check_commit.py", "a non-conventional message fails",
241
+ ["Updated stuff."], 1, "does not match"),
242
+ ("check_commit.py", "a capitalized description fails",
243
+ ["feat: Suspend the subscription"], 1, "start lowercase"),
244
+ ("check_commit.py", "an unknown type fails",
245
+ ["wip: suspend the subscription"], 1, "is not one of"),
246
+ ("lessons.py", "normalization regressions pass", ["selftest"], 0, "ok"),
247
+ ("lessons.py", "--root before the subcommand is the documented form",
248
+ ["--root", tempfile.gettempdir(), "list", "--status", "confirmed"], 0, ""),
249
+ ]
250
+
251
+
252
+ def run_tooling():
253
+ """Exercise the scripts the skill ships beyond the validators.
254
+
255
+ A shipped script nothing ever calls is where a crash hides: check_commit.py once raised a
256
+ traceback on the message form of its own argument, and no gate noticed because no gate ran it.
257
+ """
258
+ print("\ntooling - every shipped script must run, not just parse")
259
+ ok = True
260
+ for script, desc, argv, want_code, want_frag in TOOLING:
261
+ code, out = run_script(script, *argv)
262
+ if code == want_code and want_frag.lower() in out.lower():
263
+ print(f" ok {script}: {desc}")
264
+ continue
265
+ ok = False
266
+ print(f" BROKEN {script}: {desc} - expected exit={want_code} and {want_frag!r}, "
267
+ f"got exit={code}")
268
+ print(" " + out.strip().replace("\n", "\n "))
269
+
270
+ # The completion gate must not go green on an empty run. This is a control, not a mutant:
271
+ # "no report found" once exited 0, which reads as a pass to anything checking exit codes.
272
+ empty = tempfile.mkdtemp(prefix="empty-root-")
273
+ try:
274
+ for argv, want, desc in [
275
+ (["--root", empty], 2, "gating nothing exits 2, not 0"),
276
+ (["--root", empty, "--allow-empty"], 0, "an explicit empty sweep is allowed"),
277
+ (["missing-feature", "--root", empty], 2, "an unresolvable feature exits 2"),
278
+ ]:
279
+ code, out = run_script("validate_verification.py", *argv)
280
+ if code == want:
281
+ print(f" ok validate_verification.py: {desc}")
282
+ else:
283
+ ok = False
284
+ print(f" BROKEN validate_verification.py: {desc} - expected exit={want}, "
285
+ f"got {code}: {out.strip()}")
286
+ finally:
287
+ shutil.rmtree(empty, ignore_errors=True)
288
+
289
+ # the lessons store has to survive a real round-trip on disk, not only its unit checks
290
+ root = tempfile.mkdtemp(prefix="lessons-")
291
+ try:
292
+ steps = [
293
+ (["--root", root, "init"], "Initialized"),
294
+ (["--root", root, "add", "--feature", FEATURE, "--signal", "surviving_mutant",
295
+ "--source", "app/billing/x.rb:31", "--text", "assert the dedup path returns the row"],
296
+ "ADDED"),
297
+ (["--root", root, "status"], "1 total"),
298
+ ]
299
+ for argv, frag in steps:
300
+ code, out = run_script("lessons.py", *argv)
301
+ if code != 0 or frag.lower() not in out.lower():
302
+ ok = False
303
+ print(f" BROKEN lessons.py: round-trip step {argv[2]!r} - expected {frag!r}, "
304
+ f"got exit={code}: {out.strip()}")
305
+ break
306
+ else:
307
+ for artifact in ("lessons.json", "LESSONS.md"):
308
+ if not os.path.isfile(os.path.join(root, ".specs", artifact)):
309
+ ok = False
310
+ print(f" BROKEN lessons.py: round-trip wrote no .specs/{artifact}")
311
+ break
312
+ else:
313
+ print(" ok lessons.py: init - add - status round-trip writes both artifacts")
314
+ finally:
315
+ shutil.rmtree(root, ignore_errors=True)
316
+ return ok
317
+
318
+
319
+ def main(argv=None):
320
+ verbose = "-v" in (argv or sys.argv[1:])
321
+ if not os.path.isdir(FIXTURES):
322
+ print(f"selftest: fixtures directory missing: {FIXTURES}", file=sys.stderr)
323
+ return 1
324
+
325
+ base = scaffold()
326
+ print("baseline - a clean artifact set must pass every gate")
327
+ baseline_ok = True
328
+ for validator in ("validate_plan", "validate_checks", "validate_verification"):
329
+ code, out = run(validator, base)
330
+ if code != 0:
331
+ baseline_ok = False
332
+ print(f" BROKEN {validator} exit={code}")
333
+ print(" " + out.strip().replace("\n", "\n "))
334
+ else:
335
+ print(f" ok {validator}")
336
+ shutil.rmtree(base)
337
+
338
+ print("\nfault injection - every mutant must be killed")
339
+ killed = survived = 0
340
+ for validator, artifact, desc, mutate, want_code, want_msg in CASES:
341
+ root = scaffold()
342
+ path = os.path.join(root, ".specs", "features", FEATURE, artifact)
343
+ with open(path, encoding="utf-8") as f:
344
+ original = f.read()
345
+ mutated = mutate(original)
346
+ if mutated == original:
347
+ survived += 1
348
+ print(f" VACUOUS {validator}: {desc} - the mutation did not apply, so the case proves nothing")
349
+ shutil.rmtree(root)
350
+ continue
351
+ with open(path, "w", encoding="utf-8") as f:
352
+ f.write(mutated)
353
+ code, out = run(validator, root)
354
+ if code == want_code and want_msg in out:
355
+ killed += 1
356
+ if verbose:
357
+ print(f" killed {validator}: {desc}")
358
+ else:
359
+ survived += 1
360
+ print(f" SURVIVED {validator}: {desc}")
361
+ print(f" expected exit={want_code} and {want_msg!r}, got exit={code}")
362
+ print(" " + out.strip().replace("\n", "\n "))
363
+ shutil.rmtree(root)
364
+
365
+ if not verbose and survived == 0:
366
+ print(f" all {killed} killed (-v to list them)")
367
+
368
+ print("\nnegative controls - a profile-scoped gate must not fire below its profile")
369
+ controls_ok = True
370
+ for validator, desc, edits in NEGATIVE_CONTROLS:
371
+ root = scaffold()
372
+ for artifact, edit in edits.items():
373
+ path = os.path.join(root, ".specs", "features", FEATURE, artifact)
374
+ with open(path, encoding="utf-8") as f:
375
+ original = f.read()
376
+ mutated = edit(original)
377
+ if mutated == original:
378
+ controls_ok = False
379
+ print(f" VACUOUS {validator}: {desc} - the edit to {artifact} did not apply")
380
+ with open(path, "w", encoding="utf-8") as f:
381
+ f.write(mutated)
382
+ code, out = run(validator, root)
383
+ if code == 0:
384
+ print(f" ok {validator}: {desc}")
385
+ else:
386
+ controls_ok = False
387
+ print(f" FIRED {validator}: {desc} - expected exit=0, got {code}")
388
+ print(" " + out.strip().replace("\n", "\n "))
389
+ shutil.rmtree(root)
390
+
391
+ tooling_ok = run_tooling()
392
+
393
+ print(f"\nselftest: {killed} killed, {survived} survived, "
394
+ f"controls {'ok' if controls_ok else 'BROKEN'}, "
395
+ f"tooling {'ok' if tooling_ok else 'BROKEN'}, "
396
+ f"baseline {'clean' if baseline_ok else 'BROKEN'}")
397
+ return 0 if (survived == 0 and baseline_ok and controls_ok and tooling_ok) else 1
398
+
399
+
400
+ if __name__ == "__main__":
401
+ raise SystemExit(main())