sdcs 1.3.0__py3-none-any.whl
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.
- sdcs/__init__.py +8 -0
- sdcs/audit.py +326 -0
- sdcs/cli.py +184 -0
- sdcs/init.py +588 -0
- sdcs/verifier/__init__.py +15 -0
- sdcs/verifier/topology.py +298 -0
- sdcs-1.3.0.dist-info/METADATA +457 -0
- sdcs-1.3.0.dist-info/RECORD +12 -0
- sdcs-1.3.0.dist-info/WHEEL +5 -0
- sdcs-1.3.0.dist-info/entry_points.txt +4 -0
- sdcs-1.3.0.dist-info/licenses/LICENSE +21 -0
- sdcs-1.3.0.dist-info/top_level.txt +1 -0
sdcs/__init__.py
ADDED
sdcs/audit.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sdcs.audit — Positive Ground Truth & Corpus Diversity Auditor
|
|
3
|
+
Conforming to SPEC-001 v1.3.0 (Pillars 7 & Failure Mitigation 7.4)
|
|
4
|
+
|
|
5
|
+
Verifies:
|
|
6
|
+
1. Asset Reachability: Fixtures referenced in evals.md exist on disk.
|
|
7
|
+
2. Cryptographic Integrity: Computes normalized SHA-256 digests.
|
|
8
|
+
3. Anti-Evasion Normalization: Strips comments, line-end padding, and
|
|
9
|
+
whitespace variance before digest calculation.
|
|
10
|
+
4. Corpus Diversity Guard: Flags duplicate/near-duplicate fixtures
|
|
11
|
+
masquerading as independent tests (Phantom Corpus Trap).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import hashlib
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import NamedTuple
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FixtureEntry(NamedTuple):
|
|
23
|
+
fixture_id: str
|
|
24
|
+
path_str: str
|
|
25
|
+
recorded_hash: str
|
|
26
|
+
status: str
|
|
27
|
+
line_number: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def calculate_sha256(filepath: Path) -> str:
|
|
31
|
+
"""Calculates standard raw SHA-256 digest over file bytes."""
|
|
32
|
+
hasher = hashlib.sha256()
|
|
33
|
+
with open(filepath, "rb") as f:
|
|
34
|
+
while chunk := f.read(65536):
|
|
35
|
+
hasher.update(chunk)
|
|
36
|
+
return hasher.hexdigest()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def strip_comments_and_whitespace(content: str, extension: str) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Normalizes text fixtures to defeat trivial hashing evasion (SPEC-001 §7.4).
|
|
42
|
+
Strips comments, removes leading/trailing line padding, and drops empty lines.
|
|
43
|
+
"""
|
|
44
|
+
lines = content.splitlines()
|
|
45
|
+
normalized_lines: list[str] = []
|
|
46
|
+
|
|
47
|
+
ext = extension.lower()
|
|
48
|
+
is_hash_comment = ext in {".py", ".sh", ".bash", ".yaml", ".yml", ".toml", ".ini"}
|
|
49
|
+
is_c_comment = ext in {".js", ".ts", ".c", ".cpp", ".h", ".java", ".go", ".rs"}
|
|
50
|
+
is_markup_comment = ext in {".html", ".xml", ".md"}
|
|
51
|
+
|
|
52
|
+
for raw_line in lines:
|
|
53
|
+
line = raw_line.strip()
|
|
54
|
+
if not line:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
# Strip full-line and inline '#' comments
|
|
58
|
+
if is_hash_comment:
|
|
59
|
+
if line.startswith("#"):
|
|
60
|
+
continue
|
|
61
|
+
if " #" in line:
|
|
62
|
+
line = line.split(" #", 1)[0].rstrip()
|
|
63
|
+
|
|
64
|
+
# Strip full-line and inline '//' comments
|
|
65
|
+
elif is_c_comment:
|
|
66
|
+
if line.startswith("//"):
|
|
67
|
+
continue
|
|
68
|
+
if " //" in line:
|
|
69
|
+
line = line.split(" //", 1)[0].rstrip()
|
|
70
|
+
|
|
71
|
+
# Strip single-line HTML/Markdown comments
|
|
72
|
+
elif is_markup_comment:
|
|
73
|
+
if line.startswith("<!--") and line.endswith("-->"):
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
if line:
|
|
77
|
+
normalized_lines.append(line)
|
|
78
|
+
|
|
79
|
+
return "\n".join(normalized_lines)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def calculate_normalized_sha256(file_path: Path) -> str:
|
|
83
|
+
"""
|
|
84
|
+
Calculates SHA-256 digest on normalized text for supported extensions.
|
|
85
|
+
Falls back to raw byte hashing for binary or non-UTF-8 assets.
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
89
|
+
content = f.read()
|
|
90
|
+
normalized = strip_comments_and_whitespace(content, file_path.suffix)
|
|
91
|
+
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
|
|
92
|
+
except (UnicodeDecodeError, PermissionError, OSError):
|
|
93
|
+
# Binary fixture fallback (PDF, PNG, etc.): hash raw bytes directly
|
|
94
|
+
return calculate_sha256(file_path)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def locate_evals_file(base_dir: Path, explicit_path: str | None = None) -> Path:
|
|
98
|
+
"""Finds evals.md in the root, .agent/, or at an explicit user path."""
|
|
99
|
+
if explicit_path:
|
|
100
|
+
p = Path(explicit_path)
|
|
101
|
+
if not p.is_absolute():
|
|
102
|
+
p = base_dir / p
|
|
103
|
+
return p
|
|
104
|
+
|
|
105
|
+
candidates = [
|
|
106
|
+
base_dir / "evals.md",
|
|
107
|
+
base_dir / ".agent" / "evals.md",
|
|
108
|
+
base_dir / "EVALS.md",
|
|
109
|
+
base_dir / ".agent" / "EVALS.md",
|
|
110
|
+
base_dir / "docs" / "evals.md",
|
|
111
|
+
]
|
|
112
|
+
for c in candidates:
|
|
113
|
+
if c.is_file():
|
|
114
|
+
return c
|
|
115
|
+
|
|
116
|
+
return base_dir / "evals.md"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
find_evals_file = locate_evals_file
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_evals_table(evals_input: str | Path) -> list[FixtureEntry]:
|
|
123
|
+
"""Parses markdown table entries from evals.md content or file path."""
|
|
124
|
+
if isinstance(evals_input, Path):
|
|
125
|
+
evals_content = evals_input.read_text(encoding="utf-8", errors="ignore")
|
|
126
|
+
elif isinstance(evals_input, str):
|
|
127
|
+
if "\n" not in evals_input and Path(evals_input).is_file():
|
|
128
|
+
evals_content = Path(evals_input).read_text(encoding="utf-8", errors="ignore")
|
|
129
|
+
else:
|
|
130
|
+
evals_content = evals_input
|
|
131
|
+
else:
|
|
132
|
+
evals_content = str(evals_input)
|
|
133
|
+
|
|
134
|
+
entries: list[FixtureEntry] = []
|
|
135
|
+
lines = evals_content.splitlines()
|
|
136
|
+
table_started = False
|
|
137
|
+
|
|
138
|
+
for idx, line in enumerate(lines, start=1):
|
|
139
|
+
stripped = line.strip()
|
|
140
|
+
if not stripped.startswith("|"):
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
lower = stripped.lower()
|
|
144
|
+
# Detect table headers (supports both "Fixture ID" and "Asset ID")
|
|
145
|
+
if ("fixture id" in lower or "asset id" in lower) and "path" in lower:
|
|
146
|
+
table_started = True
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
# Skip divider rows
|
|
150
|
+
if table_started and re.match(r"^\|(\s*:?-+:?\s*\|)+$", stripped):
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
if table_started:
|
|
154
|
+
cols = [c.strip().strip("`") for c in stripped.split("|")[1:-1]]
|
|
155
|
+
if len(cols) >= 3:
|
|
156
|
+
fixture_id = cols[0]
|
|
157
|
+
path_str = cols[1]
|
|
158
|
+
recorded_hash = cols[2]
|
|
159
|
+
status = cols[3] if len(cols) > 3 else "active"
|
|
160
|
+
|
|
161
|
+
# Skip repeated header, divider, or empty rows
|
|
162
|
+
if (
|
|
163
|
+
fixture_id.lower() in {"fixture id", "asset id", ":---", "---"}
|
|
164
|
+
or not fixture_id
|
|
165
|
+
or not path_str
|
|
166
|
+
):
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
entries.append(
|
|
170
|
+
FixtureEntry(
|
|
171
|
+
fixture_id=fixture_id,
|
|
172
|
+
path_str=path_str,
|
|
173
|
+
recorded_hash=recorded_hash,
|
|
174
|
+
status=status,
|
|
175
|
+
line_number=idx,
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return entries
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def run_audit(
|
|
183
|
+
evals_file: Path | None = None,
|
|
184
|
+
repo_root: Path | None = None,
|
|
185
|
+
update_pending: bool = False,
|
|
186
|
+
) -> bool:
|
|
187
|
+
"""
|
|
188
|
+
Audits ground truth references in evals.md against repository fixtures.
|
|
189
|
+
Returns True if audit passes with zero violations, False otherwise.
|
|
190
|
+
"""
|
|
191
|
+
# Normalize argument routing: supports run_audit(repo_root) or run_audit(evals_file, repo_root)
|
|
192
|
+
if evals_file is not None and evals_file.is_dir():
|
|
193
|
+
repo_root = evals_file
|
|
194
|
+
evals_file = locate_evals_file(repo_root)
|
|
195
|
+
elif repo_root is None:
|
|
196
|
+
if evals_file is not None and evals_file.is_file():
|
|
197
|
+
repo_root = evals_file.parent
|
|
198
|
+
else:
|
|
199
|
+
repo_root = Path.cwd()
|
|
200
|
+
evals_file = locate_evals_file(repo_root)
|
|
201
|
+
elif evals_file is None:
|
|
202
|
+
evals_file = locate_evals_file(repo_root)
|
|
203
|
+
|
|
204
|
+
print("====================================================================")
|
|
205
|
+
print(" SDCS :: Corpus Integrity & Diversity Audit (SPEC-001 v1.3.0)")
|
|
206
|
+
print(f" Spec Target: {evals_file}")
|
|
207
|
+
print(f" Working Dir: {repo_root}")
|
|
208
|
+
print(" Normalizer: Whitespace & Comment Invariant Filter (Active)")
|
|
209
|
+
print("====================================================================\n")
|
|
210
|
+
|
|
211
|
+
if not evals_file.is_file():
|
|
212
|
+
print(f"[FATAL] evals.md file not found at: {evals_file}", file=sys.stderr)
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
entries = parse_evals_table(evals_file.read_text(encoding="utf-8", errors="ignore"))
|
|
216
|
+
if not entries:
|
|
217
|
+
print("[WARN] No fixture rows found in evals.md table.")
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
seen_hashes: dict[str, list[str]] = {}
|
|
221
|
+
pending_entries: list[tuple[FixtureEntry, str]] = []
|
|
222
|
+
failures = 0
|
|
223
|
+
|
|
224
|
+
print(f"Found {len(entries)} fixture entries. Executing verification...\n")
|
|
225
|
+
|
|
226
|
+
for entry in entries:
|
|
227
|
+
fixture_path = repo_root / entry.path_str
|
|
228
|
+
|
|
229
|
+
# 1. Asset Reachability Gate
|
|
230
|
+
if not fixture_path.exists():
|
|
231
|
+
print(f" [REACHABILITY FAIL] Fixture '{entry.fixture_id}' not found on disk:")
|
|
232
|
+
print(f" Path: {fixture_path}")
|
|
233
|
+
failures += 1
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
computed_hash = calculate_normalized_sha256(fixture_path)
|
|
237
|
+
|
|
238
|
+
# Register for diversity verification
|
|
239
|
+
seen_hashes.setdefault(computed_hash, []).append(f"{entry.fixture_id} ({entry.path_str})")
|
|
240
|
+
|
|
241
|
+
# 2. Cryptographic Integrity Gate
|
|
242
|
+
if entry.recorded_hash.lower() in {"pending", "tbd", "todo", ""}:
|
|
243
|
+
pending_entries.append((entry, computed_hash))
|
|
244
|
+
print(f" · [PENDING] {entry.fixture_id:<12} => Computed: {computed_hash[:16]}...")
|
|
245
|
+
else:
|
|
246
|
+
rec_clean = entry.recorded_hash.strip().lower()
|
|
247
|
+
matches = computed_hash == rec_clean or (
|
|
248
|
+
len(rec_clean) in (8, 12, 16) and computed_hash.startswith(rec_clean)
|
|
249
|
+
)
|
|
250
|
+
if not matches:
|
|
251
|
+
print(f" [HASH DRIFT FAIL] {entry.fixture_id:<12} (line {entry.line_number})")
|
|
252
|
+
print(f" Expected: {entry.recorded_hash}")
|
|
253
|
+
print(f" Computed: {computed_hash}")
|
|
254
|
+
failures += 1
|
|
255
|
+
else:
|
|
256
|
+
print(f" ✓ [VERIFIED] {entry.fixture_id:<12} => {computed_hash[:16]}...")
|
|
257
|
+
|
|
258
|
+
# 3. Corpus Diversity Guard (Phantom Corpus Trap Detection)
|
|
259
|
+
print("\n--------------------------------------------------------------------")
|
|
260
|
+
print(" Evaluating Corpus Diversity Invariant (SHA-256 Collision Check)...")
|
|
261
|
+
diversity_violations = 0
|
|
262
|
+
for h, fixtures in seen_hashes.items():
|
|
263
|
+
if len(fixtures) > 1:
|
|
264
|
+
diversity_violations += 1
|
|
265
|
+
print("\n[PHANTOM CORPUS ERROR] Duplicate or trivially padded fixtures detected:")
|
|
266
|
+
print(f" Digest: {h}")
|
|
267
|
+
for f in fixtures:
|
|
268
|
+
print(f" - {f}")
|
|
269
|
+
|
|
270
|
+
if diversity_violations > 0:
|
|
271
|
+
print("\nSPEC-001 Violation: Golden fixtures must represent distinct test cases.")
|
|
272
|
+
failures += diversity_violations
|
|
273
|
+
|
|
274
|
+
# 4. Handle Pending Records
|
|
275
|
+
if pending_entries:
|
|
276
|
+
print(f"\nDiscovered {len(pending_entries)} 'pending' fixtures.")
|
|
277
|
+
if update_pending:
|
|
278
|
+
print("Writing computed hashes to evals.md...")
|
|
279
|
+
content = evals_file.read_text(encoding="utf-8")
|
|
280
|
+
for entry, computed in pending_entries:
|
|
281
|
+
pattern = rf"(\|\s*`?{re.escape(entry.fixture_id)}`?\s*\|\s*`?{re.escape(entry.path_str)}`?\s*\|\s*)`?pending`?(\s*\|)"
|
|
282
|
+
content = re.sub(pattern, rf"\g<1>{computed}\g<2>", content, flags=re.IGNORECASE)
|
|
283
|
+
evals_file.write_text(content, encoding="utf-8")
|
|
284
|
+
print("Successfully updated evals.md with computed SHA-256 digests.")
|
|
285
|
+
else:
|
|
286
|
+
print("Run with '--update-pending' to populate these hashes automatically.")
|
|
287
|
+
|
|
288
|
+
print("\n====================================================================")
|
|
289
|
+
if failures == 0:
|
|
290
|
+
print(" [STATUS: PASSED] All fixtures reachable, verified, and diversified.")
|
|
291
|
+
print("====================================================================")
|
|
292
|
+
return True
|
|
293
|
+
else:
|
|
294
|
+
print(f" [STATUS: FAILED] Audit halted with {failures} error(s).")
|
|
295
|
+
print("====================================================================")
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def main():
|
|
300
|
+
parser = argparse.ArgumentParser(
|
|
301
|
+
description="Audit evals.md fixture integrity, anti-evasion normalization, and corpus diversity."
|
|
302
|
+
)
|
|
303
|
+
parser.add_argument("--evals-path", default=None, help="Path to evals.md file")
|
|
304
|
+
parser.add_argument(
|
|
305
|
+
"--repo-root",
|
|
306
|
+
default=".",
|
|
307
|
+
help="Root repository directory (default: current directory)",
|
|
308
|
+
)
|
|
309
|
+
parser.add_argument(
|
|
310
|
+
"--update-pending",
|
|
311
|
+
action="store_true",
|
|
312
|
+
help="Automatically replace 'pending' entries in evals.md with computed hashes",
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
args = parser.parse_args()
|
|
316
|
+
repo_root = Path(args.repo_root).resolve()
|
|
317
|
+
evals_file = locate_evals_file(repo_root, args.evals_path)
|
|
318
|
+
|
|
319
|
+
success = run_audit(
|
|
320
|
+
evals_file=evals_file, repo_root=repo_root, update_pending=args.update_pending
|
|
321
|
+
)
|
|
322
|
+
sys.exit(0 if success else 1)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
if __name__ == "__main__":
|
|
326
|
+
main()
|
sdcs/cli.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sdcs.cli - Unified CLI Router for Spec-Driven Cognitive Scaffolding (SPEC-001 v1.2)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from sdcs import __version__
|
|
10
|
+
from sdcs.audit import locate_evals_file, run_audit
|
|
11
|
+
from sdcs.init import init_scaffold
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
prog="sdcs",
|
|
17
|
+
description=f"Spec-Driven Cognitive Scaffolding (SDCS v{__version__}) CLI",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--version",
|
|
21
|
+
action="version",
|
|
22
|
+
version=f"sdcs {__version__} (SPEC-001 v1.2)",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
|
|
26
|
+
|
|
27
|
+
# Subcommand: init
|
|
28
|
+
init_parser = subparsers.add_parser(
|
|
29
|
+
"init",
|
|
30
|
+
help="Initialize 7-pillar SDCS cognitive scaffolding in a repository",
|
|
31
|
+
)
|
|
32
|
+
init_parser.add_argument(
|
|
33
|
+
"--target-dir",
|
|
34
|
+
type=Path,
|
|
35
|
+
default=Path("."),
|
|
36
|
+
help="Target repository root path (default: current directory)",
|
|
37
|
+
)
|
|
38
|
+
init_parser.add_argument(
|
|
39
|
+
"--use-agent-dir",
|
|
40
|
+
action="store_true",
|
|
41
|
+
help="Store scaffolding files inside a '.agent/' subdirectory instead of root",
|
|
42
|
+
)
|
|
43
|
+
init_parser.add_argument(
|
|
44
|
+
"--hierarchical",
|
|
45
|
+
action="store_true",
|
|
46
|
+
help="Generate hierarchical multi-tiered cartography maps",
|
|
47
|
+
)
|
|
48
|
+
init_parser.add_argument(
|
|
49
|
+
"--skip-agents-md",
|
|
50
|
+
action="store_true",
|
|
51
|
+
help="Skip generating AGENTS.md behavioral prompting file",
|
|
52
|
+
)
|
|
53
|
+
init_parser.add_argument(
|
|
54
|
+
"--skip-hooks",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Skip generating .githooks/pre-commit protection hook",
|
|
57
|
+
)
|
|
58
|
+
init_parser.add_argument(
|
|
59
|
+
"--force",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="Overwrite existing scaffolding files if present",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Subcommand: audit
|
|
65
|
+
audit_parser = subparsers.add_parser(
|
|
66
|
+
"audit",
|
|
67
|
+
help="Audit ground truth references in evals.md for reachability, hashes, and diversity",
|
|
68
|
+
)
|
|
69
|
+
audit_parser.add_argument(
|
|
70
|
+
"--evals-path",
|
|
71
|
+
default=None,
|
|
72
|
+
help="Path to evals.md file",
|
|
73
|
+
)
|
|
74
|
+
audit_parser.add_argument(
|
|
75
|
+
"--repo-root",
|
|
76
|
+
type=Path,
|
|
77
|
+
default=Path("."),
|
|
78
|
+
help="Path to repository root (default: current directory)",
|
|
79
|
+
)
|
|
80
|
+
audit_parser.add_argument(
|
|
81
|
+
"--update-pending",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help="Automatically replace 'pending' entries in evals.md with computed hashes",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Subcommand: grill
|
|
87
|
+
grill_parser = subparsers.add_parser(
|
|
88
|
+
"grill",
|
|
89
|
+
help="Display the /grillme adversarial spec elicitation prompt for authoring roadmap.md",
|
|
90
|
+
)
|
|
91
|
+
grill_parser.add_argument(
|
|
92
|
+
"--milestone",
|
|
93
|
+
type=str,
|
|
94
|
+
default=None,
|
|
95
|
+
help="Target milestone ID (e.g. M-001) to contextualize prompt",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Subcommand: verify
|
|
99
|
+
verify_parser = subparsers.add_parser(
|
|
100
|
+
"verify",
|
|
101
|
+
help="Run specification, invariant, and topological verification checks",
|
|
102
|
+
)
|
|
103
|
+
verify_parser.add_argument(
|
|
104
|
+
"--topology",
|
|
105
|
+
action="store_true",
|
|
106
|
+
help="Audit codebase AST against wiring.yaml boundary contracts",
|
|
107
|
+
)
|
|
108
|
+
verify_parser.add_argument(
|
|
109
|
+
"--append-rejections",
|
|
110
|
+
action="store_true",
|
|
111
|
+
help="Automatically persist unique boundary violations into decisions.md",
|
|
112
|
+
)
|
|
113
|
+
verify_parser.add_argument(
|
|
114
|
+
"--wiring-path",
|
|
115
|
+
type=Path,
|
|
116
|
+
default=None,
|
|
117
|
+
help="Path to wiring.yaml file (default: auto-detect)",
|
|
118
|
+
)
|
|
119
|
+
verify_parser.add_argument(
|
|
120
|
+
"--repo-root",
|
|
121
|
+
type=Path,
|
|
122
|
+
default=Path("."),
|
|
123
|
+
help="Path to repository root (default: current directory)",
|
|
124
|
+
)
|
|
125
|
+
verify_parser.add_argument(
|
|
126
|
+
"--all",
|
|
127
|
+
action="store_true",
|
|
128
|
+
help="Execute all verification checks (topology and evals)",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
args = parser.parse_args()
|
|
132
|
+
|
|
133
|
+
if args.command == "init":
|
|
134
|
+
init_scaffold(
|
|
135
|
+
target_dir=args.target_dir,
|
|
136
|
+
use_agent_dir=args.use_agent_dir,
|
|
137
|
+
hierarchical=args.hierarchical,
|
|
138
|
+
skip_agents_md=args.skip_agents_md,
|
|
139
|
+
skip_hooks=args.skip_hooks,
|
|
140
|
+
force=args.force,
|
|
141
|
+
)
|
|
142
|
+
elif args.command == "audit":
|
|
143
|
+
repo_root = args.repo_root.resolve()
|
|
144
|
+
evals_file = locate_evals_file(repo_root, args.evals_path)
|
|
145
|
+
passed = run_audit(
|
|
146
|
+
evals_file=evals_file, repo_root=repo_root, update_pending=args.update_pending
|
|
147
|
+
)
|
|
148
|
+
sys.exit(0 if passed else 1)
|
|
149
|
+
elif args.command == "grill":
|
|
150
|
+
from sdcs.init import generate_grillme_md
|
|
151
|
+
|
|
152
|
+
print(generate_grillme_md(args.milestone))
|
|
153
|
+
elif args.command == "verify":
|
|
154
|
+
from sdcs.verifier.topology import run_topology_audit
|
|
155
|
+
|
|
156
|
+
repo_root = args.repo_root.resolve()
|
|
157
|
+
exit_code = 0
|
|
158
|
+
|
|
159
|
+
# Execute topology audit if requested, if --all is set, or as default verify action
|
|
160
|
+
if args.topology or args.all or not any([args.topology, args.all]):
|
|
161
|
+
code = run_topology_audit(
|
|
162
|
+
repo_root=repo_root,
|
|
163
|
+
wiring_path=args.wiring_path,
|
|
164
|
+
append_rejections=args.append_rejections,
|
|
165
|
+
)
|
|
166
|
+
if code != 0:
|
|
167
|
+
exit_code = code
|
|
168
|
+
|
|
169
|
+
# If --all is requested, also run the evals audit
|
|
170
|
+
if args.all:
|
|
171
|
+
evals_file = locate_evals_file(repo_root)
|
|
172
|
+
if evals_file and evals_file.is_file():
|
|
173
|
+
evals_passed = run_audit(evals_file=evals_file, repo_root=repo_root)
|
|
174
|
+
if not evals_passed:
|
|
175
|
+
exit_code = 1
|
|
176
|
+
|
|
177
|
+
sys.exit(exit_code)
|
|
178
|
+
else:
|
|
179
|
+
parser.print_help()
|
|
180
|
+
sys.exit(0)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
main()
|