Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extraKnownMarketplaces": {
"reqstool-ai": {
"source": {
"source": "github",
"repo": "reqstool/reqstool-ai"
},
"autoUpdate": true
}
},
"enabledPlugins": {
"reqstool@reqstool-ai": true,
"reqstool-openspec@reqstool-ai": true
}
}
33 changes: 33 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ on:
- reopened
- synchronize

permissions:
contents: read

jobs:
linting:
name: Reuse linting job
Expand All @@ -19,6 +22,10 @@ jobs:
build:
needs: linting
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
reqstool-source: [pypi, main]
steps:
- name: Check out source repository
uses: actions/checkout@v7
Expand All @@ -34,9 +41,35 @@ jobs:
run: hatch run dev:pytest --junitxml=build/junit.xml --cov=reqstool_python_decorators --cov-report=xml:build/coverage.xml
- name: Build project
run: hatch build
- name: Self-apply own decorator processor to own src/tests
# Relies on `hatch build` (above) not cleaning build/ -- annotations.yml lands
# alongside the junit/coverage output from the "Run tests" step, and
# reqstool_config.yml expects to find both there. If hatch build ever starts
# cleaning build/, move the pytest output dir outside of it.
run: hatch run dev:python scripts/generate_annotations.py
- name: Install reqstool
uses: reqstool/.github/.github/actions/install-reqstool@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
with:
reqstool-source: ${{ matrix.reqstool-source }}
- name: Validate reqstool spec completeness
# not yet available in the latest PyPI release; supplementary to (not a
# replacement for) the `reqstool status --fail-if-incomplete` gate below, which
# is the actual required check on the pypi leg
if: matrix.reqstool-source == 'main'
uses: reqstool/.github/.github/actions/validate-reqstool@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
- name: Run reqstool status
# Required gate on both matrix legs -- fails the build unless every
# requirement is implemented and verified.
uses: reqstool/.github/.github/actions/reqstool-status@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
with:
fail-if-incomplete: "true"
# Upload artifacts for later use
- name: Upload Artifacts
if: matrix.reqstool-source == 'pypi'
uses: actions/upload-artifact@v7
with:
name: dist
path: dist/

validate-openspec:
uses: reqstool/.github/.github/workflows/common-validate-openspec.yml@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,5 @@ pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python,intellij+all,visualstudiocode

# Claude Code
.claude/
.claude/*
!.claude/settings.json
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"reqstool": {
"command": "reqstool",
"args": ["mcp"]
}
}
}
32 changes: 32 additions & 0 deletions .reqstool-ai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# reqstool-ai configuration
#
# This file tells reqstool-ai skills where to find your reqstool files
# and how to generate IDs for new requirements and SVCs.
#
# Place this file at: .reqstool-ai.yaml (project root)

# Project URN — matches the urn in your reqstool YAML files
urn: reqstool-python-decorators

# Revision string for new requirements and SVCs
revision: "0.1.0"

# System-level reqstool directory (contains the SSOT requirements and SVCs)
system:
path: docs/reqstool

# Subproject modules — each module imports a subset of requirements/SVCs via filters
#
# Required fields per module:
# path — path to the module's reqstool directory (contains filter files)
# req_prefix — prefix for requirement IDs belonging to this module (e.g., CORE_)
# svc_prefix — prefix for SVC IDs belonging to this module (e.g., SVC_CORE_)
#
# Add as many modules as your project has. The module name (key) is used in
# commands like `/reqstool:status core` and `/reqstool:add-req core`.
modules:
# Matches the existing DECORATORS_NNN / SVC_DECORATORS_NNN ID convention.
decorators:
path: docs/reqstool
req_prefix: "DECORATORS_"
svc_prefix: "SVC_DECORATORS_"
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ For DCO sign-off, commit conventions, and code review process, see the organizat

- Python 3.13+
- [Hatch](https://hatch.pypa.io/) (`pip install hatch`)
- [reqstool](https://github.com/reqstool/reqstool-client) (`pipx install reqstool`)
- [OpenSpec](https://github.com/Fission-AI/OpenSpec) (`npm install -g @fission-ai/openspec`)

## Setup

Expand All @@ -17,6 +19,18 @@ cd reqstool-python-decorators
hatch env create
```

If using Claude Code, opening this repo will prompt you to confirm adding the `reqstool-ai`
marketplace and enabling the `reqstool`/`reqstool-openspec` plugins (configured in
`.claude/settings.json`) — accept the prompt.

Then regenerate the `opsx` slash commands and OpenSpec skills
(`.claude/commands/opsx/`, `.claude/skills/openspec-*`) — they're CLI-generated tool scaffolding,
not committed to the repo:

```bash
openspec update # or: openspec init --tools claude --force
```

## Build & Test

```bash
Expand Down
11 changes: 11 additions & 0 deletions docs/reqstool/reqstool_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/reqstool_config.schema.json

language: python
build: hatch
resources:
requirements: requirements.yml
software_verification_cases: software_verification_cases.yml
manual_verification_results: manual_verification_results.yml
annotations: ../../build/reqstool/annotations.yml
test_results:
- ../../build/**/*.xml
45 changes: 45 additions & 0 deletions docs/reqstool/requirements.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/requirements.schema.json
---
metadata:
urn: reqstool-python-decorators
variant: microservice
title: Reqstool Python Decorators
url: https://github.com/reqstool/reqstool-python-decorators

requirements:
- id: DECORATORS_001
title: Decorator definitions
significance: shall
description: >-
The library shall provide Requirements and SVCs decorator factories that attach a
tuple of string IDs (requirements or svc_ids respectively) to the decorated function,
async function, or class as an attribute, without altering the decorated callable's
identity or name.
categories:
- functional-suitability
revision: "0.1.0"

- id: DECORATORS_002
title: Decorator processing
significance: shall
description: >-
The processor shall discover Python source files under a given set of directories,
parse each file's AST to find functions, async functions, and classes decorated with
Requirements or SVCs, and group the collected IDs into an implementations/tests
structure keyed by requirement or SVC ID, with each entry recording the fully
qualified name and a normalized element kind (FUNCTION/ASYNCFUNCTION mapped to
METHOD).
categories:
- functional-suitability
revision: "0.1.0"

- id: DECORATORS_003
title: YAML export
significance: shall
description: >-
The processor shall serialize the processed implementations/tests structure to a
YAML file at the given output path, prefixed with a JSON schema header comment,
creating any missing parent directories first.
categories:
- functional-suitability
revision: "0.1.0"
20 changes: 20 additions & 0 deletions docs/reqstool/software_verification_cases.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/software_verification_cases.schema.json

cases:
- id: SVC_DECORATORS_001
requirement_ids: ["DECORATORS_001"]
title: "Verify Requirements and SVCs attach ID tuples to functions, async functions, and classes"
verification: automated-test
revision: "0.1.0"

- id: SVC_DECORATORS_002
requirement_ids: ["DECORATORS_002"]
title: "Verify file discovery, AST extraction, element-kind mapping, and ID grouping into implementations/tests"
verification: automated-test
revision: "0.1.0"

- id: SVC_DECORATORS_003
requirement_ids: ["DECORATORS_003"]
title: "Verify the exported YAML matches the expected schema-tagged implementations/tests structure"
verification: automated-test
revision: "0.1.0"
108 changes: 108 additions & 0 deletions openspec/openspecui.hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// @reqstool-openspec-hooks: 0.1.1
import { spawn, ChildProcess } from "child_process";
import type { OnReadDocumentHookV1 } from "openspecui/hooks";

// Minimal MCP client over stdio (JSON-RPC 2.0, newline-delimited).
// Uses only Node.js built-ins — no npm packages required.
class McpStdioClient {
private proc: ChildProcess;
private buf = "";
private pending = new Map<
number,
{ resolve: (v: unknown) => void; reject: (e: Error) => void }
>();
private id = 1;
readonly ready: Promise<void>;

constructor(cwd: string) {
this.proc = spawn("reqstool", ["mcp"], {
cwd,
stdio: ["pipe", "pipe", "pipe"],
});
this.proc.stdout!.on("data", (chunk: Buffer) => {
this.buf += chunk.toString();
let nl: number;
while ((nl = this.buf.indexOf("\n")) !== -1) {
const line = this.buf.slice(0, nl).trim();
this.buf = this.buf.slice(nl + 1);
if (line) this.handle(line);
}
});
this.ready = this.init();
}

private handle(line: string) {
try {
const msg = JSON.parse(line) as { id?: number; result?: unknown; error?: { message: string } };
if (msg.id !== undefined) {
const p = this.pending.get(msg.id);
if (p) {
this.pending.delete(msg.id);
msg.error ? p.reject(new Error(msg.error.message)) : p.resolve(msg.result);
}
}
} catch (e) {
console.warn("[reqstool-openspec] Skipping non-JSON line from reqstool mcp:", e instanceof Error ? e.message : e);
}
}

private send(method: string, params: unknown, expectReply = true): Promise<unknown> {
if (!expectReply) {
this.proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
return Promise.resolve();
}
const id = this.id++;
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
this.proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
});
}

private async init(): Promise<void> {
await this.send("initialize", {
protocolVersion: "2024-11-05",
capabilities: { tools: {} },
clientInfo: { name: "openspecui", version: "1.0" },
});
this.send("notifications/initialized", {}, false);
}

async enrich(content: string, preset: string): Promise<string> {
await this.ready;
const result = (await this.send("tools/call", {
name: "enrich_document",
arguments: { content, preset },
})) as { content: { text: string }[] };
return result.content[0].text;
}

close() {
this.proc.stdin?.end();
this.proc.kill();
}
}

let client: McpStdioClient | null = null;

export const onReadDocument: OnReadDocumentHookV1 = async (ctx, read) => {
if (!client) {
client = new McpStdioClient(ctx.projectDir);
ctx.lifecycle.onDispose(() => {
client?.close();
client = null;
});
}

const result = await read();
const preset = `openspec:${ctx.document.kind}`;

try {
const enriched = await client.enrich(result.markdown, preset);
return { ...result, markdown: enriched, sourceLabel: `reqstool ${preset}` };
} catch (e) {
return {
...result,
diagnostics: [{ level: "warning", message: `reqstool enrich failed: ${e}` }],
};
}
};
15 changes: 15 additions & 0 deletions openspec/specs/decorator-definitions/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Decorator Definitions Specification

## Purpose

Requirement and SVC content is owned by reqstool (single source of truth). This spec references
reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via
`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`.

## Requirements

### Requirement: DECORATORS_001
The system SHALL implement DECORATORS_001.

#### Scenario: SVC_DECORATORS_001
The system SHALL pass SVC_DECORATORS_001.
15 changes: 15 additions & 0 deletions openspec/specs/decorator-processing/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Decorator Processing Specification

## Purpose

Requirement and SVC content is owned by reqstool (single source of truth). This spec references
reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via
`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`.

## Requirements

### Requirement: DECORATORS_002
The system SHALL implement DECORATORS_002.

#### Scenario: SVC_DECORATORS_002
The system SHALL pass SVC_DECORATORS_002.
15 changes: 15 additions & 0 deletions openspec/specs/yaml-export/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# YAML Export Specification

## Purpose

Requirement and SVC content is owned by reqstool (single source of truth). This spec references
reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via
`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`.

## Requirements

### Requirement: DECORATORS_003
The system SHALL implement DECORATORS_003.

#### Scenario: SVC_DECORATORS_003
The system SHALL pass SVC_DECORATORS_003.
Loading