Skip to content

Commit b7bac87

Browse files
committed
feat(reqstool): dogfood OpenSpec ↔ reqstool traceability
Bootstraps the OpenSpec spec layer and reqstool SSOT for this library, mirroring reqstool-client#407's pattern: - 3 capability specs in openspec/specs/ (annotation-definitions, annotation-processing, yaml-export); docs/reqstool/ SSOT with ANNOTATIONS_001-003 / SVC_ANNOTATIONS_001-003 - Self-applies this library's own @Requirements/@svcs annotations to its own Requirements/SVCs declarations and AbstractAnnotationsProcessor's process()/exportToYAML() methods -- via a new `self-apply` Maven profile (two-pass: install, then re-compile main+test with annotationProcessorPaths pointing at the just-installed jar) - Fixes a real bug along the way: SVCsTests.java's testMultipleSVCs instantiated Requirements instead of SVCs - CI (build.yml): [pypi, main] matrix via shared reqstool/.github actions; self-apply + yq-merge step (main/test annotation outputs land in separate generated-sources dirs with no existing mojo to combine them, unlike the Maven/Gradle plugins) producing target/reqstool/annotations.yml; validate-openspec job - .claude/settings.json (auto-enables reqstool/reqstool-openspec plugins) and CONTRIBUTING.md prerequisites added from the start, per the pattern fixed in reqstool-java-gradle-plugin#69/ reqstool-java-maven-plugin#176/reqstool-python-poetry-plugin#132 Found and fixed upstream along the way: reqstool-client#418/#419 -- annotations.schema.json's elementKind enum (and a second, independent CHECK constraint in storage/schema.py) was missing ANNOTATION_TYPE, the ElementKind Java reports for @interface declarations. The CHECK constraint gap was silently swallowing inserts via `INSERT OR IGNORE` with no warning logged, the kind of silent-drift bug this dogfooding rollout's CLI-vs-MCP/schema validation step exists to catch. Validated: reqstool status (3/3 PASS), reqstool validate --strict, openspec validate --specs --strict (3 passed), full test suite green. CLI-only validation performed (no MCP parity check) -- low risk given no skipped/missing-test scenarios are present here, the known CLI/MCP divergence area (reqstool-client#411). Signed-off-by: Jimisola Laursen <jimisola@jimisola.com>
1 parent 65aa6b6 commit b7bac87

19 files changed

Lines changed: 376 additions & 5 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: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,57 @@ on:
1010
- reopened
1111
- synchronize
1212

13+
permissions:
14+
contents: read
15+
1316
jobs:
1417
linting:
1518
name: Check linting
1619
uses: ./.github/workflows/lint.yml
1720
build:
1821
runs-on: ubuntu-latest
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
reqstool-source: [pypi, main]
1926
steps:
2027
- uses: actions/checkout@v7
21-
- name: Build and run tests
28+
- name: Set up JDK
2229
uses: actions/setup-java@v5
2330
with:
2431
java-version: "21"
2532
distribution: "temurin"
26-
- run: mvn clean verify
33+
- name: Build and run tests
34+
run: mvn clean verify
35+
- name: Install into local repository
36+
run: mvn -DskipTests install
37+
- name: Self-apply own annotation processor to own sources
38+
run: mvn compiler:compile compiler:testCompile -Pself-apply
39+
- name: Combine main/test annotation processor output
40+
# The processor's main/test annotation outputs land in separate
41+
# generated-sources directories (no shared mojo to combine them, unlike
42+
# the Maven/Gradle plugins) -- merge them the same way those plugins'
43+
# combineOutput() would, so reqstool status has real, non-fabricated data.
44+
run: |
45+
mkdir -p target/reqstool
46+
{
47+
echo "# yaml-language-server: \$schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/annotations.schema.json"
48+
yq eval-all '. as $item ireduce ({}; . * $item )' \
49+
target/generated-sources/annotations/resources/annotations.yml \
50+
target/generated-test-sources/test-annotations/resources/annotations.yml | grep -v "^#"
51+
} > target/reqstool/annotations.yml
52+
- name: Install reqstool
53+
uses: reqstool/.github/.github/actions/install-reqstool@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
54+
with:
55+
reqstool-source: ${{ matrix.reqstool-source }}
56+
- name: Validate reqstool spec completeness
57+
# not yet available in the latest PyPI release
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+
uses: reqstool/.github/.github/actions/reqstool-status@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
62+
with:
63+
fail-if-incomplete: "true"
64+
65+
validate-openspec:
66+
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
@@ -45,4 +45,5 @@ buildNumber.properties
4545
### VisualStudioCode ###
4646
.vscode/*
4747

48-
.claude/
48+
.claude/*
49+
!.claude/settings.json

.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-java-annotations
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 ANNOTATIONS_NNN / SVC_ANNOTATIONS_NNN ID convention.
29+
annotations:
30+
path: docs/reqstool
31+
req_prefix: "ANNOTATIONS_"
32+
svc_prefix: "SVC_ANNOTATIONS_"

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
- Java 21+
1010
- Maven 3.9+
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

@@ -16,6 +18,18 @@ git clone https://github.com/reqstool/reqstool-java-annotations.git
1618
cd reqstool-java-annotations
1719
```
1820

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

2135
```bash

docs/reqstool/reqstool_config.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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: java
4+
build: maven
5+
resources:
6+
requirements: requirements.yml
7+
software_verification_cases: software_verification_cases.yml
8+
annotations: ../../target/reqstool/annotations.yml
9+
test_results:
10+
- ../../target/surefire-reports/*.xml

docs/reqstool/requirements.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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-java-annotations
5+
variant: microservice
6+
title: Reqstool Java Annotations
7+
url: https://github.com/reqstool/reqstool-java-annotations
8+
9+
requirements:
10+
- id: ANNOTATIONS_001
11+
title: Annotation definitions
12+
significance: shall
13+
description: >-
14+
The library shall define @Requirements (targeting types, methods, and fields) and
15+
@SVCs (targeting types and methods) annotations, each retaining a string array of
16+
IDs at source level only.
17+
categories:
18+
- functional-suitability
19+
revision: "0.1.0"
20+
21+
- id: ANNOTATIONS_002
22+
title: Annotation element collection
23+
significance: shall
24+
description: >-
25+
The annotation processor shall collect every element annotated with a supported
26+
annotation across all compilation rounds, accumulating multiple element locations
27+
against the same requirement or SVC ID.
28+
categories:
29+
- functional-suitability
30+
revision: "0.1.0"
31+
32+
- id: ANNOTATIONS_003
33+
title: YAML export
34+
significance: shall
35+
description: >-
36+
The annotation processor shall serialize collected annotation locations to an
37+
annotations.yml resource at the standard SOURCE_OUTPUT location, with a JSON
38+
schema header comment and the appropriate implementations/tests element key for
39+
@Requirements and @SVCs respectively.
40+
categories:
41+
- functional-suitability
42+
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_ANNOTATIONS_001
5+
requirement_ids: ["ANNOTATIONS_001"]
6+
title: "Verify @Requirements and @SVCs retain single and multiple string values"
7+
verification: automated-test
8+
revision: "0.1.0"
9+
10+
- id: SVC_ANNOTATIONS_002
11+
requirement_ids: ["ANNOTATIONS_002"]
12+
title: "Verify the processor collects multiple element locations for the same ID, end to end"
13+
verification: automated-test
14+
revision: "0.1.0"
15+
16+
- id: SVC_ANNOTATIONS_003
17+
requirement_ids: ["ANNOTATIONS_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+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Annotation Definitions Specification
2+
3+
## Purpose
4+
5+
Requirement and SVC content is owned by reqstool (single source of truth). This spec references
6+
reqstool requirement and SVC IDs only; titles and descriptions are injected at read time via
7+
`reqstool enrich` (or the openspecui hook). See `docs/reqstool/`.
8+
9+
## Requirements
10+
11+
### Requirement: ANNOTATIONS_001
12+
The system SHALL implement ANNOTATIONS_001.
13+
14+
#### Scenario: SVC_ANNOTATIONS_001
15+
The system SHALL pass SVC_ANNOTATIONS_001.

0 commit comments

Comments
 (0)