specfact-cli 0.8.0__tar.gz → 0.9.0__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 (116) hide show
  1. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/PKG-INFO +1 -1
  2. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/pyproject.toml +1 -1
  3. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/__init__.py +1 -1
  4. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/__init__.py +1 -1
  5. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/cli.py +1 -1
  6. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/commands/enforce.py +39 -37
  7. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/commands/import_cmd.py +222 -127
  8. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/commands/plan.py +491 -383
  9. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/commands/sync.py +277 -168
  10. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/models/__init__.py +36 -0
  11. specfact_cli-0.9.0/src/specfact_cli/models/bridge.py +203 -0
  12. specfact_cli-0.9.0/src/specfact_cli/models/project.py +417 -0
  13. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/sync/__init__.py +9 -0
  14. specfact_cli-0.9.0/src/specfact_cli/sync/bridge_probe.py +365 -0
  15. specfact_cli-0.9.0/src/specfact_cli/sync/bridge_sync.py +508 -0
  16. specfact_cli-0.9.0/src/specfact_cli/sync/bridge_watch.py +449 -0
  17. specfact_cli-0.9.0/src/specfact_cli/templates/__init__.py +14 -0
  18. specfact_cli-0.9.0/src/specfact_cli/templates/bridge_templates.py +244 -0
  19. specfact_cli-0.9.0/src/specfact_cli/utils/bundle_loader.py +339 -0
  20. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/utils/structure.py +122 -1
  21. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/.gitignore +0 -0
  22. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/LICENSE.md +0 -0
  23. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/README.md +0 -0
  24. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/mappings/node-async.yaml +0 -0
  25. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/mappings/python-async.yaml +0 -0
  26. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/mappings/speckit-default.yaml +0 -0
  27. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-enforce.md +0 -0
  28. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-import-from-code.md +0 -0
  29. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-plan-add-feature.md +0 -0
  30. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-plan-add-story.md +0 -0
  31. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-plan-compare.md +0 -0
  32. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-plan-init.md +0 -0
  33. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-plan-promote.md +0 -0
  34. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-plan-review.md +0 -0
  35. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-plan-select.md +0 -0
  36. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-plan-update-feature.md +0 -0
  37. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-plan-update-idea.md +0 -0
  38. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-repro.md +0 -0
  39. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/prompts/specfact-sync.md +0 -0
  40. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/schemas/deviation.schema.json +0 -0
  41. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/schemas/plan.schema.json +0 -0
  42. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/schemas/protocol.schema.json +0 -0
  43. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/templates/github-action.yml.j2 +0 -0
  44. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/templates/plan.bundle.yaml.j2 +0 -0
  45. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/templates/pr-template.md.j2 +0 -0
  46. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/templates/protocol.yaml.j2 +0 -0
  47. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/resources/templates/telemetry.yaml.example +0 -0
  48. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/agents/__init__.py +0 -0
  49. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/agents/analyze_agent.py +0 -0
  50. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/agents/base.py +0 -0
  51. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/agents/plan_agent.py +0 -0
  52. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/agents/registry.py +0 -0
  53. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/agents/sync_agent.py +0 -0
  54. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/analyzers/__init__.py +0 -0
  55. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  56. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
  57. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  58. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  59. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  60. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  61. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  62. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/commands/__init__.py +0 -0
  63. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/commands/constitution.py +0 -0
  64. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/commands/generate.py +0 -0
  65. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/commands/init.py +0 -0
  66. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/commands/repro.py +0 -0
  67. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/common/__init__.py +0 -0
  68. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/common/logger_setup.py +0 -0
  69. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/common/logging_utils.py +0 -0
  70. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/common/text_utils.py +0 -0
  71. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/common/utils.py +0 -0
  72. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/comparators/__init__.py +0 -0
  73. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  74. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  75. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  76. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/generators/__init__.py +0 -0
  77. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/generators/contract_generator.py +0 -0
  78. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/generators/plan_generator.py +0 -0
  79. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/generators/protocol_generator.py +0 -0
  80. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/generators/report_generator.py +0 -0
  81. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/generators/workflow_generator.py +0 -0
  82. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/importers/__init__.py +0 -0
  83. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/importers/speckit_converter.py +0 -0
  84. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  85. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/migrations/__init__.py +0 -0
  86. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  87. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/models/deviation.py +0 -0
  88. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/models/enforcement.py +0 -0
  89. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/models/plan.py +0 -0
  90. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/models/protocol.py +0 -0
  91. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/models/sdd.py +0 -0
  92. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/modes/__init__.py +0 -0
  93. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/modes/detector.py +0 -0
  94. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/modes/router.py +0 -0
  95. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  96. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/runtime.py +0 -0
  97. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/sync/repository_sync.py +0 -0
  98. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/sync/speckit_sync.py +0 -0
  99. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/sync/watcher.py +0 -0
  100. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/telemetry.py +0 -0
  101. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/utils/__init__.py +0 -0
  102. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  103. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/utils/console.py +0 -0
  104. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  105. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/utils/feature_keys.py +0 -0
  106. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/utils/git.py +0 -0
  107. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/utils/github_annotations.py +0 -0
  108. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/utils/ide_setup.py +0 -0
  109. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/utils/prompts.py +0 -0
  110. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/utils/structured_io.py +0 -0
  111. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/utils/yaml_utils.py +0 -0
  112. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/validators/__init__.py +0 -0
  113. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/validators/contract_validator.py +0 -0
  114. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/validators/fsm.py +0 -0
  115. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/validators/repro_checker.py +0 -0
  116. {specfact_cli-0.8.0 → specfact_cli-0.9.0}/src/specfact_cli/validators/schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfact-cli
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts. Automate legacy code documentation and prevent modernization regressions.
5
5
  Project-URL: Homepage, https://github.com/nold-ai/specfact-cli
6
6
  Project-URL: Repository, https://github.com/nold-ai/specfact-cli.git
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "specfact-cli"
7
- version = "0.8.0"
7
+ version = "0.9.0"
8
8
  description = "Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts. Automate legacy code documentation and prevent modernization regressions."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -3,4 +3,4 @@ SpecFact CLI - Spec→Contract→Sentinel tool for contract-driven development.
3
3
  """
4
4
 
5
5
  # Define the package version (kept in sync with pyproject.toml and setup.py)
6
- __version__ = "0.8.0"
6
+ __version__ = "0.9.0"
@@ -9,6 +9,6 @@ This package provides command-line tools for:
9
9
  - Validating reproducibility
10
10
  """
11
11
 
12
- __version__ = "0.8.0"
12
+ __version__ = "0.9.0"
13
13
 
14
14
  __all__ = ["__version__"]
@@ -291,7 +291,7 @@ app.add_typer(
291
291
  name="constitution",
292
292
  help="Manage project constitutions (Spec-Kit compatibility layer)",
293
293
  )
294
- app.add_typer(import_cmd.app, name="import", help="Import codebases and Spec-Kit projects")
294
+ app.add_typer(import_cmd.app, name="import", help="Import codebases and external tool projects (e.g., Spec-Kit, Linear, Jira)")
295
295
  app.add_typer(plan.app, name="plan", help="Manage development plans")
296
296
  app.add_typer(generate.app, name="generate", help="Generate artifacts from SDD and plans")
297
297
  app.add_typer(enforce.app, name="enforce", help="Configure quality gates")
@@ -104,23 +104,19 @@ def stage(
104
104
 
105
105
  @app.command("sdd")
106
106
  @beartype
107
+ @require(lambda bundle: isinstance(bundle, str) and len(bundle) > 0, "Bundle name must be non-empty string")
107
108
  @require(lambda sdd: sdd is None or isinstance(sdd, Path), "SDD must be None or Path")
108
- @require(lambda plan: plan is None or isinstance(plan, Path), "Plan must be None or Path")
109
109
  @require(
110
110
  lambda format: isinstance(format, str) and format.lower() in ("yaml", "json", "markdown"),
111
111
  "Format must be yaml, json, or markdown",
112
112
  )
113
113
  @require(lambda out: out is None or isinstance(out, Path), "Out must be None or Path")
114
114
  def enforce_sdd(
115
+ bundle: str = typer.Argument(..., help="Project bundle name (e.g., legacy-api, auth-module)"),
115
116
  sdd: Path | None = typer.Option(
116
117
  None,
117
118
  "--sdd",
118
- help="Path to SDD manifest (default: .specfact/sdd.<format>)",
119
- ),
120
- plan: Path | None = typer.Option(
121
- None,
122
- "--plan",
123
- help="Path to plan bundle (default: active plan)",
119
+ help="Path to SDD manifest (default: .specfact/sdd/<bundle-name>.<format>)",
124
120
  ),
125
121
  format: str = typer.Option(
126
122
  "yaml",
@@ -139,21 +135,21 @@ def enforce_sdd(
139
135
  ),
140
136
  ) -> None:
141
137
  """
142
- Validate SDD manifest against plan bundle and contracts.
138
+ Validate SDD manifest against project bundle and contracts.
143
139
 
144
140
  Checks:
145
- - SDD ↔ plan hash match
141
+ - SDD ↔ bundle hash match
146
142
  - Coverage thresholds (contracts/story, invariants/feature, architecture facets)
147
143
  - Frozen sections (hash mismatch detection)
148
144
  - Contract density metrics
149
145
 
150
146
  Example:
151
- specfact enforce sdd
152
- specfact enforce sdd --plan .specfact/plans/main.bundle.yaml
153
- specfact enforce sdd --format json --out validation-report.json
147
+ specfact enforce sdd legacy-api
148
+ specfact enforce sdd auth-module --format json --out validation-report.json
154
149
  """
155
- from specfact_cli.migrations.plan_migrator import load_plan_bundle
156
150
  from specfact_cli.models.sdd import SDDManifest
151
+ from specfact_cli.utils.bundle_loader import load_project_bundle
152
+ from specfact_cli.utils.structure import SpecFactStructure
157
153
  from specfact_cli.utils.structured_io import (
158
154
  StructuredFormat,
159
155
  dump_structured_file,
@@ -169,12 +165,19 @@ def enforce_sdd(
169
165
  console.print("\n[bold cyan]SpecFact CLI - SDD Validation[/bold cyan]")
170
166
  console.print("=" * 60)
171
167
 
172
- # Find SDD manifest path
168
+ # Find bundle directory
169
+ bundle_dir = SpecFactStructure.project_dir(bundle_name=bundle)
170
+ if not bundle_dir.exists():
171
+ console.print(f"[bold red]✗[/bold red] Project bundle not found: {bundle_dir}")
172
+ console.print(f"[dim]Create one with: specfact plan init {bundle}[/dim]")
173
+ raise typer.Exit(1)
174
+
175
+ # Find SDD manifest path (one per bundle: .specfact/sdd/<bundle-name>.yaml)
173
176
  if sdd is None:
174
177
  base_path = Path(".")
175
178
  # Try YAML first, then JSON
176
- sdd_yaml = base_path / SpecFactStructure.ROOT / "sdd.yaml"
177
- sdd_json = base_path / SpecFactStructure.ROOT / "sdd.json"
179
+ sdd_yaml = base_path / SpecFactStructure.SDD / f"{bundle}.yaml"
180
+ sdd_json = base_path / SpecFactStructure.SDD / f"{bundle}.json"
178
181
  if sdd_yaml.exists():
179
182
  sdd = sdd_yaml
180
183
  elif sdd_json.exists():
@@ -182,47 +185,46 @@ def enforce_sdd(
182
185
  else:
183
186
  console.print("[bold red]✗[/bold red] SDD manifest not found")
184
187
  console.print(f"[dim]Expected: {sdd_yaml} or {sdd_json}[/dim]")
185
- console.print("[dim]Create one with: specfact plan harden[/dim]")
188
+ console.print(f"[dim]Create one with: specfact plan harden {bundle}[/dim]")
186
189
  raise typer.Exit(1)
187
190
 
188
191
  if not sdd.exists():
189
192
  console.print(f"[bold red]✗[/bold red] SDD manifest not found: {sdd}")
190
193
  raise typer.Exit(1)
191
194
 
192
- # Find plan path (reuse logic from plan.py)
193
- plan_path = _find_plan_path(plan)
194
- if plan_path is None or not plan_path.exists():
195
- console.print("[bold red]✗[/bold red] Plan bundle not found")
196
- raise typer.Exit(1)
197
-
198
195
  try:
199
196
  # Load SDD manifest
200
197
  console.print(f"[dim]Loading SDD manifest: {sdd}[/dim]")
201
198
  sdd_data = load_structured_file(sdd)
202
199
  sdd_manifest = SDDManifest.model_validate(sdd_data)
203
200
 
204
- # Load plan bundle
205
- console.print(f"[dim]Loading plan bundle: {plan_path}[/dim]")
206
- bundle = load_plan_bundle(plan_path)
207
- bundle.update_summary(include_hash=True)
208
- plan_hash = bundle.metadata.summary.content_hash if bundle.metadata and bundle.metadata.summary else None
201
+ # Load project bundle
202
+ console.print(f"[dim]Loading project bundle: {bundle_dir}[/dim]")
203
+ project_bundle = load_project_bundle(bundle_dir, validate_hashes=False)
204
+ summary = project_bundle.compute_summary(include_hash=True)
205
+ project_hash = summary.content_hash
209
206
 
210
- if not plan_hash:
211
- console.print("[bold red]✗[/bold red] Failed to compute plan bundle hash")
207
+ if not project_hash:
208
+ console.print("[bold red]✗[/bold red] Failed to compute project bundle hash")
212
209
  raise typer.Exit(1)
213
210
 
211
+ # Convert to PlanBundle for compatibility with validation functions
212
+ from specfact_cli.commands.plan import _convert_project_bundle_to_plan_bundle
213
+
214
+ plan_bundle = _convert_project_bundle_to_plan_bundle(project_bundle)
215
+
214
216
  # Create validation report
215
217
  report = ValidationReport()
216
218
 
217
219
  # 1. Validate hash match
218
220
  console.print("\n[cyan]Validating hash match...[/cyan]")
219
- if sdd_manifest.plan_bundle_hash != plan_hash:
221
+ if sdd_manifest.plan_bundle_hash != project_hash:
220
222
  deviation = Deviation(
221
223
  type=DeviationType.HASH_MISMATCH,
222
224
  severity=DeviationSeverity.HIGH,
223
- description=f"SDD plan bundle hash mismatch: expected {plan_hash[:16]}..., got {sdd_manifest.plan_bundle_hash[:16]}...",
224
- location=".specfact/sdd.yaml",
225
- fix_hint="Run 'specfact plan harden' to update SDD manifest with current plan hash",
225
+ description=f"SDD bundle hash mismatch: expected {project_hash[:16]}..., got {sdd_manifest.plan_bundle_hash[:16]}...",
226
+ location=str(sdd),
227
+ fix_hint=f"Run 'specfact plan harden {bundle}' to update SDD manifest with current bundle hash",
226
228
  )
227
229
  report.add_deviation(deviation)
228
230
  console.print("[bold red]✗[/bold red] Hash mismatch detected")
@@ -235,10 +237,10 @@ def enforce_sdd(
235
237
  from specfact_cli.validators.contract_validator import calculate_contract_density, validate_contract_density
236
238
 
237
239
  # Calculate contract density metrics
238
- metrics = calculate_contract_density(sdd_manifest, bundle)
240
+ metrics = calculate_contract_density(sdd_manifest, plan_bundle)
239
241
 
240
242
  # Validate against thresholds
241
- density_deviations = validate_contract_density(sdd_manifest, bundle, metrics)
243
+ density_deviations = validate_contract_density(sdd_manifest, plan_bundle, metrics)
242
244
 
243
245
  # Add deviations to report
244
246
  for deviation in density_deviations:
@@ -295,7 +297,7 @@ def enforce_sdd(
295
297
 
296
298
  # Save report
297
299
  if output_format == "markdown":
298
- _save_markdown_report(out, report, sdd_manifest, bundle, plan_hash)
300
+ _save_markdown_report(out, report, sdd_manifest, bundle, project_hash)
299
301
  elif output_format == "json":
300
302
  dump_structured_file(report.model_dump(mode="json"), out, StructuredFormat.JSON)
301
303
  else: # yaml