specsmith 0.1.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.
- specsmith/__init__.py +5 -0
- specsmith/__main__.py +7 -0
- specsmith/auditor.py +470 -0
- specsmith/cli.py +592 -0
- specsmith/commands/__init__.py +3 -0
- specsmith/compressor.py +152 -0
- specsmith/config.py +269 -0
- specsmith/differ.py +81 -0
- specsmith/doctor.py +106 -0
- specsmith/exporter.py +150 -0
- specsmith/importer.py +536 -0
- specsmith/integrations/__init__.py +53 -0
- specsmith/integrations/aider.py +38 -0
- specsmith/integrations/base.py +31 -0
- specsmith/integrations/claude_code.py +58 -0
- specsmith/integrations/copilot.py +55 -0
- specsmith/integrations/cursor.py +62 -0
- specsmith/integrations/gemini.py +49 -0
- specsmith/integrations/warp.py +70 -0
- specsmith/integrations/windsurf.py +46 -0
- specsmith/scaffolder.py +372 -0
- specsmith/templates/agents.md.j2 +159 -0
- specsmith/templates/docs/architecture.md.j2 +39 -0
- specsmith/templates/docs/requirements.md.j2 +130 -0
- specsmith/templates/docs/test-spec.md.j2 +42 -0
- specsmith/templates/docs/workflow.md.j2 +15 -0
- specsmith/templates/gitattributes.j2 +15 -0
- specsmith/templates/gitignore.j2 +119 -0
- specsmith/templates/governance/context-budget.md.j2 +50 -0
- specsmith/templates/governance/drift-metrics.md.j2 +54 -0
- specsmith/templates/governance/roles.md.j2 +30 -0
- specsmith/templates/governance/rules.md.j2 +49 -0
- specsmith/templates/governance/verification.md.j2 +59 -0
- specsmith/templates/governance/workflow.md.j2 +80 -0
- specsmith/templates/ledger.md.j2 +27 -0
- specsmith/templates/pyproject.toml.j2 +18 -0
- specsmith/templates/python/cli.py.j2 +13 -0
- specsmith/templates/python/init.py.j2 +3 -0
- specsmith/templates/readme.md.j2 +30 -0
- specsmith/templates/scripts/exec.cmd.j2 +33 -0
- specsmith/templates/scripts/exec.sh.j2 +38 -0
- specsmith/templates/scripts/run.cmd.j2 +14 -0
- specsmith/templates/scripts/run.sh.j2 +7 -0
- specsmith/templates/scripts/setup.cmd.j2 +23 -0
- specsmith/templates/scripts/setup.sh.j2 +12 -0
- specsmith/tools.py +410 -0
- specsmith/upgrader.py +118 -0
- specsmith/validator.py +218 -0
- specsmith/vcs/__init__.py +41 -0
- specsmith/vcs/base.py +122 -0
- specsmith/vcs/bitbucket.py +135 -0
- specsmith/vcs/github.py +261 -0
- specsmith/vcs/gitlab.py +142 -0
- specsmith-0.1.0.dist-info/METADATA +137 -0
- specsmith-0.1.0.dist-info/RECORD +59 -0
- specsmith-0.1.0.dist-info/WHEEL +5 -0
- specsmith-0.1.0.dist-info/entry_points.txt +2 -0
- specsmith-0.1.0.dist-info/licenses/LICENSE +21 -0
- specsmith-0.1.0.dist-info/top_level.txt +1 -0
specsmith/__init__.py
ADDED
specsmith/__main__.py
ADDED
specsmith/auditor.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Auditor — drift detection and health checks (Spec Sections 23 + 26)."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AuditResult:
|
|
18
|
+
"""Result of a single audit check."""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
passed: bool
|
|
22
|
+
message: str
|
|
23
|
+
fixable: bool = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class AuditReport:
|
|
28
|
+
"""Aggregate audit report."""
|
|
29
|
+
|
|
30
|
+
results: list[AuditResult] = field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def passed(self) -> int:
|
|
34
|
+
return sum(1 for r in self.results if r.passed)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def failed(self) -> int:
|
|
38
|
+
return sum(1 for r in self.results if not r.passed)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def fixable(self) -> int:
|
|
42
|
+
return sum(1 for r in self.results if not r.passed and r.fixable)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def healthy(self) -> bool:
|
|
46
|
+
return self.failed == 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Governance file existence checks
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
REQUIRED_FILES = [
|
|
54
|
+
"AGENTS.md",
|
|
55
|
+
"LEDGER.md",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
GOVERNANCE_FILES = [
|
|
59
|
+
"docs/governance/rules.md",
|
|
60
|
+
"docs/governance/workflow.md",
|
|
61
|
+
"docs/governance/roles.md",
|
|
62
|
+
"docs/governance/context-budget.md",
|
|
63
|
+
"docs/governance/verification.md",
|
|
64
|
+
"docs/governance/drift-metrics.md",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
RECOMMENDED_FILES = [
|
|
68
|
+
"docs/REQUIREMENTS.md",
|
|
69
|
+
"docs/TEST_SPEC.md",
|
|
70
|
+
"docs/architecture.md",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_governance_files(root: Path) -> list[AuditResult]:
|
|
75
|
+
"""Check that all required governance files exist."""
|
|
76
|
+
results: list[AuditResult] = []
|
|
77
|
+
|
|
78
|
+
for f in REQUIRED_FILES:
|
|
79
|
+
path = root / f
|
|
80
|
+
results.append(
|
|
81
|
+
AuditResult(
|
|
82
|
+
name=f"file-exists:{f}",
|
|
83
|
+
passed=path.exists(),
|
|
84
|
+
message=f"Required file {f} {'exists' if path.exists() else 'MISSING'}",
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Modular governance: either all exist or AGENTS.md is self-contained (>200 lines)
|
|
89
|
+
agents_path = root / "AGENTS.md"
|
|
90
|
+
agents_lines = 0
|
|
91
|
+
if agents_path.exists():
|
|
92
|
+
agents_lines = len(agents_path.read_text(encoding="utf-8").splitlines())
|
|
93
|
+
|
|
94
|
+
if agents_lines > 200:
|
|
95
|
+
# Modular governance is REQUIRED
|
|
96
|
+
for f in GOVERNANCE_FILES:
|
|
97
|
+
path = root / f
|
|
98
|
+
results.append(
|
|
99
|
+
AuditResult(
|
|
100
|
+
name=f"file-exists:{f}",
|
|
101
|
+
passed=path.exists(),
|
|
102
|
+
message=(
|
|
103
|
+
f"Governance file {f} {'exists' if path.exists() else 'MISSING'}"
|
|
104
|
+
f" (AGENTS.md is {agents_lines} lines — modular split required)"
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
# Recommended but not required
|
|
110
|
+
for f in GOVERNANCE_FILES:
|
|
111
|
+
path = root / f
|
|
112
|
+
if path.exists():
|
|
113
|
+
results.append(
|
|
114
|
+
AuditResult(
|
|
115
|
+
name=f"file-exists:{f}",
|
|
116
|
+
passed=True,
|
|
117
|
+
message=f"Governance file {f} exists",
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
for f in RECOMMENDED_FILES:
|
|
122
|
+
path = root / f
|
|
123
|
+
results.append(
|
|
124
|
+
AuditResult(
|
|
125
|
+
name=f"recommended:{f}",
|
|
126
|
+
passed=path.exists(),
|
|
127
|
+
message=f"Recommended file {f} {'exists' if path.exists() else 'missing'}",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return results
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# Requirement ↔ Test consistency
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
_REQ_PATTERN = re.compile(r"\b(REQ-[A-Z]+-\d+)\b")
|
|
139
|
+
_TEST_PATTERN = re.compile(r"\b(TEST-[A-Z]+-\d+)\b")
|
|
140
|
+
_TEST_COVERS_PATTERN = re.compile(r"Covers:\s*(REQ-[A-Z]+-\d+(?:\s*,\s*REQ-[A-Z]+-\d+)*)")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def check_req_test_consistency(root: Path) -> list[AuditResult]:
|
|
144
|
+
"""Check that every REQ has at least one TEST and vice versa."""
|
|
145
|
+
results: list[AuditResult] = []
|
|
146
|
+
|
|
147
|
+
req_path = root / "docs" / "REQUIREMENTS.md"
|
|
148
|
+
test_path = root / "docs" / "TEST_SPEC.md"
|
|
149
|
+
|
|
150
|
+
if not req_path.exists() or not test_path.exists():
|
|
151
|
+
results.append(
|
|
152
|
+
AuditResult(
|
|
153
|
+
name="req-test-consistency",
|
|
154
|
+
passed=True,
|
|
155
|
+
message="Skipped: REQUIREMENTS.md or TEST_SPEC.md not found",
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
return results
|
|
159
|
+
|
|
160
|
+
req_text = req_path.read_text(encoding="utf-8")
|
|
161
|
+
test_text = test_path.read_text(encoding="utf-8")
|
|
162
|
+
|
|
163
|
+
req_ids = set(_REQ_PATTERN.findall(req_text))
|
|
164
|
+
|
|
165
|
+
# Find which REQs are covered by tests
|
|
166
|
+
covered_reqs: set[str] = set()
|
|
167
|
+
for match in _TEST_COVERS_PATTERN.finditer(test_text):
|
|
168
|
+
for req_id in _REQ_PATTERN.findall(match.group(0)):
|
|
169
|
+
covered_reqs.add(req_id)
|
|
170
|
+
|
|
171
|
+
uncovered = req_ids - covered_reqs
|
|
172
|
+
if uncovered:
|
|
173
|
+
results.append(
|
|
174
|
+
AuditResult(
|
|
175
|
+
name="req-test-coverage",
|
|
176
|
+
passed=False,
|
|
177
|
+
message=(
|
|
178
|
+
f"{len(uncovered)} REQ(s) without test coverage: {', '.join(sorted(uncovered))}"
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
results.append(
|
|
184
|
+
AuditResult(
|
|
185
|
+
name="req-test-coverage",
|
|
186
|
+
passed=True,
|
|
187
|
+
message=f"All {len(req_ids)} REQ(s) have test coverage",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Orphaned tests
|
|
192
|
+
orphaned = covered_reqs - req_ids
|
|
193
|
+
if orphaned:
|
|
194
|
+
results.append(
|
|
195
|
+
AuditResult(
|
|
196
|
+
name="orphaned-tests",
|
|
197
|
+
passed=False,
|
|
198
|
+
message=(
|
|
199
|
+
f"{len(orphaned)} TEST(s) reference non-existent REQ(s): "
|
|
200
|
+
f"{', '.join(sorted(orphaned))}"
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return results
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Ledger health
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def check_ledger_health(root: Path) -> list[AuditResult]:
|
|
214
|
+
"""Check ledger quality and staleness."""
|
|
215
|
+
results: list[AuditResult] = []
|
|
216
|
+
ledger_path = root / "LEDGER.md"
|
|
217
|
+
|
|
218
|
+
if not ledger_path.exists():
|
|
219
|
+
results.append(
|
|
220
|
+
AuditResult(
|
|
221
|
+
name="ledger-exists",
|
|
222
|
+
passed=False,
|
|
223
|
+
message="LEDGER.md not found",
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
return results
|
|
227
|
+
|
|
228
|
+
text = ledger_path.read_text(encoding="utf-8")
|
|
229
|
+
lines = text.splitlines()
|
|
230
|
+
line_count = len(lines)
|
|
231
|
+
|
|
232
|
+
# Size check
|
|
233
|
+
if line_count > 500:
|
|
234
|
+
results.append(
|
|
235
|
+
AuditResult(
|
|
236
|
+
name="ledger-size",
|
|
237
|
+
passed=False,
|
|
238
|
+
message=(
|
|
239
|
+
f"LEDGER.md has {line_count} lines (threshold: 500). "
|
|
240
|
+
f"Consider `specsmith compress`."
|
|
241
|
+
),
|
|
242
|
+
fixable=True,
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
results.append(
|
|
247
|
+
AuditResult(
|
|
248
|
+
name="ledger-size",
|
|
249
|
+
passed=True,
|
|
250
|
+
message=f"LEDGER.md has {line_count} lines (within threshold)",
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Open TODOs
|
|
255
|
+
open_todos = sum(1 for line in lines if "- [ ]" in line)
|
|
256
|
+
closed_todos = sum(1 for line in lines if "- [x]" in line)
|
|
257
|
+
if open_todos > 20:
|
|
258
|
+
results.append(
|
|
259
|
+
AuditResult(
|
|
260
|
+
name="ledger-open-todos",
|
|
261
|
+
passed=False,
|
|
262
|
+
message=f"{open_todos} open TODOs in ledger (may indicate stale items)",
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
results.append(
|
|
267
|
+
AuditResult(
|
|
268
|
+
name="ledger-open-todos",
|
|
269
|
+
passed=True,
|
|
270
|
+
message=f"{open_todos} open, {closed_todos} closed TODOs",
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return results
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
# Context size (governance bloat detection)
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def check_context_size(root: Path) -> list[AuditResult]:
|
|
283
|
+
"""Check governance file sizes against thresholds."""
|
|
284
|
+
results: list[AuditResult] = []
|
|
285
|
+
thresholds = {
|
|
286
|
+
"AGENTS.md": 200,
|
|
287
|
+
"docs/governance/rules.md": 300,
|
|
288
|
+
"docs/governance/workflow.md": 300,
|
|
289
|
+
"docs/governance/roles.md": 200,
|
|
290
|
+
"docs/governance/context-budget.md": 200,
|
|
291
|
+
"docs/governance/verification.md": 200,
|
|
292
|
+
"docs/governance/drift-metrics.md": 200,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for rel_path, max_lines in thresholds.items():
|
|
296
|
+
path = root / rel_path
|
|
297
|
+
if not path.exists():
|
|
298
|
+
continue
|
|
299
|
+
line_count = len(path.read_text(encoding="utf-8").splitlines())
|
|
300
|
+
ok = line_count <= max_lines
|
|
301
|
+
results.append(
|
|
302
|
+
AuditResult(
|
|
303
|
+
name=f"context-size:{rel_path}",
|
|
304
|
+
passed=ok,
|
|
305
|
+
message=(
|
|
306
|
+
f"{rel_path}: {line_count} lines"
|
|
307
|
+
+ ("" if ok else f" (exceeds {max_lines} threshold)")
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return results
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
# Public API
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def check_tool_configuration(root: Path) -> list[AuditResult]:
|
|
321
|
+
"""Check that CI config references the expected verification tools."""
|
|
322
|
+
results: list[AuditResult] = []
|
|
323
|
+
scaffold_path = root / "scaffold.yml"
|
|
324
|
+
if not scaffold_path.exists():
|
|
325
|
+
return results # Not a specsmith project — skip silently
|
|
326
|
+
|
|
327
|
+
import yaml
|
|
328
|
+
|
|
329
|
+
from specsmith.config import ProjectConfig
|
|
330
|
+
from specsmith.tools import get_tools
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
with open(scaffold_path) as f:
|
|
334
|
+
raw = yaml.safe_load(f)
|
|
335
|
+
config = ProjectConfig(**raw)
|
|
336
|
+
except Exception: # noqa: BLE001
|
|
337
|
+
return results # Malformed config — other checks will catch this
|
|
338
|
+
|
|
339
|
+
tools = get_tools(config)
|
|
340
|
+
if not any([tools.lint, tools.typecheck, tools.test, tools.security]):
|
|
341
|
+
return results # No tools registered for this type
|
|
342
|
+
|
|
343
|
+
# Check CI configs for expected tool references
|
|
344
|
+
ci_files = list(root.glob(".github/workflows/*.yml")) + [
|
|
345
|
+
root / ".gitlab-ci.yml",
|
|
346
|
+
root / "bitbucket-pipelines.yml",
|
|
347
|
+
]
|
|
348
|
+
ci_content = ""
|
|
349
|
+
for ci_file in ci_files:
|
|
350
|
+
if ci_file.exists():
|
|
351
|
+
ci_content += ci_file.read_text(encoding="utf-8")
|
|
352
|
+
|
|
353
|
+
if not ci_content:
|
|
354
|
+
results.append(
|
|
355
|
+
AuditResult(
|
|
356
|
+
name="tool-ci-config",
|
|
357
|
+
passed=True,
|
|
358
|
+
message="No CI config found — tool verification skipped",
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
return results
|
|
362
|
+
|
|
363
|
+
missing: list[str] = []
|
|
364
|
+
# Check that at least the primary lint and test tools appear in CI
|
|
365
|
+
for cmd in tools.lint[:1]:
|
|
366
|
+
tool_name = cmd.split()[0] # e.g. "ruff" from "ruff check"
|
|
367
|
+
if tool_name not in ci_content:
|
|
368
|
+
missing.append(f"lint:{tool_name}")
|
|
369
|
+
for cmd in tools.test[:1]:
|
|
370
|
+
tool_name = cmd.split()[0]
|
|
371
|
+
if tool_name not in ci_content:
|
|
372
|
+
missing.append(f"test:{tool_name}")
|
|
373
|
+
|
|
374
|
+
if missing:
|
|
375
|
+
results.append(
|
|
376
|
+
AuditResult(
|
|
377
|
+
name="tool-ci-config",
|
|
378
|
+
passed=False,
|
|
379
|
+
message=f"CI config missing expected tools: {', '.join(missing)}",
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
else:
|
|
383
|
+
results.append(
|
|
384
|
+
AuditResult(
|
|
385
|
+
name="tool-ci-config",
|
|
386
|
+
passed=True,
|
|
387
|
+
message=f"CI config references expected verification tools for {config.type.value}",
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return results
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def run_audit(root: Path) -> AuditReport:
|
|
395
|
+
"""Run all audit checks and return a report."""
|
|
396
|
+
report = AuditReport()
|
|
397
|
+
report.results.extend(check_governance_files(root))
|
|
398
|
+
report.results.extend(check_req_test_consistency(root))
|
|
399
|
+
report.results.extend(check_ledger_health(root))
|
|
400
|
+
report.results.extend(check_context_size(root))
|
|
401
|
+
report.results.extend(check_tool_configuration(root))
|
|
402
|
+
return report
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def run_auto_fix(root: Path, report: AuditReport) -> list[str]:
|
|
406
|
+
"""Attempt to auto-fix issues found by audit.
|
|
407
|
+
|
|
408
|
+
Returns list of human-readable fix descriptions.
|
|
409
|
+
"""
|
|
410
|
+
fixed: list[str] = []
|
|
411
|
+
|
|
412
|
+
for result in report.results:
|
|
413
|
+
if result.passed:
|
|
414
|
+
continue
|
|
415
|
+
|
|
416
|
+
# Fix missing required files with minimal stubs
|
|
417
|
+
if result.name == "file-exists:AGENTS.md":
|
|
418
|
+
path = root / "AGENTS.md"
|
|
419
|
+
path.write_text(
|
|
420
|
+
"# AGENTS.md\n\nGovernance hub. Populate with project details.\n",
|
|
421
|
+
encoding="utf-8",
|
|
422
|
+
)
|
|
423
|
+
fixed.append("Created stub AGENTS.md")
|
|
424
|
+
|
|
425
|
+
elif result.name == "file-exists:LEDGER.md":
|
|
426
|
+
path = root / "LEDGER.md"
|
|
427
|
+
path.write_text("# Ledger\n\nNo entries yet.\n", encoding="utf-8")
|
|
428
|
+
fixed.append("Created stub LEDGER.md")
|
|
429
|
+
|
|
430
|
+
elif result.name.startswith("file-exists:docs/governance/"):
|
|
431
|
+
rel = result.name.split(":", 1)[1]
|
|
432
|
+
path = root / rel
|
|
433
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
434
|
+
fname = path.stem.replace("-", " ").title()
|
|
435
|
+
path.write_text(f"# {fname}\n\nPopulate per spec.\n", encoding="utf-8")
|
|
436
|
+
fixed.append(f"Created stub {rel}")
|
|
437
|
+
|
|
438
|
+
# Compress oversized ledger
|
|
439
|
+
elif result.name == "ledger-size" and result.fixable:
|
|
440
|
+
from specsmith.compressor import run_compress
|
|
441
|
+
|
|
442
|
+
compress_result = run_compress(root)
|
|
443
|
+
if compress_result.archived_entries > 0:
|
|
444
|
+
fixed.append(compress_result.message)
|
|
445
|
+
|
|
446
|
+
# Generate missing CI config from tool registry
|
|
447
|
+
elif result.name == "tool-ci-config" and not result.passed:
|
|
448
|
+
scaffold_path = root / "scaffold.yml"
|
|
449
|
+
if scaffold_path.exists():
|
|
450
|
+
import yaml
|
|
451
|
+
|
|
452
|
+
from specsmith.config import ProjectConfig
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
with open(scaffold_path) as f:
|
|
456
|
+
raw = yaml.safe_load(f)
|
|
457
|
+
config = ProjectConfig(**raw)
|
|
458
|
+
if config.vcs_platform:
|
|
459
|
+
from specsmith.vcs import get_platform
|
|
460
|
+
|
|
461
|
+
platform = get_platform(config.vcs_platform)
|
|
462
|
+
platform.generate_all(config, root)
|
|
463
|
+
fixed.append(
|
|
464
|
+
f"Generated {config.vcs_platform} CI config "
|
|
465
|
+
f"with tools for {config.type.value}"
|
|
466
|
+
)
|
|
467
|
+
except Exception: # noqa: BLE001
|
|
468
|
+
pass # Best-effort
|
|
469
|
+
|
|
470
|
+
return fixed
|