Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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