Skip to content

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

Closed

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.py → generate_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

  1. added
    new featureIssues or PRs for a new feature that doesn't currently exist
    phase-2Phase 2 spec generation
    outputOutput and markdown writing
    on Mar 4, 2026
  2. Dimwiddle commented on Mar 7, 2026

    @Dimwiddle
    ContributorAuthor

    Implementation Notes

    Branch: codex/134-draft-spec-generator-and-markdown-writer
    Approach: Implement draft spec markdown generation from DraftFeature/SpecStep, add parser underscore-directory exclusion, and cover behavior with focused discovery + parser tests.

    Decisions

    • Use staging path .specleft/specs/_discovered/ and parser underscore-directory exclusion for future-proof draft/private directories.
    • Keep writer logic presentation-only: scenario step content comes directly from prebuilt SpecStep objects.
    • Enforce exactly 3 steps per scenario in the writer to guarantee stable draft shape.

    Tasks

    • Implement src/specleft/discovery/spec_writer.py with dry-run, overwrite, and validation-aware writing.
    • Update parser recursion to skip underscore-prefixed directories.
    • Update discovery model defaults/references from discovered/ to _discovered/ where needed.
    • Add/extend unit tests in tests/discovery/test_spec_writer.py for writer behavior and parser exclusion.
    • Update discovery feature scenarios/tests in features/feature-spec-discovery.md for this issue.
    • Run lint/tests and prepare commit for #134.
  3. added 2 commits that reference this issue on Mar 7, 2026
  4. linked a pull request that will close this issueAdd discovery draft spec writer (#134) #150on Mar 9, 2026
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