Skip to content

Commit f6a370b

Browse files
feat(reqstool): add OpenSpec ↔ reqstool dogfooding layer (#68)
* feat(reqstool): add OpenSpec ↔ reqstool dogfooding layer Apply the reqstool-client#407 dogfooding pattern to this repo: tag RequirementsToolTask's four behaviourally-distinct methods with @requirements, add a docs/reqstool SSOT (requirements.yml, software_verification_cases.yml, reqstool_config.yml), add matching thin OpenSpec capability specs, add .reqstool-ai.yaml, and close test-coverage gaps (assembleZipArtifact's happy path and skipAssembleZipArtifact were previously untested) so every SVC has a real verifying test. Wire CI to install reqstool, merge the annotation processor's own main/test output into a combined annotations.yml (true self-application via mavenLocal-style re-application isn't wired up for Gradle yet), and gate on `reqstool status --check-all-reqs-met` plus `openspec validate --specs --strict`. Unlike the Maven plugin, Gradle's annotationProcessor/testAnnotationProcessor configurations auto-discover the processor without any extra wiring. Signed-off-by: Jimisola Laursen <jimisola@jimisola.com> * Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Jimisola Laursen <jimisola@jimisola.com> --------- Signed-off-by: Jimisola Laursen <jimisola@jimisola.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent a0e807b commit f6a370b

12 files changed

Lines changed: 361 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,16 @@ on:
1010
- reopened
1111
- synchronize
1212

13+
permissions:
14+
contents: read
15+
1316
jobs:
1417
build:
1518
runs-on: ubuntu-latest
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
reqstool-source: [pypi, main]
1623
steps:
1724
- uses: actions/checkout@v7
1825
- name: Set up Java
@@ -22,3 +29,32 @@ jobs:
2229
distribution: "temurin"
2330
- name: Build
2431
run: ./gradlew clean build
32+
- name: Self-apply plugin to assemble dataset zip
33+
# True self-application (publishing to mavenLocal and re-applying the plugin
34+
# to its own sources from a separate project) is not yet wired up for Gradle.
35+
# As an interim measure, merge the annotation processor's own main/test output
36+
# (already produced by `./gradlew build` above) the same way the plugin's
37+
# combineOutput() would, so reqstool status has real, non-fabricated data.
38+
run: |
39+
mkdir -p build/reqstool
40+
{
41+
echo "# yaml-language-server: \$schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/annotations.schema.json"
42+
yq eval-all '. as $item ireduce ({}; . * $item )' \
43+
build/generated/sources/annotationProcessor/java/main/resources/annotations.yml \
44+
build/generated/sources/annotationProcessor/java/test/resources/annotations.yml | grep -v "^#"
45+
} > build/reqstool/annotations.yml
46+
- name: Install reqstool
47+
uses: reqstool/.github/.github/actions/install-reqstool@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
48+
with:
49+
reqstool-source: ${{ matrix.reqstool-source }}
50+
- name: Validate reqstool spec completeness
51+
# not yet available in the latest PyPI release
52+
if: matrix.reqstool-source == 'main'
53+
uses: reqstool/.github/.github/actions/validate-reqstool@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
54+
- name: Run reqstool status
55+
uses: reqstool/.github/.github/actions/reqstool-status@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22
56+
with:
57+
fail-if-incomplete: "true"
58+
59+
validate-openspec:
60+
uses: reqstool/.github/.github/workflows/common-validate-openspec.yml@9c6feaab046f4782f430dd2527fdb82c2a5cd926 # main 2026-06-22

.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-gradle-plugin
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 GRADLE_PLUGIN_NNN / SVC_GRADLE_PLUGIN_NNN ID convention.
29+
plugin:
30+
path: docs/reqstool
31+
req_prefix: "GRADLE_PLUGIN_"
32+
svc_prefix: "SVC_GRADLE_PLUGIN_"

build.gradle

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,16 @@ dependencies {
4545
// Jackson for YAML processing
4646
implementation 'com.fasterxml.jackson.core:jackson-databind:2.22.0'
4747
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.22.0'
48-
48+
4949
// SnakeYAML for reqstool_config.yml generation
5050
implementation 'org.yaml:snakeyaml:2.6'
51-
51+
52+
// reqstool dogfooding: @Requirements/@SVCs annotations + processor
53+
implementation 'io.github.reqstool:reqstool-java-annotations:1.0.0'
54+
annotationProcessor 'io.github.reqstool:reqstool-java-annotations:1.0.0'
55+
testImplementation 'io.github.reqstool:reqstool-java-annotations:1.0.0'
56+
testAnnotationProcessor 'io.github.reqstool:reqstool-java-annotations:1.0.0'
57+
5258
// Testing - JUnit BOM for version management
5359
testImplementation platform('org.junit:junit-bom:6.1.0')
5460
testImplementation 'org.junit.jupiter:junit-jupiter-api'

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: gradle
5+
resources:
6+
requirements: requirements.yml
7+
software_verification_cases: software_verification_cases.yml
8+
annotations: ../../build/reqstool/annotations.yml
9+
test_results:
10+
- ../../build/test-results/test/*.xml

docs/reqstool/requirements.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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-gradle-plugin
5+
variant: microservice
6+
title: Reqstool Java Gradle Plugin
7+
url: https://github.com/reqstool/reqstool-java-gradle-plugin
8+
9+
requirements:
10+
- id: GRADLE_PLUGIN_001
11+
title: Annotation combination
12+
significance: shall
13+
description: >-
14+
The plugin shall combine requirement implementation annotations and SVC test
15+
annotations gathered from multiple source sets into a single annotations
16+
dataset, merging test entries for SVC IDs that appear in more than one
17+
source set.
18+
categories:
19+
- functional-suitability
20+
revision: "0.1.0"
21+
22+
- id: GRADLE_PLUGIN_002
23+
title: Zip artifact assembly
24+
significance: shall
25+
description: >-
26+
The plugin shall assemble a reqstool dataset ZIP artifact containing the
27+
requirements dataset files, the combined annotations file, matching test
28+
result files, and a generated reqstool_config.yml.
29+
categories:
30+
- functional-suitability
31+
revision: "0.1.0"
32+
33+
- id: GRADLE_PLUGIN_003
34+
title: Skip flags
35+
significance: shall
36+
description: >-
37+
The plugin shall support skip flags that bypass task execution entirely
38+
or bypass only ZIP artifact assembly, while still combining annotations.
39+
categories:
40+
- functional-suitability
41+
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_GRADLE_PLUGIN_001
5+
requirement_ids: ["GRADLE_PLUGIN_001"]
6+
title: "Verify implementation and test annotations are combined and merged across source sets"
7+
verification: automated-test
8+
revision: "0.1.0"
9+
10+
- id: SVC_GRADLE_PLUGIN_002
11+
requirement_ids: ["GRADLE_PLUGIN_002"]
12+
title: "Verify the zip artifact is assembled with the mandatory and optional reqstool resources"
13+
verification: automated-test
14+
revision: "0.1.0"
15+
16+
- id: SVC_GRADLE_PLUGIN_003
17+
requirement_ids: ["GRADLE_PLUGIN_003"]
18+
title: "Verify skip and skipAssembleZipArtifact each bypass their respective step"
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 Combination 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: GRADLE_PLUGIN_001
12+
The system SHALL implement GRADLE_PLUGIN_001.
13+
14+
#### Scenario: SVC_GRADLE_PLUGIN_001
15+
The system SHALL pass SVC_GRADLE_PLUGIN_001.

openspec/specs/skip-flags/spec.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Skip Flags 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: GRADLE_PLUGIN_003
12+
The system SHALL implement GRADLE_PLUGIN_003.
13+
14+
#### Scenario: SVC_GRADLE_PLUGIN_003
15+
The system SHALL pass SVC_GRADLE_PLUGIN_003.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Zip Assembly 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: GRADLE_PLUGIN_002
12+
The system SHALL implement GRADLE_PLUGIN_002.
13+
14+
#### Scenario: SVC_GRADLE_PLUGIN_002
15+
The system SHALL pass SVC_GRADLE_PLUGIN_002.

0 commit comments

Comments
 (0)