CtrlK
BlogDocsLog inGet started
Tessl Logo

spec-driven-development/spec-as-source

Spec-driven development on OpenSpec, with mechanical spec-as-source enforcement: a custom 'spec-as-source' OpenSpec schema adds file-ownership (targets) and test-verification ([@test]) metadata to every capability spec, three scripts (link check, ownership check, manifest build) keep code and specs from drifting apart, plus requirement-gathering, spec-writer, work-review, and a session-handoff skill with a proactive context-warning hook.

68

Quality

85%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

product_context.pyskills/plan-mode/scripts/

#!/usr/bin/env python3
# GENERATED FROM SPEC — DO NOT EDIT DIRECTLY
# Source: openspec/specs/plan-mode/spec.md
"""Inspect optional product-context sources without mutating the project."""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import shlex
import sys
from pathlib import Path
from typing import Any, Iterable, Mapping

try:
    import yaml
except ImportError:  # Reported as a named contract failure only when needed.
    yaml = None  # type: ignore[assignment]


CONTRACT_VERSION = 1
SOURCE_TYPES = ("prd", "personas", "tech-stack", "adr", "backlog")
CONVENTIONAL = {
    "prd": ("PRD.md", "docs/PRD.md"),
    "personas": ("USER-PERSONAS.md", "docs/USER-PERSONAS.md"),
    "tech-stack": ("TECH-STACK.md", "docs/TECH-STACK.md"),
    "backlog": ("BACKLOG.md", "docs/BACKLOG.md"),
}
ADR_EXACT = ("ADR.md", "docs/ADR.md")
ADR_GLOBS = ("adr/*.md", "docs/adr/*.md")


class InvalidProductContext(ValueError):
    """The explicit manifest cannot be interpreted safely."""


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def verification_command(relative: str) -> str:
    code = (
        "import hashlib,pathlib; "
        f"p=pathlib.Path({relative!r}); "
        "print(hashlib.sha256(p.read_bytes()).hexdigest())"
    )
    return f"python3 -c {shlex.quote(code)}"


def within_project(project_root: Path, resolved: Path) -> bool:
    try:
        resolved.relative_to(project_root)
    except ValueError:
        return False
    return True


def normalize_manifest_path(project_root: Path, raw: str) -> tuple[str, Path]:
    if not raw or "\x00" in raw:
        raise InvalidProductContext("source paths must be non-empty strings")

    candidate = Path(raw)
    if candidate.is_absolute():
        raise InvalidProductContext(f"absolute source path is forbidden: {raw}")

    normalized = Path(os.path.normpath(raw))
    if normalized == Path("."):
        raise InvalidProductContext(f"source path must name a regular file: {raw}")

    configured = project_root / normalized
    try:
        resolved = configured.resolve(strict=False)
    except (OSError, RuntimeError) as exc:
        raise InvalidProductContext(f"cannot resolve source path {raw}: {exc}") from exc

    if not within_project(project_root, resolved):
        raise InvalidProductContext(f"source path escapes the project: {raw}")
    if configured.exists() and not configured.is_file():
        raise InvalidProductContext(f"source path is not a regular file: {raw}")

    relative = normalized.as_posix()
    return relative, configured


def file_item(relative: str, path: Path) -> dict[str, Any]:
    if not path.exists():
        return {
            "path": relative,
            "state": "missing",
            "sha256": None,
            "verify_command": None,
        }

    digest = sha256(path)
    return {
        "path": relative,
        "state": "fresh",
        "sha256": digest,
        "verify_command": verification_command(relative),
    }


def source_bucket(state: str, files: Iterable[dict[str, Any]] = ()) -> dict[str, Any]:
    ordered = sorted(files, key=lambda item: item["path"])
    return {"state": state, "files": ordered}


def load_recorded_digests(path: Path) -> dict[tuple[str, str], str]:
    try:
        document = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, UnicodeError, json.JSONDecodeError) as exc:
        raise InvalidProductContext(f"cannot read recorded inventory {path}: {exc}") from exc

    if not isinstance(document, dict) or document.get("version") != CONTRACT_VERSION:
        raise InvalidProductContext("recorded inventory must be a version 1 inventory object")
    sources = document.get("sources")
    if not isinstance(sources, dict):
        raise InvalidProductContext("recorded inventory sources must be a map")

    digests: dict[tuple[str, str], str] = {}
    for source_type in SOURCE_TYPES:
        bucket = sources.get(source_type)
        if not isinstance(bucket, dict) or not isinstance(bucket.get("files"), list):
            raise InvalidProductContext(f"recorded inventory source {source_type} is malformed")
        for item in bucket["files"]:
            if not isinstance(item, dict) or not isinstance(item.get("path"), str):
                raise InvalidProductContext(f"recorded inventory source {source_type} has a malformed file")
            digest = item.get("sha256")
            if digest is None:
                continue
            if not isinstance(digest, str) or len(digest) != 64:
                raise InvalidProductContext(
                    f"recorded inventory source {source_type} has an invalid SHA-256"
                )
            try:
                int(digest, 16)
            except ValueError as exc:
                raise InvalidProductContext(
                    f"recorded inventory source {source_type} has an invalid SHA-256"
                ) from exc
            key = (source_type, item["path"])
            if key in digests:
                raise InvalidProductContext(
                    f"recorded inventory repeats {source_type} path {item['path']}"
                )
            digests[key] = digest.lower()
    return digests


def apply_recorded_digests(
    sources: dict[str, Any],
    recorded: Mapping[tuple[str, str], str],
) -> None:
    for source_type in SOURCE_TYPES:
        bucket = sources[source_type]
        for item in bucket["files"]:
            digest = recorded.get((source_type, item["path"]))
            if digest is None:
                continue
            item["recorded_sha256"] = digest
            if item["state"] != "missing":
                item["state"] = "fresh" if item["sha256"] == digest else "stale"

        if bucket["state"] == "conflict":
            continue
        states = {item["state"] for item in bucket["files"]}
        if "stale" in states:
            bucket["state"] = "stale"
        elif "missing" in states:
            bucket["state"] = "missing"
        elif bucket["files"]:
            bucket["state"] = "fresh"


def load_manifest(project_root: Path, manifest: Path) -> dict[str, Any]:
    if yaml is None:
        raise InvalidProductContext("PyYAML is required to read openspec/product-context.yaml")

    try:
        document = yaml.safe_load(manifest.read_text(encoding="utf-8"))
    except (OSError, UnicodeError, yaml.YAMLError) as exc:
        raise InvalidProductContext(f"cannot parse openspec/product-context.yaml: {exc}") from exc

    if not isinstance(document, dict):
        raise InvalidProductContext("manifest root must be a map")
    if any(not isinstance(key, str) for key in document):
        raise InvalidProductContext("manifest keys must be strings")

    keys = set(document)
    expected = {"version", "sources"}
    if keys != expected:
        missing = sorted(expected - keys)
        unknown = sorted(keys - expected)
        details = []
        if missing:
            details.append(f"missing keys: {', '.join(missing)}")
        if unknown:
            details.append(f"unknown keys: {', '.join(unknown)}")
        raise InvalidProductContext("manifest schema mismatch (" + "; ".join(details) + ")")

    version = document["version"]
    if type(version) is not int or version != CONTRACT_VERSION:
        raise InvalidProductContext(f"unsupported manifest version: {version!r}")

    sources = document["sources"]
    if not isinstance(sources, dict):
        raise InvalidProductContext("sources must be a map")
    if any(not isinstance(key, str) for key in sources):
        raise InvalidProductContext("source type keys must be strings")

    unknown_types = sorted(set(sources) - set(SOURCE_TYPES))
    if unknown_types:
        raise InvalidProductContext(f"unknown source types: {', '.join(unknown_types)}")

    inventory: dict[str, Any] = {}
    for source_type in SOURCE_TYPES:
        if source_type not in sources:
            inventory[source_type] = source_bucket("not configured")
            continue

        configured_paths = sources[source_type]
        if not isinstance(configured_paths, list):
            raise InvalidProductContext(f"source {source_type} must be a list")
        if any(not isinstance(raw, str) for raw in configured_paths):
            raise InvalidProductContext(f"source {source_type} entries must be strings")

        items_by_path: dict[str, dict[str, Any]] = {}
        for raw in configured_paths:
            relative, path = normalize_manifest_path(project_root, raw)
            items_by_path[relative] = file_item(relative, path)

        items = list(items_by_path.values())
        if not items:
            state = "not configured"
        elif any(item["state"] == "missing" for item in items):
            state = "missing"
        else:
            state = "fresh"
        inventory[source_type] = source_bucket(state, items)

    return inventory


def safe_conventional_file(project_root: Path, relative: str) -> dict[str, Any] | None:
    path = project_root / relative
    if not path.is_file():
        return None
    try:
        resolved = path.resolve(strict=True)
    except (OSError, RuntimeError):
        return None
    if not within_project(project_root, resolved):
        return None
    return file_item(relative, path)


def discover_conventional(project_root: Path) -> dict[str, Any]:
    inventory: dict[str, Any] = {}

    for source_type in SOURCE_TYPES:
        if source_type == "adr":
            continue
        items = [
            item
            for relative in CONVENTIONAL[source_type]
            if (item := safe_conventional_file(project_root, relative)) is not None
        ]
        if len(items) > 1:
            state = "conflict"
        elif items:
            state = "fresh"
        else:
            state = "not configured"
        inventory[source_type] = source_bucket(state, items)

    adr_paths = set(ADR_EXACT)
    for pattern in ADR_GLOBS:
        adr_paths.update(path.relative_to(project_root).as_posix() for path in project_root.glob(pattern))
    adr_items = [
        item
        for relative in sorted(adr_paths)
        if (item := safe_conventional_file(project_root, relative)) is not None
    ]
    inventory["adr"] = source_bucket("fresh" if adr_items else "not configured", adr_items)

    return {source_type: inventory[source_type] for source_type in SOURCE_TYPES}


def inspect(project_root: Path, recorded_inventory: Path | None = None) -> dict[str, Any]:
    try:
        root = project_root.resolve(strict=True)
    except (OSError, RuntimeError) as exc:
        raise InvalidProductContext(f"project root cannot be resolved: {project_root}: {exc}") from exc
    if not root.is_dir():
        raise InvalidProductContext(f"project root is not a directory: {project_root}")

    manifest = root / "openspec/product-context.yaml"
    if manifest.exists():
        if not manifest.is_file():
            raise InvalidProductContext("openspec/product-context.yaml is not a regular file")
        mode = "manifest"
        sources = load_manifest(root, manifest)
    else:
        mode = "conventional"
        sources = discover_conventional(root)

    if recorded_inventory is not None:
        apply_recorded_digests(sources, load_recorded_digests(recorded_inventory))

    return {"version": CONTRACT_VERSION, "mode": mode, "sources": sources}


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--project-root",
        type=Path,
        default=Path.cwd(),
        help="project root to inspect (defaults to the current directory)",
    )
    parser.add_argument(
        "--recorded-inventory",
        type=Path,
        help="previous helper JSON whose SHA-256 values are the freshness baseline",
    )
    args = parser.parse_args(argv)

    try:
        result = inspect(args.project_root, args.recorded_inventory)
    except (InvalidProductContext, OSError) as exc:
        print(f"INVALID-PRODUCT-CONTEXT: {exc}", file=sys.stderr)
        return 2

    print(json.dumps(result, indent=2, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

skills

plan-mode

README.md

tile.json