Skip to content

Discovery Phase 2: draft spec generator and markdown writer #134

@Dimwiddle

Description

@Dimwiddle

Summary

Convert a list[DraftFeature] into SpecLeft-format markdown files written to .specleft/specs/discovered/. Reuses existing feature_writer.py utilities for ID generation and validation. Uses SpecStep objects from DraftScenario directly — no lossy string round-trip.

Depends on: #124, #133

New file

src/specleft/discovery/spec_writer.py

from specleft.discovery.models import DraftFeature, DraftScenario
from specleft.schema import SpecStep, StepType

def generate_draft_specs(
    draft_features: list[DraftFeature],
    output_dir: Path,
    dry_run: bool = False,
    overwrite: bool = False,
) -> list[Path]:
    """
    Returns list of written file paths (or would-be paths if dry_run=True).
    Skips existing files unless overwrite=True.
    Creates output_dir if it does not exist (unless dry_run).
    """

Output format per file

# Feature: User Authentication
<!-- generated by specleft discover — review before promoting to .specleft/specs/ -->

## Scenarios

### Scenario: valid-credentials
priority: medium

<!-- source: tests/auth/test_login.py:14 -->
- Given a user with valid credentials
- When they attempt to login
- Then they should be authenticated

### Scenario: expired-token
priority: medium

<!-- source: tests/auth/test_login.py:28 -->
- Given a user with an expired token
- When they attempt an authenticated request
- Then they should receive a 401 response

Step generation from SpecStep objects

DraftScenario.steps is list[SpecStep] — no string parsing needed. The writer serialises each step directly:

for step in scenario.steps:
    # step.type is StepType (GIVEN, WHEN, THEN, AND, BUT)
    # step.description is the text
    line = f"- {step.type.value} {step.description}"

Miners and the grouping algorithm are responsible for constructing SpecStep objects. The step generation rules per ItemKind remain:

Kind Given When Then
TEST_FUNCTION context from name tokens before first verb verb token as action remainder as outcome
API_ROUTE "a valid request" "{METHOD} {path} is called" "a response is returned"
DOCSTRING first sentence of raw_text second sentence third sentence; pad to 3
GIT_COMMIT "the system is in a known state" commit subject as action "the expected outcome occurs"

These rules are applied when constructing DraftScenario objects (in grouping.py or a helper), not in the writer.

Parser exclusion — _discovered/ convention

The staging directory should use an underscore prefix convention to distinguish it from user-created spec directories:

Default staging path: .specleft/specs/_discovered/

Update src/specleft/parser.py to skip directories whose name starts with _ when recursing. This is consistent with Python's _private convention and prevents naming collisions with user features:

  • _discovered/ → skipped by parser
  • _drafts/ → would also be skipped (future-proof)
  • discovered/ → would NOT be skipped (user could name a feature this)

Note: This changes the staging path from .specleft/specs/discovered/ to .specleft/specs/_discovered/. Update all references in #124 (DraftSpec.output_dir), #136 (discover command), and #137 (start command).

Utilities to reuse

  • src/specleft/utils/feature_writer.pygenerate_feature_id(), generate_scenario_id(), validate_feature_id(), validate_scenario_id()

Acceptance criteria

  • dry_run=True returns correct file paths without writing any files
  • Generated markdown parses successfully with the existing SpecParser after promotion (zero validation errors)
  • Steps are serialised directly from SpecStep objects — no string→parse round-trip
  • Each scenario has exactly 3 steps (Given / When / Then)
  • feature_id and scenario_id pass validate_feature_id() and validate_scenario_id()
  • Existing file is not overwritten when overwrite=False (default)
  • overwrite=True replaces the existing file
  • SpecsConfig.from_directory(".specleft/specs/") does NOT load files from .specleft/specs/_discovered/
  • Parser skips all directories whose name starts with _ when recursing
  • Tests in tests/discovery/test_spec_writer.py
  • Update scenarios and tests in features/feature-spec-discovery.md to cover the functionality introduced by this issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    new featureIssues or PRs for a new feature that doesn't currently existoutputOutput and markdown writingphase-2Phase 2 spec generation

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions