@cleocode/skills 2026.5.3 → 2026.5.5
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/package.json +1 -1
- package/skills/ct-council/SKILL.md +0 -377
- package/skills/ct-council/optimization/HARDENING-PLAYBOOK.md +0 -107
- package/skills/ct-council/optimization/README.md +0 -74
- package/skills/ct-council/optimization/scenarios.yaml +0 -121
- package/skills/ct-council/optimization/scripts/campaign.py +0 -543
- package/skills/ct-council/optimization/scripts/test_campaign.py +0 -143
- package/skills/ct-council/references/chairman.md +0 -119
- package/skills/ct-council/references/contrarian.md +0 -70
- package/skills/ct-council/references/evidence-pack.md +0 -145
- package/skills/ct-council/references/examples.md +0 -235
- package/skills/ct-council/references/executor.md +0 -83
- package/skills/ct-council/references/expansionist.md +0 -68
- package/skills/ct-council/references/first-principles.md +0 -73
- package/skills/ct-council/references/outsider.md +0 -73
- package/skills/ct-council/references/peer-review.md +0 -125
- package/skills/ct-council/scripts/analyze_runs.py +0 -293
- package/skills/ct-council/scripts/fixtures/executor_multi.md +0 -198
- package/skills/ct-council/scripts/fixtures/missing_advisor.md +0 -117
- package/skills/ct-council/scripts/fixtures/missing_convergence.md +0 -190
- package/skills/ct-council/scripts/fixtures/thin_evidence.md +0 -193
- package/skills/ct-council/scripts/fixtures/valid.md +0 -226
- package/skills/ct-council/scripts/fixtures/valid_with_llmtxt.md +0 -226
- package/skills/ct-council/scripts/llmtxt_ref.py +0 -223
- package/skills/ct-council/scripts/run_council.py +0 -578
- package/skills/ct-council/scripts/telemetry.py +0 -624
- package/skills/ct-council/scripts/test_telemetry.py +0 -509
- package/skills/ct-council/scripts/test_validate.py +0 -452
- package/skills/ct-council/scripts/validate.py +0 -396
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
validate.py — structural validator for Council run outputs.
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
python3 validate.py <path-to-output.md>
|
|
7
|
-
python3 validate.py --json <path> # emit JSON report on stdout
|
|
8
|
-
python3 validate.py --strict <path> # treat warnings as failures
|
|
9
|
-
|
|
10
|
-
Exit codes:
|
|
11
|
-
0 — valid
|
|
12
|
-
1 — structural violations (fatal)
|
|
13
|
-
2 — semantic warnings (convergence, weak grounding) with --strict
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import argparse
|
|
19
|
-
import json
|
|
20
|
-
import re
|
|
21
|
-
import sys
|
|
22
|
-
from dataclasses import dataclass, asdict
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
|
|
25
|
-
ADVISORS = ["Contrarian", "First Principles", "Expansionist", "Outsider", "Executor"]
|
|
26
|
-
|
|
27
|
-
PEER_REVIEW_ROTATION = [
|
|
28
|
-
("Contrarian", "First Principles"),
|
|
29
|
-
("First Principles", "Expansionist"),
|
|
30
|
-
("Expansionist", "Outsider"),
|
|
31
|
-
("Outsider", "Executor"),
|
|
32
|
-
("Executor", "Contrarian"),
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
ADVISOR_REQUIRED_MARKERS = [
|
|
36
|
-
"**Frame:**",
|
|
37
|
-
"**Evidence anchored:**",
|
|
38
|
-
"**Verdict from this lens:**",
|
|
39
|
-
"**Single sharpest point:**",
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
EXECUTOR_EXTRA_MARKERS = [
|
|
43
|
-
"**The action (one):**",
|
|
44
|
-
"**Expected outcome",
|
|
45
|
-
"**What this unblocks:**",
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
PEER_REVIEW_GATES = ["G1 Rigor", "G2 Evidence grounding", "G3 Frame integrity", "G4 Actionability"]
|
|
49
|
-
|
|
50
|
-
PEER_REVIEW_REQUIRED_MARKERS = [
|
|
51
|
-
"**Gate results:**",
|
|
52
|
-
"**Strongest finding",
|
|
53
|
-
"**Gap from",
|
|
54
|
-
"**What I would add:**",
|
|
55
|
-
"**Disposition:**",
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
CHAIRMAN_REQUIRED_SUBSECTIONS = [
|
|
59
|
-
"### Gate summary",
|
|
60
|
-
"### Recommendation",
|
|
61
|
-
"### Why this, not the alternatives",
|
|
62
|
-
"### What each advisor got right",
|
|
63
|
-
"### Conditions on the recommendation",
|
|
64
|
-
"### Next 60-minute action",
|
|
65
|
-
"### Confidence",
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@dataclass
|
|
70
|
-
class Violation:
|
|
71
|
-
kind: str # "structural" | "semantic" | "warning"
|
|
72
|
-
section: str
|
|
73
|
-
message: str
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class Validator:
|
|
77
|
-
def __init__(self, md: str):
|
|
78
|
-
self.md = md
|
|
79
|
-
self.violations: list[Violation] = []
|
|
80
|
-
|
|
81
|
-
# ─── helpers ────────────────────────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
def _section_body(self, header_regex: str) -> str | None:
|
|
84
|
-
"""Return the body text under the first header matching header_regex, up to the next same-or-higher level header.
|
|
85
|
-
|
|
86
|
-
Line-based scan that correctly ignores headers inside ``` code fences.
|
|
87
|
-
"""
|
|
88
|
-
header_re = re.compile(r"^(#{1,6})\s+(.+?)\s*$")
|
|
89
|
-
lines = self.md.split("\n")
|
|
90
|
-
in_fence = False
|
|
91
|
-
start_line = None # first line of body (after matching header)
|
|
92
|
-
start_level = None
|
|
93
|
-
end_line = len(lines) # body end (exclusive)
|
|
94
|
-
|
|
95
|
-
for i, line in enumerate(lines):
|
|
96
|
-
if line.lstrip().startswith("```"):
|
|
97
|
-
in_fence = not in_fence
|
|
98
|
-
continue
|
|
99
|
-
if in_fence:
|
|
100
|
-
continue
|
|
101
|
-
m = header_re.match(line)
|
|
102
|
-
if not m:
|
|
103
|
-
continue
|
|
104
|
-
level = len(m.group(1))
|
|
105
|
-
title = m.group(2).strip()
|
|
106
|
-
if start_line is None:
|
|
107
|
-
if re.match(header_regex, title):
|
|
108
|
-
start_line = i + 1
|
|
109
|
-
start_level = level
|
|
110
|
-
else:
|
|
111
|
-
if level <= start_level:
|
|
112
|
-
end_line = i
|
|
113
|
-
break
|
|
114
|
-
|
|
115
|
-
if start_line is None:
|
|
116
|
-
return None
|
|
117
|
-
return "\n".join(lines[start_line:end_line])
|
|
118
|
-
|
|
119
|
-
def _fail(self, section: str, message: str, kind: str = "structural"):
|
|
120
|
-
self.violations.append(Violation(kind=kind, section=section, message=message))
|
|
121
|
-
|
|
122
|
-
# ─── checks ─────────────────────────────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
def check_top_header(self):
|
|
125
|
-
m = re.search(r"^#\s+The Council\s+—\s+(.+)$", self.md, re.MULTILINE)
|
|
126
|
-
if not m:
|
|
127
|
-
self._fail("H1", "Missing H1: expected '# The Council — <one-line question>'.")
|
|
128
|
-
return
|
|
129
|
-
question = m.group(1).strip()
|
|
130
|
-
if len(question) < 10:
|
|
131
|
-
self._fail("H1", f"Restated question too short: {question!r}. Expected a full one-sentence decision question.")
|
|
132
|
-
|
|
133
|
-
def check_evidence_pack(self):
|
|
134
|
-
body = self._section_body(r"^Evidence pack$")
|
|
135
|
-
if body is None:
|
|
136
|
-
self._fail("Evidence pack", "Missing '## Evidence pack' section.")
|
|
137
|
-
return
|
|
138
|
-
items = re.findall(r"^\s*\d+\.\s+(.+?)(?=^\s*\d+\.\s+|\Z)", body, re.MULTILINE | re.DOTALL)
|
|
139
|
-
count = len(items)
|
|
140
|
-
if count < 3:
|
|
141
|
-
self._fail("Evidence pack", f"Evidence pack has {count} items; minimum is 3.")
|
|
142
|
-
if count > 7:
|
|
143
|
-
self._fail("Evidence pack", f"Evidence pack has {count} items; maximum is 7.", kind="warning")
|
|
144
|
-
for i, item in enumerate(items, 1):
|
|
145
|
-
item_text = item.strip()
|
|
146
|
-
# Each item must contain a rationale separator (—, --, or ":").
|
|
147
|
-
has_rationale = ("—" in item_text) or (" -- " in item_text) or (": " in item_text and "`" in item_text)
|
|
148
|
-
if not has_rationale:
|
|
149
|
-
self._fail("Evidence pack", f"Item {i} appears to lack a rationale (expected 'citation — why this matters').")
|
|
150
|
-
# Each item should have a citation-like token: backticks, file:line, sha, or URL.
|
|
151
|
-
has_citation = bool(re.search(r"`[^`]+`|\b[0-9a-f]{7,40}\b|https?://|\.(ts|py|md|rs|tsx|js|sql)\b", item_text))
|
|
152
|
-
if not has_citation:
|
|
153
|
-
self._fail("Evidence pack", f"Item {i} appears to lack a citation (expected `path:line` | `symbol` | sha | URL).", kind="warning")
|
|
154
|
-
|
|
155
|
-
def check_advisor_sections(self):
|
|
156
|
-
for advisor in ADVISORS:
|
|
157
|
-
body = self._section_body(rf"^Advisor:\s+{re.escape(advisor)}$")
|
|
158
|
-
if body is None:
|
|
159
|
-
self._fail(f"Advisor: {advisor}", f"Missing '### Advisor: {advisor}' section.")
|
|
160
|
-
continue
|
|
161
|
-
for marker in ADVISOR_REQUIRED_MARKERS:
|
|
162
|
-
if marker not in body:
|
|
163
|
-
self._fail(f"Advisor: {advisor}", f"Missing required marker: {marker}")
|
|
164
|
-
# Evidence anchored must have ≥2 bullet items.
|
|
165
|
-
ea_match = re.search(r"\*\*Evidence anchored:\*\*(.*?)(?=\n\*\*|\Z)", body, re.DOTALL)
|
|
166
|
-
if ea_match:
|
|
167
|
-
bullets = re.findall(r"^-\s+.+", ea_match.group(1), re.MULTILINE)
|
|
168
|
-
if len(bullets) < 2:
|
|
169
|
-
self._fail(f"Advisor: {advisor}", f"Evidence anchored has {len(bullets)} items; minimum is 2.")
|
|
170
|
-
# Executor-specific markers.
|
|
171
|
-
if advisor == "Executor":
|
|
172
|
-
for marker in EXECUTOR_EXTRA_MARKERS:
|
|
173
|
-
if marker not in body:
|
|
174
|
-
self._fail("Advisor: Executor", f"Missing required marker: {marker}")
|
|
175
|
-
self.check_executor_single_action(body)
|
|
176
|
-
|
|
177
|
-
def check_executor_single_action(self, body: str):
|
|
178
|
-
"""The action must be exactly one paragraph, not a numbered or bulleted list."""
|
|
179
|
-
m = re.search(r"\*\*The action \(one\):\*\*(.+?)(?=\n\*\*|\Z)", body, re.DOTALL)
|
|
180
|
-
if not m:
|
|
181
|
-
return
|
|
182
|
-
action_body = m.group(1).strip()
|
|
183
|
-
numbered = re.findall(r"^\s*\d+\.\s+", action_body, re.MULTILINE)
|
|
184
|
-
bulleted = re.findall(r"^\s*[-*]\s+", action_body, re.MULTILINE)
|
|
185
|
-
if len(numbered) > 1:
|
|
186
|
-
self._fail("Advisor: Executor",
|
|
187
|
-
f"'The action (one)' contains {len(numbered)} numbered items; exactly one action required.")
|
|
188
|
-
if len(bulleted) > 1:
|
|
189
|
-
self._fail("Advisor: Executor",
|
|
190
|
-
f"'The action (one)' contains {len(bulleted)} bulleted items; exactly one action required.")
|
|
191
|
-
|
|
192
|
-
def check_peer_reviews(self):
|
|
193
|
-
for reviewer, reviewee in PEER_REVIEW_ROTATION:
|
|
194
|
-
header_re = rf"^{re.escape(reviewer)} reviewing {re.escape(reviewee)}$"
|
|
195
|
-
body = self._section_body(header_re)
|
|
196
|
-
if body is None:
|
|
197
|
-
self._fail("Peer review",
|
|
198
|
-
f"Missing peer review section: '### {reviewer} reviewing {reviewee}'.")
|
|
199
|
-
continue
|
|
200
|
-
for marker in PEER_REVIEW_REQUIRED_MARKERS:
|
|
201
|
-
if marker not in body:
|
|
202
|
-
self._fail(f"{reviewer} reviewing {reviewee}",
|
|
203
|
-
f"Missing required marker: {marker}")
|
|
204
|
-
# Gate lines must each appear with PASS or FAIL.
|
|
205
|
-
for gate in PEER_REVIEW_GATES:
|
|
206
|
-
gate_re = rf"-\s+{re.escape(gate)}:\s+(PASS|FAIL)\s+—\s+.+"
|
|
207
|
-
if not re.search(gate_re, body):
|
|
208
|
-
self._fail(f"{reviewer} reviewing {reviewee}",
|
|
209
|
-
f"Gate '{gate}' missing or malformed. Expected: '- {gate}: PASS|FAIL — <evidence>'.")
|
|
210
|
-
# Disposition must be one of Accept / Modify / Reject.
|
|
211
|
-
disp_match = re.search(r"\*\*Disposition:\*\*\s+(Accept|Modify|Reject)\b", body)
|
|
212
|
-
if not disp_match:
|
|
213
|
-
self._fail(f"{reviewer} reviewing {reviewee}",
|
|
214
|
-
"Disposition must be one of: Accept | Modify | Reject.")
|
|
215
|
-
|
|
216
|
-
def check_convergence_section(self):
|
|
217
|
-
body = self._section_body(r"^Phase 2\.5\s*[—-]\s*Convergence check$")
|
|
218
|
-
if body is None:
|
|
219
|
-
self._fail("Phase 2.5", "Missing '## Phase 2.5 — Convergence check' section.")
|
|
220
|
-
|
|
221
|
-
def check_chairman_verdict(self):
|
|
222
|
-
body = self._section_body(r"^Phase 3\s*[—-]\s*Chairman['’]s verdict$")
|
|
223
|
-
if body is None:
|
|
224
|
-
self._fail("Phase 3", "Missing '## Phase 3 — Chairman's verdict' section.")
|
|
225
|
-
return
|
|
226
|
-
for marker in CHAIRMAN_REQUIRED_SUBSECTIONS:
|
|
227
|
-
if marker not in body:
|
|
228
|
-
self._fail("Phase 3", f"Missing required subsection: {marker}")
|
|
229
|
-
# Gate summary table must reference all five advisors.
|
|
230
|
-
for advisor in ADVISORS:
|
|
231
|
-
if advisor not in body:
|
|
232
|
-
self._fail("Phase 3", f"Gate summary table missing row for: {advisor}")
|
|
233
|
-
# Next 60-minute action must have non-empty body.
|
|
234
|
-
action_match = re.search(r"###\s+Next 60-minute action\s*\n(.+?)(?=\n###|\Z)", body, re.DOTALL)
|
|
235
|
-
if action_match and len(action_match.group(1).strip()) < 15:
|
|
236
|
-
self._fail("Phase 3", "Next 60-minute action is empty or too short to be actionable.")
|
|
237
|
-
# Confidence must be low/medium/high.
|
|
238
|
-
conf_match = re.search(r"###\s+Confidence\s*\n(.+?)(?=\n###|\Z)", body, re.DOTALL)
|
|
239
|
-
if conf_match:
|
|
240
|
-
conf_text = conf_match.group(1).strip().lower()
|
|
241
|
-
if not any(level in conf_text for level in ["low", "medium", "high"]):
|
|
242
|
-
self._fail("Phase 3", "Confidence must be one of: low | medium | high.")
|
|
243
|
-
|
|
244
|
-
# ─── entry ──────────────────────────────────────────────────────────────
|
|
245
|
-
|
|
246
|
-
def validate(self, phase: int | None = None) -> list[Violation]:
|
|
247
|
-
"""Run structural checks. If `phase` is given, only check sections
|
|
248
|
-
that should exist by the end of that phase:
|
|
249
|
-
phase=0 → H1 + evidence pack only
|
|
250
|
-
phase=1 → +5 advisor sections
|
|
251
|
-
phase=2 → +5 peer reviews
|
|
252
|
-
phase=3 (or None) → +convergence + chairman (full output)
|
|
253
|
-
"""
|
|
254
|
-
if phase is None:
|
|
255
|
-
phase = 3
|
|
256
|
-
self.check_top_header()
|
|
257
|
-
self.check_evidence_pack()
|
|
258
|
-
if phase >= 1:
|
|
259
|
-
self.check_advisor_sections()
|
|
260
|
-
if phase >= 2:
|
|
261
|
-
self.check_peer_reviews()
|
|
262
|
-
if phase >= 3:
|
|
263
|
-
self.check_convergence_section()
|
|
264
|
-
self.check_chairman_verdict()
|
|
265
|
-
return self.violations
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def detect_phase(md: str) -> int:
|
|
269
|
-
"""Best-effort detection of which phase a partial file represents.
|
|
270
|
-
|
|
271
|
-
Returns 0..3 based on which sections are present:
|
|
272
|
-
- Phase 3 header → 3 (assume full output)
|
|
273
|
-
- Any peer-review section header → 2
|
|
274
|
-
- Any '### Advisor:' header → 1
|
|
275
|
-
- Otherwise → 0 (just evidence pack)
|
|
276
|
-
|
|
277
|
-
Authors typically build the file phase-by-phase; this lets the validator
|
|
278
|
-
match the author's actual progress instead of failing on missing sections
|
|
279
|
-
that legitimately don't exist yet.
|
|
280
|
-
"""
|
|
281
|
-
# Use the same fence-aware scan logic as Validator._section_body would.
|
|
282
|
-
header_re = re.compile(r"^(#{1,6})\s+(.+?)\s*$")
|
|
283
|
-
in_fence = False
|
|
284
|
-
has_phase3 = False
|
|
285
|
-
has_phase2 = False
|
|
286
|
-
has_phase1 = False
|
|
287
|
-
for line in md.split("\n"):
|
|
288
|
-
if line.lstrip().startswith("```"):
|
|
289
|
-
in_fence = not in_fence
|
|
290
|
-
continue
|
|
291
|
-
if in_fence:
|
|
292
|
-
continue
|
|
293
|
-
m = header_re.match(line)
|
|
294
|
-
if not m:
|
|
295
|
-
continue
|
|
296
|
-
title = m.group(2).strip()
|
|
297
|
-
if re.match(r"^Phase 3\b", title):
|
|
298
|
-
has_phase3 = True
|
|
299
|
-
elif "reviewing" in title and any(a in title for a in ADVISORS):
|
|
300
|
-
has_phase2 = True
|
|
301
|
-
elif title.startswith("Advisor:"):
|
|
302
|
-
has_phase1 = True
|
|
303
|
-
if has_phase3:
|
|
304
|
-
return 3
|
|
305
|
-
if has_phase2:
|
|
306
|
-
return 2
|
|
307
|
-
if has_phase1:
|
|
308
|
-
return 1
|
|
309
|
-
return 0
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
def report(violations: list[Violation], as_json: bool = False) -> str:
|
|
313
|
-
if as_json:
|
|
314
|
-
return json.dumps({
|
|
315
|
-
"valid": not any(v.kind == "structural" for v in violations),
|
|
316
|
-
"violations": [asdict(v) for v in violations],
|
|
317
|
-
}, indent=2)
|
|
318
|
-
if not violations:
|
|
319
|
-
return "✅ Council output is structurally valid."
|
|
320
|
-
lines = [f"❌ {len(violations)} violation(s) found:"]
|
|
321
|
-
for v in violations:
|
|
322
|
-
prefix = {"structural": "FAIL", "semantic": "SEMA", "warning": "WARN"}.get(v.kind, "????")
|
|
323
|
-
lines.append(f" [{prefix}] {v.section}: {v.message}")
|
|
324
|
-
return "\n".join(lines)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
def main():
|
|
328
|
-
parser = argparse.ArgumentParser(
|
|
329
|
-
description="Validate a Council run output (full or partial).",
|
|
330
|
-
epilog=(
|
|
331
|
-
"Use --phase N to validate a partial file that hasn't reached Phase 3 yet:\n"
|
|
332
|
-
" --phase 0 → only H1 + evidence pack (use after writing phase0.md)\n"
|
|
333
|
-
" --phase 1 → also 5 advisor sections\n"
|
|
334
|
-
" --phase 2 → also 5 peer reviews\n"
|
|
335
|
-
" (no flag) → full output (default; auto-detects partial files and suggests --phase)"
|
|
336
|
-
),
|
|
337
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
338
|
-
)
|
|
339
|
-
parser.add_argument("path", help="Path to the markdown to validate.")
|
|
340
|
-
parser.add_argument("--json", action="store_true", help="Emit JSON report on stdout.")
|
|
341
|
-
parser.add_argument("--strict", action="store_true", help="Fail on warnings, not just structural errors.")
|
|
342
|
-
parser.add_argument(
|
|
343
|
-
"--phase",
|
|
344
|
-
type=int,
|
|
345
|
-
choices=[0, 1, 2, 3],
|
|
346
|
-
default=None,
|
|
347
|
-
help="Partial-validation mode. Without this flag, validates the full output (Phase 3).",
|
|
348
|
-
)
|
|
349
|
-
parser.add_argument(
|
|
350
|
-
"--auto-detect",
|
|
351
|
-
action="store_true",
|
|
352
|
-
help="Auto-detect phase from file contents and validate up to that phase. Useful for in-progress files.",
|
|
353
|
-
)
|
|
354
|
-
args = parser.parse_args()
|
|
355
|
-
|
|
356
|
-
path = Path(args.path)
|
|
357
|
-
if not path.exists():
|
|
358
|
-
print(f"❌ File not found: {path}", file=sys.stderr)
|
|
359
|
-
sys.exit(1)
|
|
360
|
-
|
|
361
|
-
md = path.read_text()
|
|
362
|
-
requested_phase = args.phase
|
|
363
|
-
|
|
364
|
-
# If user passed nothing AND the file has no Phase 3 header, surface a
|
|
365
|
-
# helpful message instead of dumping a wall of "missing section" errors.
|
|
366
|
-
if requested_phase is None and not args.auto_detect:
|
|
367
|
-
detected = detect_phase(md)
|
|
368
|
-
if detected < 3:
|
|
369
|
-
phase_names = {0: "evidence pack only", 1: "through Phase 1 (advisors)", 2: "through Phase 2 (peer reviews)"}
|
|
370
|
-
print(
|
|
371
|
-
f"⚠️ Detected partial file (highest section present: phase {detected} — "
|
|
372
|
-
f"{phase_names.get(detected, 'unknown')}).\n"
|
|
373
|
-
f" Validating with --phase {detected} so missing downstream sections don't drown out real issues.\n"
|
|
374
|
-
f" Pass --phase 3 (or wait until output.md is fully assembled) to enforce full structure.\n",
|
|
375
|
-
file=sys.stderr,
|
|
376
|
-
)
|
|
377
|
-
requested_phase = detected
|
|
378
|
-
|
|
379
|
-
if args.auto_detect:
|
|
380
|
-
requested_phase = detect_phase(md)
|
|
381
|
-
|
|
382
|
-
v = Validator(md)
|
|
383
|
-
violations = v.validate(phase=requested_phase)
|
|
384
|
-
print(report(violations, as_json=args.json))
|
|
385
|
-
|
|
386
|
-
structural = [x for x in violations if x.kind == "structural"]
|
|
387
|
-
warnings = [x for x in violations if x.kind == "warning"]
|
|
388
|
-
if structural:
|
|
389
|
-
sys.exit(1)
|
|
390
|
-
if args.strict and warnings:
|
|
391
|
-
sys.exit(2)
|
|
392
|
-
sys.exit(0)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if __name__ == "__main__":
|
|
396
|
-
main()
|