specsmith 0.1.4.dev26__tar.gz → 0.1.4.dev30__tar.gz

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 (83) hide show
  1. {specsmith-0.1.4.dev26/src/specsmith.egg-info → specsmith-0.1.4.dev30}/PKG-INFO +1 -1
  2. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/pyproject.toml +1 -1
  3. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/auditor.py +54 -10
  4. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/importer.py +134 -14
  5. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/scaffolder.py +1 -0
  6. specsmith-0.1.4.dev30/src/specsmith/templates/editorconfig.j2 +93 -0
  7. specsmith-0.1.4.dev30/src/specsmith/templates/gitattributes.j2 +146 -0
  8. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/gitignore.j2 +71 -1
  9. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/tools.py +22 -3
  10. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30/src/specsmith.egg-info}/PKG-INFO +1 -1
  11. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith.egg-info/SOURCES.txt +1 -0
  12. specsmith-0.1.4.dev26/src/specsmith/templates/gitattributes.j2 +0 -15
  13. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/LICENSE +0 -0
  14. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/README.md +0 -0
  15. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/setup.cfg +0 -0
  16. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/__init__.py +0 -0
  17. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/__main__.py +0 -0
  18. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/cli.py +0 -0
  19. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/commands/__init__.py +0 -0
  20. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/compressor.py +0 -0
  21. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/config.py +0 -0
  22. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/differ.py +0 -0
  23. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/doctor.py +0 -0
  24. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/exporter.py +0 -0
  25. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/integrations/__init__.py +0 -0
  26. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/integrations/aider.py +0 -0
  27. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/integrations/base.py +0 -0
  28. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/integrations/claude_code.py +0 -0
  29. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/integrations/copilot.py +0 -0
  30. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/integrations/cursor.py +0 -0
  31. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/integrations/gemini.py +0 -0
  32. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/integrations/warp.py +0 -0
  33. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/integrations/windsurf.py +0 -0
  34. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/ledger.py +0 -0
  35. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/plugins.py +0 -0
  36. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/releaser.py +0 -0
  37. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/requirements.py +0 -0
  38. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/session.py +0 -0
  39. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/agents.md.j2 +0 -0
  40. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
  41. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
  42. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
  43. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/docs/workflow.md.j2 +0 -0
  44. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
  45. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
  46. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/governance/roles.md.j2 +0 -0
  47. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/governance/rules.md.j2 +0 -0
  48. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/governance/verification.md.j2 +0 -0
  49. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/governance/workflow.md.j2 +0 -0
  50. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/ledger.md.j2 +0 -0
  51. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/pyproject.toml.j2 +0 -0
  52. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/python/cli.py.j2 +0 -0
  53. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/python/init.py.j2 +0 -0
  54. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/readme.md.j2 +0 -0
  55. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
  56. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
  57. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
  58. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
  59. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
  60. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
  61. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/updater.py +0 -0
  62. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/upgrader.py +0 -0
  63. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/validator.py +0 -0
  64. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/vcs/__init__.py +0 -0
  65. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/vcs/base.py +0 -0
  66. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/vcs/bitbucket.py +0 -0
  67. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/vcs/github.py +0 -0
  68. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/vcs/gitlab.py +0 -0
  69. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith/vcs_commands.py +0 -0
  70. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith.egg-info/dependency_links.txt +0 -0
  71. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith.egg-info/entry_points.txt +0 -0
  72. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith.egg-info/requires.txt +0 -0
  73. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/src/specsmith.egg-info/top_level.txt +0 -0
  74. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/tests/test_auditor.py +0 -0
  75. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/tests/test_cli.py +0 -0
  76. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/tests/test_compressor.py +0 -0
  77. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/tests/test_importer.py +0 -0
  78. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/tests/test_integrations.py +0 -0
  79. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/tests/test_scaffolder.py +0 -0
  80. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/tests/test_smoke.py +0 -0
  81. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/tests/test_tools.py +0 -0
  82. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/tests/test_validator.py +0 -0
  83. {specsmith-0.1.4.dev26 → specsmith-0.1.4.dev30}/tests/test_vcs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specsmith
3
- Version: 0.1.4.dev26
3
+ Version: 0.1.4.dev30
4
4
  Summary: Forge governed project scaffolds from the Agentic AI Development Workflow Specification.
5
5
  Author: BitConcepts
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "specsmith"
7
- version = "0.1.4.dev26"
7
+ version = "0.1.4.dev30"
8
8
  description = "Forge governed project scaffolds from the Agentic AI Development Workflow Specification."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -283,18 +283,62 @@ def check_ledger_health(root: Path) -> list[AuditResult]:
283
283
  # ---------------------------------------------------------------------------
284
284
 
285
285
 
286
+ # Default thresholds (used when no project type is detected)
287
+ _DEFAULT_THRESHOLDS: dict[str, int] = {
288
+ "AGENTS.md": 200,
289
+ "docs/governance/rules.md": 800,
290
+ "docs/governance/workflow.md": 400,
291
+ "docs/governance/roles.md": 300,
292
+ "docs/governance/context-budget.md": 300,
293
+ "docs/governance/verification.md": 400,
294
+ "docs/governance/drift-metrics.md": 300,
295
+ }
296
+
297
+ # Type-specific overrides — hardware/embedded projects have denser rules.
298
+ _TYPE_THRESHOLD_OVERRIDES: dict[str, dict[str, int]] = {
299
+ "fpga-rtl": {
300
+ "docs/governance/rules.md": 1000,
301
+ "docs/governance/workflow.md": 500,
302
+ "docs/governance/verification.md": 600,
303
+ },
304
+ "yocto-bsp": {
305
+ "docs/governance/rules.md": 1000,
306
+ "docs/governance/workflow.md": 500,
307
+ "docs/governance/verification.md": 500,
308
+ },
309
+ "embedded-hardware": {
310
+ "docs/governance/rules.md": 1000,
311
+ "docs/governance/verification.md": 500,
312
+ },
313
+ "pcb-hardware": {
314
+ "docs/governance/rules.md": 900,
315
+ "docs/governance/verification.md": 500,
316
+ },
317
+ }
318
+
319
+
320
+ def _get_thresholds(root: Path) -> dict[str, int]:
321
+ """Get governance size thresholds, scaled by project type if available."""
322
+ thresholds = dict(_DEFAULT_THRESHOLDS)
323
+ scaffold_path = root / "scaffold.yml"
324
+ if scaffold_path.exists():
325
+ try:
326
+ import yaml
327
+
328
+ with open(scaffold_path) as f:
329
+ raw = yaml.safe_load(f) or {}
330
+ ptype = raw.get("type", "")
331
+ overrides = _TYPE_THRESHOLD_OVERRIDES.get(ptype, {})
332
+ thresholds.update(overrides)
333
+ except Exception: # noqa: BLE001
334
+ pass # Use defaults on any error
335
+ return thresholds
336
+
337
+
286
338
  def check_context_size(root: Path) -> list[AuditResult]:
287
- """Check governance file sizes against thresholds."""
339
+ """Check governance file sizes against type-aware thresholds."""
288
340
  results: list[AuditResult] = []
289
- thresholds = {
290
- "AGENTS.md": 200,
291
- "docs/governance/rules.md": 300,
292
- "docs/governance/workflow.md": 300,
293
- "docs/governance/roles.md": 200,
294
- "docs/governance/context-budget.md": 200,
295
- "docs/governance/verification.md": 200,
296
- "docs/governance/drift-metrics.md": 200,
297
- }
341
+ thresholds = _get_thresholds(root)
298
342
 
299
343
  for rel_path, max_lines in thresholds.items():
300
344
  path = root / rel_path
@@ -38,6 +38,10 @@ _EXT_LANG: dict[str, str] = {
38
38
  ".yaml": "yaml",
39
39
  ".bb": "bitbake",
40
40
  ".bbappend": "bitbake",
41
+ ".bbclass": "bitbake",
42
+ ".inc": "bitbake",
43
+ ".dts": "devicetree",
44
+ ".dtsi": "devicetree",
41
45
  ".proto": "protobuf",
42
46
  ".graphql": "graphql",
43
47
  ".gql": "graphql",
@@ -64,6 +68,7 @@ _BUILD_SYSTEMS: dict[str, str] = {
64
68
  "build.gradle.kts": "gradle",
65
69
  "meson.build": "meson",
66
70
  "bitbake": "bitbake",
71
+ "kas.yml": "kas",
67
72
  "west.yml": "west",
68
73
  "pubspec.yaml": "flutter",
69
74
  "*.csproj": "dotnet",
@@ -629,11 +634,75 @@ _DRIFT_KW: list[str] = [
629
634
  ]
630
635
 
631
636
 
637
+ def _clean_diff_markers(text: str) -> str:
638
+ """Strip git diff/merge conflict artifacts from text.
639
+
640
+ Removes leading |-, |+, || prefixes and <<<<<<< / >>>>>>> markers.
641
+ """
642
+ import re
643
+
644
+ cleaned_lines: list[str] = []
645
+ skip_block = False
646
+ for line in text.splitlines():
647
+ # Skip merge conflict markers entirely
648
+ if line.startswith(("<<<<<<", ">>>>>>", "======")):
649
+ skip_block = not skip_block if line.startswith("<<<<<<") else skip_block
650
+ if line.startswith(">>>>>>>"):
651
+ skip_block = False
652
+ continue
653
+ if skip_block:
654
+ continue
655
+ # Strip diff marker prefixes: |-, |+, ||, leading +, leading -
656
+ stripped = re.sub(r"^\|{1,2}[-+]\s?", "", line)
657
+ stripped = re.sub(r"^[-+]\s(?=[A-Z])", "", stripped) # +/- before prose
658
+ cleaned_lines.append(stripped)
659
+ return "\n".join(cleaned_lines)
660
+
661
+
662
+ def _detect_content_issues(text: str) -> list[str]:
663
+ """Detect quality issues in source text. Returns list of warnings."""
664
+ import re
665
+
666
+ warnings: list[str] = []
667
+ lines = text.splitlines()
668
+ diff_marker_count = 0
669
+ for i, line in enumerate(lines, 1):
670
+ # Diff markers
671
+ if re.match(r"^\|{1,2}[-+]", line):
672
+ diff_marker_count += 1
673
+ # Merge conflict markers
674
+ if line.startswith(("<<<<<<", ">>>>>>")):
675
+ warnings.append(f" Line {i}: unresolved merge conflict marker")
676
+ if diff_marker_count > 0:
677
+ warnings.append(
678
+ f" {diff_marker_count} line(s) with git diff markers (|-, |+) — auto-stripped"
679
+ )
680
+ return warnings
681
+
682
+
683
+ def _deduplicate_paragraphs(text: str) -> str:
684
+ """Remove duplicate paragraphs within a text block."""
685
+ paragraphs = text.split("\n\n")
686
+ seen: set[str] = set()
687
+ unique: list[str] = []
688
+ for para in paragraphs:
689
+ normalized = para.strip()
690
+ if not normalized:
691
+ continue
692
+ # Use first 200 chars as dedup key (handles minor formatting diffs)
693
+ key = normalized[:200].lower()
694
+ if key not in seen:
695
+ seen.add(key)
696
+ unique.append(para)
697
+ return "\n\n".join(unique)
698
+
699
+
632
700
  def _extract_governance_sections(root: Path) -> dict[str, str]:
633
701
  """Extract modular governance content from existing AGENTS.md.
634
702
 
635
703
  If AGENTS.md exists and is large, extract sections into modular files.
636
704
  Unmatched sections are collected into rules.md so nothing is lost.
705
+ Diff markers are stripped and duplicate paragraphs are removed.
637
706
  """
638
707
  defaults = {
639
708
  "rules": (
@@ -663,10 +732,21 @@ def _extract_governance_sections(root: Path) -> dict[str, str]:
663
732
  if not agents_path.exists():
664
733
  return defaults
665
734
 
666
- content = agents_path.read_text(encoding="utf-8")
667
- if len(content.splitlines()) < 50:
735
+ raw_content = agents_path.read_text(encoding="utf-8")
736
+ if len(raw_content.splitlines()) < 50:
668
737
  return defaults # Too short to extract from
669
738
 
739
+ # P0: Detect and report content issues, then clean diff markers
740
+ issues = _detect_content_issues(raw_content)
741
+ if issues:
742
+ import sys
743
+
744
+ print("\n[specsmith] Content quality warnings in AGENTS.md:", file=sys.stderr) # noqa: T201
745
+ for w in issues:
746
+ print(w, file=sys.stderr) # noqa: T201
747
+
748
+ content = _clean_diff_markers(raw_content)
749
+
670
750
  # Parse AGENTS.md into sections by ## headings
671
751
  sections: dict[str, str] = {}
672
752
  current_heading = ""
@@ -706,6 +786,20 @@ def _extract_governance_sections(root: Path) -> dict[str, str]:
706
786
  }
707
787
  unmatched: list[tuple[str, str]] = []
708
788
 
789
+ # Body-level content keywords for secondary classification.
790
+ # Used when heading doesn't match — scan body text for strong signals.
791
+ _BODY_ARCHITECTURE_KW = [
792
+ "register map", "address offset", "0x0", "register name",
793
+ "block diagram", "data flow", "interface spec",
794
+ "directory layout", "src/", "repository structure",
795
+ "milestone", "roadmap", "completion", "phase 2 target",
796
+ ]
797
+ _BODY_DRIFT_KW = [
798
+ "subst v:", "path-length", "one-time setup", "per-machine",
799
+ "environment variable", "install once", "bootstrap",
800
+ "windows path", "ntfs",
801
+ ]
802
+
709
803
  for heading, body in sections.items():
710
804
  key_lower = heading.lower()
711
805
  matched = False
@@ -715,7 +809,14 @@ def _extract_governance_sections(root: Path) -> dict[str, str]:
715
809
  matched = True
716
810
  break # First match wins
717
811
  if not matched:
718
- unmatched.append((heading, body))
812
+ # Secondary pass: scan body content for strong topic signals
813
+ body_lower = body[:2000].lower() # Cap scan for performance
814
+ if any(kw in body_lower for kw in _BODY_ARCHITECTURE_KW):
815
+ buckets["verification"].append((heading, body)) # technical reference
816
+ elif any(kw in body_lower for kw in _BODY_DRIFT_KW):
817
+ buckets["drift-metrics"].append((heading, body))
818
+ else:
819
+ unmatched.append((heading, body))
719
820
 
720
821
  # Unmatched sections go to rules.md as project-specific rules
721
822
  if unmatched:
@@ -739,7 +840,8 @@ def _extract_governance_sections(root: Path) -> dict[str, str]:
739
840
  parts.append(f"## {heading}\n")
740
841
  parts.append(body)
741
842
  parts.append("")
742
- result[category] = "\n".join(parts) + "\n"
843
+ # P1: Deduplicate paragraphs within each governance file
844
+ result[category] = _deduplicate_paragraphs("\n".join(parts)) + "\n"
743
845
 
744
846
  return result
745
847
 
@@ -752,7 +854,7 @@ def _infer_type(result: DetectionResult) -> ProjectType:
752
854
  # Hardware types
753
855
  if lang in ("vhdl", "verilog", "systemverilog"):
754
856
  return ProjectType.FPGA_RTL
755
- if lang == "bitbake" or build == "bitbake":
857
+ if lang == "bitbake" or build in ("bitbake", "kas"):
756
858
  return ProjectType.YOCTO_BSP
757
859
 
758
860
  # Language-specific
@@ -891,8 +993,14 @@ def generate_overlay(
891
993
  f"- Build system: {result.build_system}\n",
892
994
  )
893
995
 
894
- # docs/REQUIREMENTS.md
895
- reqs = "# Requirements\n\nRequirements auto-generated from project detection.\n\n"
996
+ # docs/REQUIREMENTS.md — skip if project already has one (anywhere under docs/)
997
+ existing_reqs = list(target.glob("docs/**/REQUIREMENTS*")) + list(
998
+ target.glob("docs/**/requirements*")
999
+ )
1000
+ if existing_reqs and not force:
1001
+ pass # Preserve existing requirements doc
1002
+ else:
1003
+ reqs = "# Requirements\n\nRequirements auto-generated from project detection.\n\n"
896
1004
  for module in result.modules:
897
1005
  mu = module.upper().replace(" ", "-")
898
1006
  reqs += (
@@ -908,10 +1016,16 @@ def generate_overlay(
908
1016
  "- **Status**: Draft\n"
909
1017
  f"- **Description**: Project builds successfully with {result.build_system}\n\n"
910
1018
  )
911
- _write("docs/REQUIREMENTS.md", reqs)
1019
+ _write("docs/REQUIREMENTS.md", reqs)
912
1020
 
913
- # docs/TEST_SPEC.md
914
- tests = "# Test Specification\n\nTests auto-generated from project detection.\n\n"
1021
+ # docs/TEST_SPEC.md — skip if project already has one
1022
+ existing_tests = list(target.glob("docs/**/TEST_SPEC*")) + list(
1023
+ target.glob("docs/**/test_spec*")
1024
+ )
1025
+ if existing_tests and not force:
1026
+ pass # Preserve existing test spec
1027
+ else:
1028
+ tests = "# Test Specification\n\nTests auto-generated from project detection.\n\n"
915
1029
  for i, test_file in enumerate(result.test_files[:20], 1):
916
1030
  tests += f"## TEST-{i:03d}\n- **File**: {test_file}\n- **Status**: Detected\n"
917
1031
  for module in result.modules:
@@ -919,10 +1033,16 @@ def generate_overlay(
919
1033
  tests += f"- **Requirement**: REQ-{module.upper()}-001\n"
920
1034
  break
921
1035
  tests += "\n"
922
- _write("docs/TEST_SPEC.md", tests)
1036
+ _write("docs/TEST_SPEC.md", tests)
923
1037
 
924
- # docs/architecture.md
925
- arch = (
1038
+ # docs/architecture.md — skip if project has architecture doc anywhere under docs/
1039
+ existing_arch = list(target.glob("docs/**/architecture*")) + list(
1040
+ target.glob("docs/**/ARCHITECTURE*")
1041
+ )
1042
+ if existing_arch and not force:
1043
+ pass # Preserve existing architecture doc
1044
+ else:
1045
+ arch = (
926
1046
  f"# Architecture — {name}\n\n"
927
1047
  "Architecture auto-generated from project detection.\n\n"
928
1048
  "## Overview\n"
@@ -944,7 +1064,7 @@ def generate_overlay(
944
1064
  arch += "## Language Distribution\n"
945
1065
  for lang_name, count in sorted(result.languages.items(), key=lambda x: -x[1]):
946
1066
  arch += f"- {lang_name}: {count} files\n"
947
- _write("docs/architecture.md", arch)
1067
+ _write("docs/architecture.md", arch)
948
1068
 
949
1069
  # --- Modular governance files ---
950
1070
  # If AGENTS.md exists and is rich, extract sections from it.
@@ -100,6 +100,7 @@ def _build_file_map(config: ProjectConfig) -> list[tuple[str, str]]:
100
100
  ("readme.md.j2", "README.md"),
101
101
  ("gitignore.j2", ".gitignore"),
102
102
  ("gitattributes.j2", ".gitattributes"),
103
+ ("editorconfig.j2", ".editorconfig"),
103
104
  # Modular governance
104
105
  ("governance/rules.md.j2", "docs/governance/rules.md"),
105
106
  ("governance/workflow.md.j2", "docs/governance/workflow.md"),
@@ -0,0 +1,93 @@
1
+ # EditorConfig — https://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ end_of_line = lf
7
+ insert_final_newline = true
8
+ trim_trailing_whitespace = true
9
+ indent_style = space
10
+ indent_size = 4
11
+ {% if project.type.value in ('cli-python', 'library-python', 'backend-frontend', 'backend-frontend-tray', 'data-ml') %}
12
+
13
+ [*.py]
14
+ indent_size = 4
15
+ max_line_length = 100
16
+ {% endif %}
17
+ {% if project.type.value in ('cli-rust', 'library-rust') %}
18
+
19
+ [*.rs]
20
+ indent_size = 4
21
+ max_line_length = 100
22
+ {% endif %}
23
+ {% if project.type.value in ('cli-go',) %}
24
+
25
+ [*.go]
26
+ indent_style = tab
27
+ indent_size = 4
28
+ {% endif %}
29
+ {% if project.type.value in ('cli-c', 'library-c', 'embedded-hardware') %}
30
+
31
+ [*.{c,h,cpp,hpp}]
32
+ indent_size = 4
33
+ {% endif %}
34
+ {% if project.type.value in ('web-frontend', 'fullstack-js', 'browser-extension', 'monorepo') %}
35
+
36
+ [*.{js,ts,jsx,tsx,css,scss,html}]
37
+ indent_size = 2
38
+
39
+ [*.json]
40
+ indent_size = 2
41
+ {% endif %}
42
+ {% if project.type.value == 'dotnet-app' %}
43
+
44
+ [*.cs]
45
+ indent_size = 4
46
+
47
+ [*.csproj]
48
+ indent_size = 2
49
+ {% endif %}
50
+ {% if project.type.value == 'fpga-rtl' %}
51
+
52
+ [*.{vhd,vhdl}]
53
+ indent_size = 2
54
+
55
+ [*.{v,sv}]
56
+ indent_size = 2
57
+
58
+ [*.{xdc,sdc,tcl}]
59
+ indent_size = 2
60
+ {% endif %}
61
+ {% if project.type.value in ('yocto-bsp',) %}
62
+
63
+ [*.{bb,bbappend,bbclass,conf}]
64
+ indent_size = 4
65
+
66
+ [*.{dts,dtsi}]
67
+ indent_size = 4
68
+ {% endif %}
69
+ {% if project.type.value in ('research-paper',) %}
70
+
71
+ [*.{tex,bib}]
72
+ indent_size = 2
73
+ {% endif %}
74
+ {% if project.type.value in ('devops-iac',) %}
75
+
76
+ [*.{tf,hcl,tfvars}]
77
+ indent_size = 2
78
+ {% endif %}
79
+
80
+ [*.{yml,yaml}]
81
+ indent_size = 2
82
+
83
+ [*.md]
84
+ trim_trailing_whitespace = false
85
+
86
+ [Makefile]
87
+ indent_style = tab
88
+
89
+ [*.{cmd,bat}]
90
+ end_of_line = crlf
91
+
92
+ [*.{ps1,psm1}]
93
+ end_of_line = crlf
@@ -0,0 +1,146 @@
1
+ # Auto-detect text files and normalize line endings
2
+ * text=auto
3
+
4
+ # Common text files
5
+ *.md text eol=lf
6
+ *.txt text eol=lf
7
+ *.yml text eol=lf
8
+ *.yaml text eol=lf
9
+ *.json text eol=lf
10
+ *.toml text eol=lf
11
+ *.cfg text eol=lf
12
+ *.ini text eol=lf
13
+ *.xml text eol=lf
14
+ .gitignore text eol=lf
15
+ .gitattributes text eol=lf
16
+ .editorconfig text eol=lf
17
+
18
+ # Shell scripts
19
+ *.sh text eol=lf
20
+ *.bash text eol=lf
21
+ *.cmd text eol=crlf
22
+ *.bat text eol=crlf
23
+ *.ps1 text eol=crlf
24
+ *.psm1 text eol=crlf
25
+ {% if project.type.value in ('cli-python', 'library-python', 'backend-frontend', 'backend-frontend-tray', 'data-ml') %}
26
+
27
+ # Python
28
+ *.py text eol=lf
29
+ *.pyi text eol=lf
30
+ *.pyx text eol=lf
31
+ {% endif %}
32
+ {% if project.type.value in ('cli-rust', 'library-rust') %}
33
+
34
+ # Rust
35
+ *.rs text eol=lf
36
+ Cargo.lock text -diff linguist-generated
37
+ {% endif %}
38
+ {% if project.type.value in ('cli-go',) %}
39
+
40
+ # Go
41
+ *.go text eol=lf
42
+ go.sum text -diff linguist-generated
43
+ {% endif %}
44
+ {% if project.type.value in ('cli-c', 'library-c', 'embedded-hardware') %}
45
+
46
+ # C / C++
47
+ *.c text eol=lf
48
+ *.h text eol=lf
49
+ *.cpp text eol=lf
50
+ *.hpp text eol=lf
51
+ *.o binary
52
+ *.a binary
53
+ *.so binary
54
+ *.dll binary
55
+ {% endif %}
56
+ {% if project.type.value in ('web-frontend', 'fullstack-js', 'browser-extension', 'monorepo') %}
57
+
58
+ # JavaScript / TypeScript
59
+ *.js text eol=lf
60
+ *.ts text eol=lf
61
+ *.jsx text eol=lf
62
+ *.tsx text eol=lf
63
+ *.css text eol=lf
64
+ *.scss text eol=lf
65
+ *.html text eol=lf
66
+ package-lock.json text -diff linguist-generated
67
+ yarn.lock text -diff linguist-generated
68
+ pnpm-lock.yaml text -diff linguist-generated
69
+ {% endif %}
70
+ {% if project.type.value == 'dotnet-app' %}
71
+
72
+ # .NET
73
+ *.cs text eol=lf
74
+ *.csproj text eol=lf
75
+ *.sln text eol=crlf
76
+ *.dll binary
77
+ *.exe binary
78
+ {% endif %}
79
+ {% if project.type.value == 'fpga-rtl' %}
80
+
81
+ # FPGA / RTL
82
+ *.vhd text eol=lf
83
+ *.vhdl text eol=lf
84
+ *.v text eol=lf
85
+ *.sv text eol=lf
86
+ *.xdc text eol=lf
87
+ *.sdc text eol=lf
88
+ *.tcl text eol=lf
89
+ *.bit binary
90
+ *.bin binary
91
+ *.xsa binary
92
+ *.ltx binary
93
+ *.mmi binary
94
+ {% endif %}
95
+ {% if project.type.value in ('yocto-bsp',) %}
96
+
97
+ # Yocto / BitBake
98
+ *.bb text eol=lf
99
+ *.bbappend text eol=lf
100
+ *.bbclass text eol=lf
101
+ *.conf text eol=lf
102
+ *.dts text eol=lf
103
+ *.dtsi text eol=lf
104
+ {% endif %}
105
+ {% if project.type.value == 'pcb-hardware' %}
106
+
107
+ # KiCad / PCB
108
+ *.kicad_pcb binary diff
109
+ *.kicad_sch binary diff
110
+ *.kicad_pro text eol=lf
111
+ *.step binary
112
+ *.wrl binary
113
+ {% endif %}
114
+ {% if project.type.value == 'research-paper' %}
115
+
116
+ # LaTeX
117
+ *.tex text eol=lf
118
+ *.bib text eol=lf
119
+ *.cls text eol=lf
120
+ *.sty text eol=lf
121
+ *.pdf binary
122
+ *.eps binary
123
+ {% endif %}
124
+ {% if project.type.value == 'api-specification' %}
125
+
126
+ # API / Protobuf
127
+ *.proto text eol=lf
128
+ *.graphql text eol=lf
129
+ {% endif %}
130
+ {% if project.type.value == 'mobile-app' %}
131
+
132
+ # Mobile
133
+ *.swift text eol=lf
134
+ *.kt text eol=lf
135
+ *.dart text eol=lf
136
+ *.apk binary
137
+ *.aab binary
138
+ *.ipa binary
139
+ {% endif %}
140
+ {% if project.type.value == 'devops-iac' %}
141
+
142
+ # Terraform / IaC
143
+ *.tf text eol=lf
144
+ *.tfvars text eol=lf
145
+ *.hcl text eol=lf
146
+ {% endif %}
@@ -86,16 +86,32 @@ fp-info-cache
86
86
  # FPGA
87
87
  *.jou
88
88
  *.str
89
+ *.bit
90
+ *.bin
91
+ *.xsa
92
+ *.ltx
93
+ *.mmi
89
94
  work/
90
95
  xsim.dir/
96
+ .Xil/
97
+ dfx_runtime.txt
98
+ *.backup.log
99
+ *.backup.jou
91
100
  {% endif %}
92
101
  {% if project.type.value in ('yocto-bsp', 'embedded-hardware') %}
93
102
 
94
- # Embedded
103
+ # Yocto / Embedded
95
104
  twister-out/
96
105
  sstate-cache/
97
106
  downloads/
98
107
  tmp/
108
+ *.wic
109
+ *.wic.xz
110
+ *.wic.bmap
111
+ *.ext4
112
+ *.manifest
113
+ *.tar.gz
114
+ cache/
99
115
  {% endif %}
100
116
  {% if project.type.value == 'research-paper' %}
101
117
 
@@ -117,3 +133,57 @@ tmp/
117
133
  *.tfstate.backup
118
134
  *.tfvars
119
135
  {% endif %}
136
+ {% if project.type.value == 'data-ml' %}
137
+
138
+ # Data / ML
139
+ .ipynb_checkpoints/
140
+ mlruns/
141
+ wandb/
142
+ data/raw/
143
+ models/*.pkl
144
+ models/*.h5
145
+ models/*.pt
146
+ *.onnx
147
+ {% endif %}
148
+ {% if project.type.value == 'microservices' %}
149
+
150
+ # Microservices
151
+ .env
152
+ .env.*
153
+ !.env.example
154
+ docker-compose.override.yml
155
+ {% endif %}
156
+ {% if project.type.value in ('monorepo',) %}
157
+
158
+ # Monorepo
159
+ .turbo/
160
+ .nx/
161
+ {% endif %}
162
+ {% if project.type.value == 'browser-extension' %}
163
+
164
+ # Browser Extension
165
+ web-ext-artifacts/
166
+ *.crx
167
+ *.xpi
168
+ {% endif %}
169
+ {% if project.type.value in ('spec-document', 'user-manual') %}
170
+
171
+ # Documentation builds
172
+ _site/
173
+ site/
174
+ _build/
175
+ .cache/
176
+ public/
177
+ {% endif %}
178
+ {% if project.type.value in ('business-plan', 'legal-compliance', 'patent-application') %}
179
+
180
+ # Office / Documents
181
+ ~$*
182
+ *.tmp
183
+ *.bak
184
+ {% endif %}
185
+ {% if project.type.value == 'api-specification' %}
186
+
187
+ # API / Generated
188
+ generated/
189
+ {% endif %}
@@ -66,9 +66,10 @@ _TOOL_REGISTRY: dict[ProjectType, ToolSet] = {
66
66
  ),
67
67
  ProjectType.YOCTO_BSP: ToolSet(
68
68
  lint=["oelint-adv"],
69
- test=["bitbake"],
69
+ test=["bitbake -c testimage"],
70
70
  build=["kas build", "bitbake"],
71
71
  security=[],
72
+ compliance=["yocto-check-layer"],
72
73
  ),
73
74
  ProjectType.PCB_HARDWARE: ToolSet(
74
75
  lint=[],
@@ -362,10 +363,28 @@ LANG_CI_META: dict[str, dict[str, str]] = {
362
363
  "docker_image": "verilator/verilator:latest",
363
364
  "install": "",
364
365
  },
365
- "markdown": {
366
+ "systemverilog": {
367
+ "gh_setup": "",
368
+ "docker_image": "verilator/verilator:latest",
369
+ "install": "",
370
+ },
371
+ "bitbake": {
372
+ "gh_setup": "",
373
+ "docker_image": "crops/poky:latest",
374
+ "install": "pip install oelint-adv",
375
+ },
376
+ "devicetree": {
366
377
  "gh_setup": "",
378
+ "docker_image": "gcc:latest",
379
+ "install": "",
380
+ },
381
+ "markdown": {
382
+ "gh_setup": (
383
+ " - uses: actions/setup-python@v6\n"
384
+ ' with:\n python-version: "3.12"\n'
385
+ ),
367
386
  "docker_image": "pandoc/core:latest",
368
- "install": "pip install vale mkdocs",
387
+ "install": "pip install vale mkdocs markdownlint-cli2 cspell",
369
388
  },
370
389
  "latex": {
371
390
  "gh_setup": "",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specsmith
3
- Version: 0.1.4.dev26
3
+ Version: 0.1.4.dev30
4
4
  Summary: Forge governed project scaffolds from the Agentic AI Development Workflow Specification.
5
5
  Author: BitConcepts
6
6
  License: MIT
@@ -39,6 +39,7 @@ src/specsmith/integrations/gemini.py
39
39
  src/specsmith/integrations/warp.py
40
40
  src/specsmith/integrations/windsurf.py
41
41
  src/specsmith/templates/agents.md.j2
42
+ src/specsmith/templates/editorconfig.j2
42
43
  src/specsmith/templates/gitattributes.j2
43
44
  src/specsmith/templates/gitignore.j2
44
45
  src/specsmith/templates/ledger.md.j2
@@ -1,15 +0,0 @@
1
- * text=auto
2
- *.py text eol=lf
3
- *.pyi text eol=lf
4
- *.sh text eol=lf
5
- *.bash text eol=lf
6
- *.ps1 text eol=crlf
7
- *.psm1 text eol=crlf
8
- *.toml text eol=lf
9
- *.cfg text eol=lf
10
- *.yml text eol=lf
11
- *.yaml text eol=lf
12
- *.json text eol=lf
13
- *.md text eol=lf
14
- .gitignore text eol=lf
15
- .gitattributes text eol=lf
File without changes