@analizza-ai/testspec 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +189 -0
  4. package/bin/cli.js +42 -0
  5. package/package.json +69 -0
  6. package/src/adapters/agents/claude.js +88 -0
  7. package/src/adapters/agents/copilot.js +39 -0
  8. package/src/adapters/agents/index.js +22 -0
  9. package/src/adapters/sdd/index.js +23 -0
  10. package/src/adapters/sdd/openspec.js +58 -0
  11. package/src/adapters/sdd/speckit.js +19 -0
  12. package/src/commands/generate.js +66 -0
  13. package/src/commands/init.js +112 -0
  14. package/src/commands/report.js +60 -0
  15. package/src/commands/validate.js +68 -0
  16. package/src/core/reporter.js +44 -0
  17. package/src/core/spec-parser.js +141 -0
  18. package/src/core/stub-generator.js +92 -0
  19. package/src/core/testcontainers.js +39 -0
  20. package/src/core/tests-builder.js +120 -0
  21. package/src/index.js +10 -0
  22. package/src/utils/config.js +29 -0
  23. package/src/utils/logger.js +13 -0
  24. package/src/utils/sdd-detector.js +23 -0
  25. package/templates/agent-instructions/AGENTS.md +39 -0
  26. package/templates/agent-instructions/CLAUDE.md +48 -0
  27. package/templates/agent-instructions/copilot.md +52 -0
  28. package/templates/agent-instructions/skills/testspec-apply-qa.md +424 -0
  29. package/templates/agent-instructions/skills/testspec-generate.md +138 -0
  30. package/templates/agent-instructions/skills/testspec-run-qa.md +338 -0
  31. package/templates/agent-instructions/skills/testspec-specify-qa.md +535 -0
  32. package/templates/stubs/jest/unit.template.js +17 -0
  33. package/templates/stubs/junit/unit.template.java +27 -0
  34. package/templates/stubs/pytest/unit.template.py +18 -0
  35. package/templates/stubs/testcontainers/node-pg-kafka.template.js +38 -0
  36. package/templates/stubs/testcontainers/node-pg.template.js +32 -0
  37. package/templates/stubs/testcontainers/spring-pg-kafka.template.java +41 -0
  38. package/templates/stubs/vitest/unit.template.js +19 -0
  39. package/templates/tests-md/default.md +43 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-05-25
11
+
12
+ ### Added
13
+ - `testspec init` — setup wizard for SDD framework + AI agent detection
14
+ - `testspec generate` — reads OpenSpec artifacts → writes `tests.md` + stubs
15
+ - `testspec validate` — maps test run results back to CT-01..N pass/fail
16
+ - `testspec report` — CT coverage/gap report
17
+ - OpenSpec adapter (full implementation)
18
+ - SpecKit adapter (stub, not yet implemented)
19
+ - Claude Code agent adapter (print-to-chat + `--api` mode)
20
+ - GitHub Copilot agent adapter (print-to-chat)
21
+ - Unit test stub templates: Vitest, Jest, Pytest, JUnit
22
+ - Integration test stub templates: Node.js + PostgreSQL, Node.js + PostgreSQL + Kafka, Spring Boot + PostgreSQL + Kafka
23
+ - `tests.md` canonical structure with CT tables, load profile hints, chaos scenarios
24
+ - Agent instruction templates: CLAUDE.md, copilot.md, AGENTS.md
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Diego Lirio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # @analizza-ai/testspec
2
+
3
+ [![npm](https://img.shields.io/npm/v/@analizza-ai/testspec)](https://www.npmjs.com/package/@analizza-ai/testspec)
4
+ [![CI](https://github.com/analizza-ai/testspec/actions/workflows/ci.yml/badge.svg)](https://github.com/analizza-ai/testspec/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/@analizza-ai/testspec)](./LICENSE)
6
+ [![node](https://img.shields.io/node/v/@analizza-ai/testspec)](./package.json)
7
+
8
+ **Spec Driven Test — the test layer for SDD**
9
+
10
+ testspec (SDT) inverts traditional test generation. Instead of writing code first and covering it with tests, SDT drives the entire test lifecycle from your spec artifacts. Specs are the single source of truth.
11
+
12
+ ---
13
+
14
+ ## Test pyramid
15
+
16
+ ```
17
+ ┌─────────────────────────────────────────────────┐
18
+ │ CHAOS ENGINEERING │ ← /testspec-run-qa
19
+ │ (resilience · DR · fault injection) │
20
+ ├─────────────────────────────────────────────────┤
21
+ │ QA LAYER │ ← /testspec-apply-qa
22
+ │ end-to-end tests · load tests (k6/Gatling) │
23
+ ├─────────────────────────────────────────────────┤
24
+ │ DEVELOPER LAYER │ ← testspec generate
25
+ │ unit tests · integration tests (Testcontainers│
26
+ │ PostgreSQL · Kafka · etc.) │
27
+ └─────────────────────────────────────────────────┘
28
+ All layers driven by: tests.md
29
+ tests.md driven by: spec.md + proposal.md + design.md
30
+ ```
31
+
32
+ ---
33
+
34
+ ## What is SDT
35
+
36
+ testspec reads the spec artifacts produced by your SDD framework (OpenSpec, SpecKit, etc.) and generates:
37
+
38
+ 1. **`tests.md`** — a technology-agnostic test document with numbered CT-01..N test cases
39
+ 2. **Unit test stubs** — Jest / Vitest / Pytest / JUnit skeletons named after CTs
40
+ 3. **Integration test stubs** — Testcontainers-based, pre-configured for your stack
41
+
42
+ The QA layer then consumes `tests.md` to generate k6/Gatling scripts without ambiguity.
43
+
44
+ ---
45
+
46
+ ## Quick start
47
+
48
+ ```bash
49
+ # install globally
50
+ npm install -g @analizza-ai/testspec
51
+
52
+ # in your project root (must have openspec/ or similar)
53
+ testspec init # detects SDD framework, selects AI agent, writes config
54
+ testspec generate # reads specs → writes tests.md + stubs
55
+ testspec validate --results test-results.json
56
+ testspec report
57
+ ```
58
+
59
+ ---
60
+
61
+ ## How it works
62
+
63
+ ```
64
+ Spec artifacts (proposal.md · design.md · specs/**/*.md · tasks.md)
65
+
66
+ testspec generate
67
+
68
+ SpecContext (scenarios, rules, contracts, dbAssertions)
69
+
70
+ Agent prompt (printed to chat or sent via --api)
71
+
72
+ tests.md (CT-01..N)
73
+
74
+ Unit stubs + Integration stubs (Testcontainers)
75
+
76
+ QA repo reads tests.md via GitHub MCP
77
+
78
+ k6 / Gatling scripts · chaos scripts
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Supported SDD frameworks
84
+
85
+ | Framework | Status |
86
+ |-----------|--------|
87
+ | OpenSpec (`@fission-ai/openspec`) | ✅ v1 |
88
+ | SpecKit | planned |
89
+ | BMAD | planned |
90
+ | Kiro (AWS) | planned |
91
+ | Custom | planned |
92
+
93
+ ## Supported AI agents
94
+
95
+ | Agent | Status |
96
+ |-------|--------|
97
+ | Claude Code | ✅ print-to-chat + `--api` |
98
+ | GitHub Copilot | ✅ print-to-chat |
99
+
100
+ ## Supported unit test frameworks
101
+
102
+ | Framework | Status |
103
+ |-----------|--------|
104
+ | Vitest | ✅ |
105
+ | Jest | ✅ |
106
+ | Pytest | ✅ |
107
+ | JUnit | ✅ |
108
+
109
+ ## Supported integration runtimes
110
+
111
+ | Runtime | Status |
112
+ |---------|--------|
113
+ | Testcontainers + PostgreSQL | ✅ |
114
+ | Testcontainers + PostgreSQL + Kafka | ✅ |
115
+ | Testcontainers + Spring Boot + Kafka | ✅ |
116
+
117
+ ---
118
+
119
+ ## tests.md format
120
+
121
+ ```yaml
122
+ ---
123
+ feature: Item Creation
124
+ change: item-creation
125
+ generated: 2026-05-25T10:00:00.000Z
126
+ sdd: openspec
127
+ sdt: 0.1.0
128
+ stack: { lang: node, db: postgresql }
129
+ qa-repo: analizza-ai/qa
130
+ ---
131
+
132
+ # Tests — Item Creation
133
+
134
+ ## Scope
135
+ ## Out of scope
136
+
137
+ ## Test cases
138
+
139
+ ### CT-01 — Create item with valid payload
140
+
141
+ | Field | Value |
142
+ |---------------------|------------------------------|
143
+ | Type | integration |
144
+ | Layer | developer |
145
+ | Precondition | User is authenticated |
146
+ | Input | POST /api/items { name: "x" }|
147
+ | Expected output | 201 Created { id: 1 } |
148
+ | DB validation | SELECT id FROM items WHERE id = 1 |
149
+ | Acceptance criteria | · item persisted · id returned |
150
+
151
+ ## Load profile hints
152
+ ## Chaos scenarios
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Configuration
158
+
159
+ `testspec.config.json` in your project root:
160
+
161
+ ```json
162
+ {
163
+ "sdd": "openspec",
164
+ "agent": "claude",
165
+ "unitFramework": "vitest",
166
+ "stubs": { "unit": true, "integration": true },
167
+ "loadHints": true,
168
+ "chaosHints": true,
169
+ "qaRepo": "your-org/qa-repo"
170
+ }
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Adapter extension guide
176
+
177
+ See [docs/adapters-sdd.md](docs/adapters-sdd.md) to add a custom SDD framework adapter.
178
+
179
+ ---
180
+
181
+ ## Contributing
182
+
183
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Issues and PRs welcome.
184
+
185
+ ---
186
+
187
+ ## License
188
+
189
+ MIT © Diego Lirio
package/bin/cli.js ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bin/cli.js
4
+ * Entry point for the testspec CLI.
5
+ */
6
+
7
+ import { program } from 'commander';
8
+ import { readFileSync } from 'fs';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, join } from 'path';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
14
+
15
+ program
16
+ .name('testspec')
17
+ .description('Spec Driven Test — generate tests.md and stubs from SDD specs')
18
+ .version(pkg.version);
19
+
20
+ const { initCommand } = await import('../src/commands/init.js');
21
+ const { generateCommand } = await import('../src/commands/generate.js');
22
+ const { validateCommand } = await import('../src/commands/validate.js');
23
+ const { reportCommand } = await import('../src/commands/report.js');
24
+
25
+ program.addCommand(initCommand);
26
+ program.addCommand(generateCommand);
27
+ program.addCommand(validateCommand);
28
+ program.addCommand(reportCommand);
29
+
30
+ // alias: testspec-generate → generate
31
+ program
32
+ .command('testspec-generate')
33
+ .description('Alias for "generate" — reads specs → writes tests.md + stubs')
34
+ .option('-c, --change <name>', 'target a specific change folder')
35
+ .option('--api', 'call Claude API instead of printing prompt to chat')
36
+ .option('--no-stubs', 'skip stub generation, write tests.md only')
37
+ .action(async (opts) => {
38
+ const { runGenerate } = await import('../src/commands/generate.js');
39
+ await runGenerate(opts);
40
+ });
41
+
42
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@analizza-ai/testspec",
3
+ "version": "0.1.1",
4
+ "description": "Spec Driven Test — the test layer for SDD",
5
+ "type": "module",
6
+ "bin": {
7
+ "testspec": "./bin/cli.js"
8
+ },
9
+ "main": "./src/index.js",
10
+ "exports": {
11
+ ".": "./src/index.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=20.19.0"
15
+ },
16
+ "scripts": {
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "test:coverage": "vitest run --coverage",
20
+ "lint": "eslint src bin tests",
21
+ "lint:fix": "eslint src bin tests --fix"
22
+ },
23
+ "keywords": [
24
+ "sdd",
25
+ "spec-driven",
26
+ "test-generation",
27
+ "testspec",
28
+ "openspec",
29
+ "claude",
30
+ "ai-testing"
31
+ ],
32
+ "author": "Diego Lirio <diegolirio.dl@gmail.com>",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/analizza-ai/testspec.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/analizza-ai/testspec/issues"
40
+ },
41
+ "homepage": "https://github.com/analizza-ai/testspec#readme",
42
+ "files": [
43
+ "bin/",
44
+ "src/",
45
+ "templates/",
46
+ "README.md",
47
+ "LICENSE",
48
+ "CHANGELOG.md"
49
+ ],
50
+ "publishConfig": {
51
+ "access": "public",
52
+ "registry": "https://registry.npmjs.org/"
53
+ },
54
+ "dependencies": {
55
+ "commander": "^12.0.0",
56
+ "gray-matter": "^4.0.3",
57
+ "chalk": "^5.3.0",
58
+ "glob": "^11.0.0",
59
+ "js-yaml": "^4.1.0"
60
+ },
61
+ "devDependencies": {
62
+ "vitest": "^1.6.0",
63
+ "eslint": "^9.0.0",
64
+ "@anthropic-ai/sdk": "^0.30.0"
65
+ },
66
+ "optionalDependencies": {
67
+ "@anthropic-ai/sdk": "^0.30.0"
68
+ }
69
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * src/adapters/agents/claude.js
3
+ * Claude adapter. Builds the SpecContext → tests.md prompt.
4
+ * Default: prints prompt to stdout (agent reads it in chat).
5
+ * With --api flag: calls Claude API (requires ANTHROPIC_API_KEY).
6
+ */
7
+
8
+ import { buildPrompt } from '../../core/tests-builder.js';
9
+ import { log } from '../../utils/logger.js';
10
+
11
+ export class ClaudeAdapter {
12
+ /**
13
+ * @param {object} specContext
14
+ * @param {object} config
15
+ * @param {{ useApi: boolean }} opts
16
+ * @returns {Promise<string>} tests.md content
17
+ */
18
+ async generateTests(specContext, config, opts = {}) {
19
+ const prompt = buildPrompt(specContext, config);
20
+
21
+ if (!opts.useApi) {
22
+ log.info('\n─── Prompt for Claude Code (/testspec-generate) ────────\n');
23
+ console.log(prompt);
24
+ log.info('\n────────────────────────────────────────────────────────\n');
25
+ log.warn('Paste the output above into your Claude Code chat to get tests.md.');
26
+ log.warn('Re-run with --api to call Claude API directly.');
27
+
28
+ // Return a placeholder tests.md so the CLI can write a file
29
+ return buildPlaceholder(specContext);
30
+ }
31
+
32
+ return this.#callApi(prompt, specContext, config);
33
+ }
34
+
35
+ async #callApi(prompt, _specContext, _config) {
36
+ let Anthropic;
37
+ try {
38
+ ({ default: Anthropic } = await import('@anthropic-ai/sdk'));
39
+ } catch {
40
+ log.error('@anthropic-ai/sdk not installed. Run: npm install @anthropic-ai/sdk');
41
+ process.exit(1);
42
+ }
43
+
44
+ const apiKey = process.env.ANTHROPIC_API_KEY;
45
+ if (!apiKey) {
46
+ log.error('ANTHROPIC_API_KEY env var is required for --api mode.');
47
+ process.exit(1);
48
+ }
49
+
50
+ const client = new Anthropic({ apiKey });
51
+ log.info('Calling Claude API…');
52
+
53
+ const message = await client.messages.create({
54
+ model: 'claude-sonnet-4-6',
55
+ max_tokens: 8192,
56
+ messages: [{ role: 'user', content: prompt }],
57
+ });
58
+
59
+ return message.content[0].text;
60
+ }
61
+ }
62
+
63
+ function buildPlaceholder(specContext) {
64
+ const now = new Date().toISOString();
65
+ return `---
66
+ feature: ${specContext.feature}
67
+ change: ${specContext.changeName}
68
+ generated: ${now}
69
+ sdd: ${specContext.sdd}
70
+ sdt: 0.1.0
71
+ status: placeholder — run with --api or paste the prompt into Claude Code
72
+ ---
73
+
74
+ # Tests — ${specContext.feature}
75
+
76
+ > This file was generated as a placeholder. Paste the printed prompt into your Claude Code chat
77
+ > and replace this file with the output, or re-run \`testspec generate --api\`.
78
+
79
+ ## Scope
80
+ _To be filled by agent_
81
+
82
+ ## Out of scope
83
+ _To be filled by agent_
84
+
85
+ ## Test cases
86
+ _To be filled by agent_
87
+ `;
88
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * src/adapters/agents/copilot.js
3
+ * GitHub Copilot adapter. Prints the prompt so Copilot can read it via copilot-instructions.md context.
4
+ * Does not call an API — Copilot reads the prompt in the editor chat.
5
+ */
6
+
7
+ import { buildPrompt } from '../../core/tests-builder.js';
8
+ import { log } from '../../utils/logger.js';
9
+
10
+ export class CopilotAdapter {
11
+ /**
12
+ * @param {object} specContext
13
+ * @param {object} config
14
+ * @returns {Promise<string>} placeholder tests.md
15
+ */
16
+ async generateTests(specContext, config) {
17
+ const prompt = buildPrompt(specContext, config);
18
+
19
+ log.info('\n─── Prompt for GitHub Copilot ───────────────────────────\n');
20
+ console.log(prompt);
21
+ log.info('\n────────────────────────────────────────────────────────\n');
22
+ log.warn('Paste the above into GitHub Copilot Chat to generate tests.md.');
23
+
24
+ const now = new Date().toISOString();
25
+ return `---
26
+ feature: ${specContext.feature}
27
+ change: ${specContext.changeName}
28
+ generated: ${now}
29
+ sdd: ${specContext.sdd}
30
+ sdt: 0.1.0
31
+ status: placeholder — paste the prompt into GitHub Copilot Chat
32
+ ---
33
+
34
+ # Tests — ${specContext.feature}
35
+
36
+ > Paste the printed prompt into GitHub Copilot Chat and replace this file with the output.
37
+ `;
38
+ }
39
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * src/adapters/agents/index.js
3
+ * Registry mapping AI agent names to their adapter implementations.
4
+ */
5
+
6
+ import { ClaudeAdapter } from './claude.js';
7
+ import { CopilotAdapter } from './copilot.js';
8
+
9
+ const registry = {
10
+ claude: ClaudeAdapter,
11
+ copilot: CopilotAdapter,
12
+ };
13
+
14
+ /**
15
+ * Returns an instantiated adapter for the given agent name.
16
+ * @param {string} name
17
+ */
18
+ export function getAdapter(name = 'claude') {
19
+ const Adapter = registry[name.toLowerCase()];
20
+ if (!Adapter) throw new Error(`Unknown agent adapter: "${name}". Supported: ${Object.keys(registry).join(', ')}`);
21
+ return new Adapter();
22
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * src/adapters/sdd/index.js
3
+ * Registry mapping SDD framework names to their adapter implementations.
4
+ */
5
+
6
+ import { OpenSpecAdapter } from './openspec.js';
7
+ import { SpecKitAdapter } from './speckit.js';
8
+
9
+ const registry = {
10
+ openspec: OpenSpecAdapter,
11
+ speckit: SpecKitAdapter,
12
+ };
13
+
14
+ /**
15
+ * Returns an instantiated adapter for the given SDD framework name.
16
+ * @param {string} name
17
+ * @returns {OpenSpecAdapter | SpecKitAdapter}
18
+ */
19
+ export function getAdapter(name = 'openspec') {
20
+ const Adapter = registry[name.toLowerCase()];
21
+ if (!Adapter) throw new Error(`Unknown SDD adapter: "${name}". Supported: ${Object.keys(registry).join(', ')}`);
22
+ return new Adapter();
23
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * src/adapters/sdd/openspec.js
3
+ * Full OpenSpec adapter. Reads change folders under openspec/changes/{name}/
4
+ * and loads proposal.md, design.md, specs/**\/*.md, tasks.md, config.yaml.
5
+ */
6
+
7
+ import { existsSync, readFileSync, readdirSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { glob } from 'glob';
10
+ import yaml from 'js-yaml';
11
+
12
+ export class OpenSpecAdapter {
13
+ /** @returns {string[]} list of change names (non-archived) */
14
+ discoverChanges(root) {
15
+ const changesDir = join(root, 'openspec', 'changes');
16
+ if (!existsSync(changesDir)) return [];
17
+ return readdirSync(changesDir, { withFileTypes: true })
18
+ .filter((e) => e.isDirectory() && e.name !== 'archive')
19
+ .map((e) => e.name)
20
+ .sort();
21
+ }
22
+
23
+ /**
24
+ * Loads all spec artifacts for a change.
25
+ * @returns {{ proposal: string, design: string, specs: string[], tasks: string, config: object }}
26
+ */
27
+ loadArtifacts(root, changeName) {
28
+ const base = join(root, 'openspec', 'changes', changeName);
29
+
30
+ const read = (file) => {
31
+ const p = join(base, file);
32
+ return existsSync(p) ? readFileSync(p, 'utf-8') : '';
33
+ };
34
+
35
+ const specFiles = glob.sync('specs/**/*.md', { cwd: base });
36
+ const specs = specFiles.map((f) => readFileSync(join(base, f), 'utf-8'));
37
+
38
+ const configPath = join(root, 'openspec', 'config.yaml');
39
+ const config = existsSync(configPath)
40
+ ? yaml.load(readFileSync(configPath, 'utf-8'))
41
+ : {};
42
+
43
+ return {
44
+ proposal: read('proposal.md'),
45
+ design: read('design.md'),
46
+ specs,
47
+ specFiles,
48
+ tasks: read('tasks.md'),
49
+ config,
50
+ changeName,
51
+ };
52
+ }
53
+
54
+ /** @returns {string} absolute path where tests.md should be written */
55
+ getOutputPath(root, changeName) {
56
+ return join(root, 'openspec', 'changes', changeName, 'tests.md');
57
+ }
58
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * src/adapters/sdd/speckit.js
3
+ * SpecKit adapter stub. Interface only — not yet implemented.
4
+ * Implement discoverChanges / loadArtifacts / getOutputPath when SpecKit is supported.
5
+ */
6
+
7
+ export class SpecKitAdapter {
8
+ discoverChanges(_root) {
9
+ throw new Error('SpecKit adapter is not yet implemented.');
10
+ }
11
+
12
+ loadArtifacts(_root, _changeName) {
13
+ throw new Error('SpecKit adapter is not yet implemented.');
14
+ }
15
+
16
+ getOutputPath(_root, _changeName) {
17
+ throw new Error('SpecKit adapter is not yet implemented.');
18
+ }
19
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * src/commands/generate.js
3
+ * Core command: reads SDD spec artifacts → builds tests.md + optional stubs.
4
+ * Also registered as the "testspec-generate" alias in bin/cli.js.
5
+ */
6
+
7
+ import { Command } from 'commander';
8
+ import { log } from '../utils/logger.js';
9
+ import { loadConfig } from '../utils/config.js';
10
+ import { getAdapter as getSddAdapter } from '../adapters/sdd/index.js';
11
+ import { getAdapter as getAgentAdapter } from '../adapters/agents/index.js';
12
+ import { parseSpecs } from '../core/spec-parser.js';
13
+ import { generateStubs } from '../core/stub-generator.js';
14
+ import { writeFileSync, mkdirSync } from 'fs';
15
+ import { dirname } from 'path';
16
+
17
+ export const generateCommand = new Command('generate')
18
+ .description('Read specs → write tests.md + optional unit/integration stubs')
19
+ .option('-c, --change <name>', 'target a specific change folder')
20
+ .option('--api', 'call Claude API instead of printing prompt to chat')
21
+ .option('--no-stubs', 'skip stub generation, write tests.md only')
22
+ .action(runGenerate);
23
+
24
+ export async function runGenerate(opts = {}) {
25
+ const config = loadConfig(process.cwd());
26
+ const sddAdapter = getSddAdapter(config.sdd);
27
+ const agentAdapter = getAgentAdapter(config.agent);
28
+
29
+ // 1. Discover and load artifacts
30
+ const changes = sddAdapter.discoverChanges(process.cwd());
31
+ if (changes.length === 0) {
32
+ log.error('No changes found. Run from a project root with SDD artifacts.');
33
+ process.exit(1);
34
+ }
35
+
36
+ const changeName = opts.change || changes[changes.length - 1];
37
+ log.info(`Processing change: ${changeName}`);
38
+
39
+ const artifacts = sddAdapter.loadArtifacts(process.cwd(), changeName);
40
+ const outputPath = sddAdapter.getOutputPath(process.cwd(), changeName);
41
+
42
+ // 2. Parse into SpecContext
43
+ const specContext = parseSpecs(artifacts, config);
44
+ log.info(`Parsed ${specContext.specs.length} spec file(s)`);
45
+
46
+ // 3. Generate tests.md via agent adapter
47
+ const testsContent = await agentAdapter.generateTests(specContext, config, {
48
+ useApi: opts.api ?? false,
49
+ });
50
+
51
+ // 4. Write tests.md
52
+ mkdirSync(dirname(outputPath), { recursive: true });
53
+ writeFileSync(outputPath, testsContent);
54
+ log.success(`tests.md written → ${outputPath}`);
55
+
56
+ // 5. Optional stubs
57
+ const stubsEnabled = opts.stubs !== false && config.stubs?.unit !== false;
58
+ if (stubsEnabled) {
59
+ const stubResults = generateStubs(specContext, config, dirname(outputPath));
60
+ log.success(`Unit stubs: ${stubResults.unit.length} file(s)`);
61
+ log.success(`Integration stubs: ${stubResults.integration.length} file(s)`);
62
+ }
63
+
64
+ const ctCount = (testsContent.match(/^### CT-\d+/gm) || []).length;
65
+ log.info(`\nSummary: ${specContext.specs.length} spec(s) → ${ctCount} CT(s) → tests.md written`);
66
+ }