Skip to content

Commit 8baa93f

Browse files
authored
feat(reqstool): dogfood OpenSpec <-> reqstool traceability (#84)
* feat(reqstool): dogfood OpenSpec <-> reqstool traceability Bootstrap the OpenSpec spec layer and derive the reqstool SSOT for this repo, mirroring reqstool-client#407. Adds 3 capability specs (decorator-definitions, decorator-processing, yaml-export) backing DECORATORS_001-003 / SVC_DECORATORS_001-003, re-tags src with @Requirements/@svcs (including genuine self-application: SVCs is tagged with DECORATORS_001 via the Requirements decorator it implements alongside), and wires tests/resources' previously-unused fixture files into a new e2e test instead of leaving them dead. Converts the decorator-mechanics unit tests in test_decorators.py from `@Requirements(...)`/`@SVCs(...)` decorator syntax to direct calls (`Requirements(...)(func)`) -- the decorator syntax form made the nested, intentionally-fake REQ_*/SVC_* IDs in those tests show up as real AST-discoverable annotations once this repo's own processor self-applies to its own tests, which `reqstool validate --strict` correctly rejected as references to non-existent requirement/SVC IDs. CI matrix on [pypi, main] via shared reqstool/.github actions, with a new scripts/generate_annotations.py self-apply step (excluding tests/resources for the same fake-ID reason above) feeding `reqstool status`/`validate`. Signed-off-by: Jimisola Laursen <jimisola@jimisola.com> * fix(reqstool): address full-pr-review findings on #84 - Drop tests/integration from the self-apply scan (empty stub dirs, was a misleading no-op) - Add missing copyright header to scripts/generate_annotations.py - Document why @requirements("DECORATORS_001") is tagged on SVCs rather than Requirements (self-reference ordering constraint) - Clarify in build.yml which step is the actual required gate (reqstool status) vs. supplementary (validate-reqstool, main-only) and note the build/ directory ordering dependency - Strengthen the e2e test with assertions on fullyQualifiedName and the YAML schema header, the parts of the pipeline most exposed to real filesystem behavior - Collapse the 10 near-identical decorator-mechanics unit tests into 3 parametrized tests, removing repeated boilerplate Signed-off-by: Jimisola Laursen <jimisola@jimisola.com> --------- Signed-off-by: Jimisola Laursen <jimisola@jimisola.com>
1 parent 123c4fd commit 8baa93f

19 files changed

Lines changed: 484 additions & 41 deletions

File tree

‎.claude/settings.json‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"extraKnownMarketplaces": {
3+
"reqstool-ai": {
4+
"source": {
5+
"source": "github",
6+
"repo": "reqstool/reqstool-ai"
7+
},
8+
"autoUpdate": true
9+
}
10+
},
11+
"enabledPlugins": {
12+
"reqstool@reqstool-ai": true,
13+
"reqstool-openspec@reqstool-ai": true
14+
}
15+
}

‎.github/workflows/build.yml‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ on:
1111
- reopened
1212
- synchronize
1313

14+
permissions:
15+
contents: read
16+
1417
jobs:
1518
linting:
1619
name: Reuse linting job
@@ -19,6 +22,10 @@ jobs:
1922
build:
2023
needs: linting
2124
runs-on: ubuntu-latest
25+
strategy:
26+
fail-fast: false
27+
matrix:
28+
reqstool-source: [pypi, main]
2229
steps:
2330
- name: Check out source repository
2431
uses: actions/checkout@v7
@@ -34,9 +41,35 @@ jobs:
3441
run: hatch run dev:pytest --junitxml=build/junit.xml --cov=reqstool_python_decorators --cov-report=xml:build/coverage.xml
3542
- name: Build project
3643
run: hatch build
44+
- name: Self-apply own decorator processor to own src/tests
45+
# Relies on `hatch build` (above) not cleaning build/ -- annotations.yml lands
46+
# alongside the junit/coverage output from the "Run tests" step, and
47+
# reqstool_config.yml expects to find both there. If hatch build ever starts
48+
# cleaning build/, move the pytest output dir outside of it.
49+
run: hatch run dev:python scripts/generate_annotations.py
50+
- name: Install reqstool
51+
uses: reqstool/.github/.github/actions/install-reqstool@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
52+
with:
53+
reqstool-source: ${{ matrix.reqstool-source }}
54+
- name: Validate reqstool spec completeness
55+
# not yet available in the latest PyPI release; supplementary to (not a
56+
# replacement for) the `reqstool status --fail-if-incomplete` gate below, which
57+
# is the actual required check on the pypi leg
58+
if: matrix.reqstool-source == 'main'
59+
uses: reqstool/.github/.github/actions/validate-reqstool@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
60+
- name: Run reqstool status
61+
# Required gate on both matrix legs -- fails the build unless every
62+
# requirement is implemented and verified.
63+
uses: reqstool/.github/.github/actions/reqstool-status@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
64+
with:
65+
fail-if-incomplete: "true"
3766
# Upload artifacts for later use
3867
- name: Upload Artifacts
68+
if: matrix.reqstool-source == 'pypi'
3969
uses: actions/upload-artifact@v7
4070
with:
4171
name: dist
4272
path: dist/
73+
74+
validate-openspec:
75+
uses: reqstool/.github/.github/workflows/common-validate-openspec.yml@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22

‎.gitignore‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,4 +283,5 @@ pyrightconfig.json
283283
# End of https://www.toptal.com/developers/gitignore/api/python,intellij+all,visualstudiocode
284284

285285
# Claude Code
286-
.claude/
286+
.claude/*
287+
!.claude/settings.json

‎.mcp.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"reqstool": {
4+
"command": "reqstool",
5+
"args": ["mcp"]
6+
}
7+
}
8+
}

‎.reqstool-ai.yaml‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# reqstool-ai configuration
2+
#
3+
# This file tells reqstool-ai skills where to find your reqstool files
4+
# and how to generate IDs for new requirements and SVCs.
5+
#
6+
# Place this file at: .reqstool-ai.yaml (project root)
7+
8+
# Project URN — matches the urn in your reqstool YAML files
9+
urn: reqstool-python-decorators
10+
11+
# Revision string for new requirements and SVCs
12+
revision: "0.1.0"
13+
14+
# System-level reqstool directory (contains the SSOT requirements and SVCs)
15+
system:
16+
path: docs/reqstool
17+
18+
# Subproject modules — each module imports a subset of requirements/SVCs via filters
19+
#
20+
# Required fields per module:
21+
# path — path to the module's reqstool directory (contains filter files)
22+
# req_prefix — prefix for requirement IDs belonging to this module (e.g., CORE_)
23+
# svc_prefix — prefix for SVC IDs belonging to this module (e.g., SVC_CORE_)
24+
#
25+
# Add as many modules as your project has. The module name (key) is used in
26+
# commands like `/reqstool:status core` and `/reqstool:add-req core`.
27+
modules:
28+
# Matches the existing DECORATORS_NNN / SVC_DECORATORS_NNN ID convention.
29+
decorators:
30+
path: docs/reqstool
31+
req_prefix: "DECORATORS_"
32+
svc_prefix: "SVC_DECORATORS_"

‎CONTRIBUTING.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ For DCO sign-off, commit conventions, and code review process, see the organizat
88

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

1214
## Setup
1315

@@ -17,6 +19,18 @@ cd reqstool-python-decorators
1719
hatch env create
1820
```
1921

22+
If using Claude Code, opening this repo will prompt you to confirm adding the `reqstool-ai`
23+
marketplace and enabling the `reqstool`/`reqstool-openspec` plugins (configured in
24+
`.claude/settings.json`) — accept the prompt.
25+
26+
Then regenerate the `opsx` slash commands and OpenSpec skills
27+
(`.claude/commands/opsx/`, `.claude/skills/openspec-*`) — they're CLI-generated tool scaffolding,
28+
not committed to the repo:
29+
30+
```bash
31+
openspec update # or: openspec init --tools claude --force
32+
```
33+
2034
## Build & Test
2135

2236
```bash

‎docs/reqstool/reqstool_config.yml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/reqstool_config.schema.json
2+
3+
language: python
4+
build: hatch
5+
resources:
6+
requirements: requirements.yml
7+
software_verification_cases: software_verification_cases.yml
8+
manual_verification_results: manual_verification_results.yml
9+
annotations: ../../build/reqstool/annotations.yml
10+
test_results:
11+
- ../../build/**/*.xml

‎docs/reqstool/requirements.yml‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/requirements.schema.json
2+
---
3+
metadata:
4+
urn: reqstool-python-decorators
5+
variant: microservice
6+
title: Reqstool Python Decorators
7+
url: https://github.com/reqstool/reqstool-python-decorators
8+
9+
requirements:
10+
- id: DECORATORS_001
11+
title: Decorator definitions
12+
significance: shall
13+
description: >-
14+
The library shall provide Requirements and SVCs decorator factories that attach a
15+
tuple of string IDs (requirements or svc_ids respectively) to the decorated function,
16+
async function, or class as an attribute, without altering the decorated callable's
17+
identity or name.
18+
categories:
19+
- functional-suitability
20+
revision: "0.1.0"
21+
22+
- id: DECORATORS_002
23+
title: Decorator processing
24+
significance: shall
25+
description: >-
26+
The processor shall discover Python source files under a given set of directories,
27+
parse each file's AST to find functions, async functions, and classes decorated with
28+
Requirements or SVCs, and group the collected IDs into an implementations/tests
29+
structure keyed by requirement or SVC ID, with each entry recording the fully
30+
qualified name and a normalized element kind (FUNCTION/ASYNCFUNCTION mapped to
31+
METHOD).
32+
categories:
33+
- functional-suitability
34+
revision: "0.1.0"
35+
36+
- id: DECORATORS_003
37+
title: YAML export
38+
significance: shall
39+
description: >-
40+
The processor shall serialize the processed implementations/tests structure to a
41+
YAML file at the given output path, prefixed with a JSON schema header comment,
42+
creating any missing parent directories first.
43+
categories:
44+
- functional-suitability
45+
revision: "0.1.0"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/software_verification_cases.schema.json
2+
3+
cases:
4+
- id: SVC_DECORATORS_001
5+
requirement_ids: ["DECORATORS_001"]
6+
title: "Verify Requirements and SVCs attach ID tuples to functions, async functions, and classes"
7+
verification: automated-test
8+
revision: "0.1.0"
9+
10+
- id: SVC_DECORATORS_002
11+
requirement_ids: ["DECORATORS_002"]
12+
title: "Verify file discovery, AST extraction, element-kind mapping, and ID grouping into implementations/tests"
13+
verification: automated-test
14+
revision: "0.1.0"
15+
16+
- id: SVC_DECORATORS_003
17+
requirement_ids: ["DECORATORS_003"]
18+
title: "Verify the exported YAML matches the expected schema-tagged implementations/tests structure"
19+
verification: automated-test
20+
revision: "0.1.0"

‎openspec/openspecui.hooks.ts‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// @reqstool-openspec-hooks: 0.1.1
2+
import { spawn, ChildProcess } from "child_process";
3+
import type { OnReadDocumentHookV1 } from "openspecui/hooks";
4+
5+
// Minimal MCP client over stdio (JSON-RPC 2.0, newline-delimited).
6+
// Uses only Node.js built-ins — no npm packages required.
7+
class McpStdioClient {
8+
private proc: ChildProcess;
9+
private buf = "";
10+
private pending = new Map<
11+
number,
12+
{ resolve: (v: unknown) => void; reject: (e: Error) => void }
13+
>();
14+
private id = 1;
15+
readonly ready: Promise<void>;
16+
17+
constructor(cwd: string) {
18+
this.proc = spawn("reqstool", ["mcp"], {
19+
cwd,
20+
stdio: ["pipe", "pipe", "pipe"],
21+
});
22+
this.proc.stdout!.on("data", (chunk: Buffer) => {
23+
this.buf += chunk.toString();
24+
let nl: number;
25+
while ((nl = this.buf.indexOf("\n")) !== -1) {
26+
const line = this.buf.slice(0, nl).trim();
27+
this.buf = this.buf.slice(nl + 1);
28+
if (line) this.handle(line);
29+
}
30+
});
31+
this.ready = this.init();
32+
}
33+
34+
private handle(line: string) {
35+
try {
36+
const msg = JSON.parse(line) as { id?: number; result?: unknown; error?: { message: string } };
37+
if (msg.id !== undefined) {
38+
const p = this.pending.get(msg.id);
39+
if (p) {
40+
this.pending.delete(msg.id);
41+
msg.error ? p.reject(new Error(msg.error.message)) : p.resolve(msg.result);
42+
}
43+
}
44+
} catch (e) {
45+
console.warn("[reqstool-openspec] Skipping non-JSON line from reqstool mcp:", e instanceof Error ? e.message : e);
46+
}
47+
}
48+
49+
private send(method: string, params: unknown, expectReply = true): Promise<unknown> {
50+
if (!expectReply) {
51+
this.proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
52+
return Promise.resolve();
53+
}
54+
const id = this.id++;
55+
return new Promise((resolve, reject) => {
56+
this.pending.set(id, { resolve, reject });
57+
this.proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
58+
});
59+
}
60+
61+
private async init(): Promise<void> {
62+
await this.send("initialize", {
63+
protocolVersion: "2024-11-05",
64+
capabilities: { tools: {} },
65+
clientInfo: { name: "openspecui", version: "1.0" },
66+
});
67+
this.send("notifications/initialized", {}, false);
68+
}
69+
70+
async enrich(content: string, preset: string): Promise<string> {
71+
await this.ready;
72+
const result = (await this.send("tools/call", {
73+
name: "enrich_document",
74+
arguments: { content, preset },
75+
})) as { content: { text: string }[] };
76+
return result.content[0].text;
77+
}
78+
79+
close() {
80+
this.proc.stdin?.end();
81+
this.proc.kill();
82+
}
83+
}
84+
85+
let client: McpStdioClient | null = null;
86+
87+
export const onReadDocument: OnReadDocumentHookV1 = async (ctx, read) => {
88+
if (!client) {
89+
client = new McpStdioClient(ctx.projectDir);
90+
ctx.lifecycle.onDispose(() => {
91+
client?.close();
92+
client = null;
93+
});
94+
}
95+
96+
const result = await read();
97+
const preset = `openspec:${ctx.document.kind}`;
98+
99+
try {
100+
const enriched = await client.enrich(result.markdown, preset);
101+
return { ...result, markdown: enriched, sourceLabel: `reqstool ${preset}` };
102+
} catch (e) {
103+
return {
104+
...result,
105+
diagnostics: [{ level: "warning", message: `reqstool enrich failed: ${e}` }],
106+
};
107+
}
108+
};

0 commit comments

Comments
 (0)