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 ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ Spec-Driven Cognitive Scaffolding (SDCS) Framework
3
+ Conforms to SPEC-001 v1.3.
4
+ """
5
+
6
+ __version__ = "1.3.0"
7
+ __author__ = "Adam Murphy"
8
+ __license__ = "MIT"
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()