orchestrata_ultimate.openspec.detect

Reference › orchestrata_ultimate › openspec › detect

detect locates a project's spec folder and recognises its format (SPEC-104 FR-001 and FR-002). OpenSpec lives in an openspec/ directory, while orchestrata's own specs live in specs/NNN-*/spec.md. detect_spec_folder takes a project root and returns a SpecFolder from models whose format is openspec, orchestrata or unknown, with its capabilities and changes listed (_subdir_names). _first_spec_md reads the first spec.md under a directory at any depth, and is_openspec decides by matching OpenSpec markers (requirement and scenario headers, or a delta header) against orchestrata markers such as REQ-NNN- ids. Detection never raises: an absent or unrecognised folder yields format unknown. The package imports it.

At a glance

Source src/orchestrata_ultimate/openspec/detect.py
Lines of code 94
Classes 0
Functions 4
Imports (internal) orchestrata_ultimate.core.base.traces, orchestrata_ultimate.openspec.models
Imported by orchestrata_ultimate.openspec
Tests that import it none

Class diagram

No classes; see Functions.

Dependency diagram

Dependency diagram orchestrata_ultimate.openspec.detect

API

Functions

Function Signature Summary
is_openspec traces is_openspec(text: str) -> bool True if the markdown uses OpenSpec structure (Requirement+Scenario, or a delta).
_first_spec_md traces _first_spec_md(specs_dir: Path) -> str Read the first spec.md under specs_dir (any nesting), or '' if none.
detect_spec_folder traces detect_spec_folder(root: str | Path) -> SpecFolder Locate the spec folder under root and classify its format (FR-001/002).
_subdir_names traces _subdir_names(directory: Path) -> list[str]

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/openspec/detect.py

"""Locate a project's spec folder and recognize its format (SPEC-104 FR-001/002).

OpenSpec lives in an ``openspec/`` directory; orchestrata's own specs live in
``specs/NNN-*/spec.md``. Detection never raises — an absent or unrecognized
folder yields a ``SpecFolder`` with format ``"unknown"``.
"""

from __future__ import annotations

import re
from pathlib import Path

from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.openspec.models import SpecFolder

# Distinctive OpenSpec markers (case-sensitive headers, per the convention).
_OPENSPEC_REQUIREMENT = re.compile(r"^### Requirement:", re.MULTILINE)
_OPENSPEC_SCENARIO = re.compile(r"^#### Scenario:", re.MULTILINE)
_OPENSPEC_DELTA = re.compile(
    r"^## (?:ADDED|MODIFIED|REMOVED|RENAMED) Requirements", re.MULTILINE
)
# orchestrata SPEC-NNN markers.
_ORCHESTRATA = re.compile(r"REQ-\d{3}-|^### FR-\d|^- \*\*Spec ID\*\*", re.MULTILINE)


@traces(spec="104", story="US-104-001", reqs=["REQ-104-002"])
def is_openspec(text: str) -> bool:
    """True if the markdown uses OpenSpec structure (Requirement+Scenario, or a delta)."""
    if _OPENSPEC_DELTA.search(text):
        return True
    return bool(_OPENSPEC_REQUIREMENT.search(text) and _OPENSPEC_SCENARIO.search(text))


@traces(spec="104", story="US-104-001", reqs=["REQ-104-002", "REQ-104-003"])
def _first_spec_md(specs_dir: Path) -> str:
    """Read the first spec.md under specs_dir (any nesting), or '' if none."""
    for spec in sorted(specs_dir.rglob("spec.md")):
        try:
            return spec.read_text(encoding="utf-8", errors="replace")
        except OSError:
            continue
    return ""


@traces(
    spec="104",
    story="US-104-001",
    reqs=["REQ-104-001", "REQ-104-002", "REQ-104-003"],
)
def detect_spec_folder(root: str | Path) -> SpecFolder:
    """Locate the spec folder under root and classify its format (FR-001/002).

    Prefers an ``openspec/`` directory; otherwise inspects a ``specs/`` folder and
    sniffs a spec.md to decide openspec vs orchestrata. Returns a SpecFolder with
    ``format="unknown"`` and ``root=None`` when nothing is found.
    """
    base = Path(root)

    openspec_dir = base / "openspec"
    if openspec_dir.is_dir():
        specs = openspec_dir / "specs"
        changes = openspec_dir / "changes"
        return SpecFolder(
            root=openspec_dir,
            format="openspec",
            capabilities=_subdir_names(specs),
            changes=[c for c in _subdir_names(changes) if c != "archive"],
        )

    specs_dir = base / "specs"
    if specs_dir.is_dir():
        sample = _first_spec_md(specs_dir)
        if is_openspec(sample):
            return SpecFolder(
                root=specs_dir, format="openspec", capabilities=_subdir_names(specs_dir)
            )
        if _ORCHESTRATA.search(sample):
            return SpecFolder(
                root=specs_dir,
                format="orchestrata",
                capabilities=_subdir_names(specs_dir),
            )
        return SpecFolder(root=specs_dir, format="unknown")

    return SpecFolder(root=None, format="unknown")


@traces(spec="104", story="US-104-001", reqs=["REQ-104-001"])
def _subdir_names(directory: Path) -> list[str]:
    if not directory.is_dir():
        return []
    return sorted(
        d.name for d in directory.iterdir() if d.is_dir() and not d.name.startswith(".")
    )