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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [4.2.0] - 2026-04-12

### Added

- **`testing.md` companion file** — a new default companion file scaffolded alongside every spec. Contains sections for automated test locations, manual QA checklists, and edge cases/boundary conditions. Generated by `specsync generate`, `specsync add-spec`, and `specsync new --full` (#225).
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "specsync"
version = "4.1.3"
version = "4.2.0"
edition = "2024"
rust-version = "1.85"
description = "Bidirectional spec-to-code validation with schema column checking — 11 languages, single binary"
Expand Down
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ spec: auth.spec.md
spec: auth.spec.md
---

## Automated Tests
## Automated Testing
<!-- Test file locations and what they cover -->

## Manual QA Checklist
Expand Down Expand Up @@ -523,6 +523,42 @@ These files are designed for team coordination and AI agent context — they giv

---

## YAML Source Files

SpecSync extracts symbols from YAML files (`.yml`, `.yaml`) including GitHub Actions workflows, Docker Compose files, and Kubernetes manifests.

**What gets extracted:**
- **Top-level keys** — any key at column 0 (e.g., `name`, `on`, `jobs`)
- **Nested children of well-known parents** — `jobs.test`, `services.web`, `volumes.pgdata`, etc. Parents: `jobs`, `services`, `volumes`, `networks`, `secrets`, `stages`, `steps`, `targets`, `outputs`, `inputs`, `permissions`, `deployments`
- **YAML anchors** — `&anchor-name` definitions

**Indentation:** Any consistent indentation (2-space, 4-space, or tabs) is supported for nested key extraction.

### Document Separator Edge Cases

YAML supports multi-document files separated by `---` (common in Kubernetes manifests). SpecSync handles these correctly:

```yaml
# Multi-document YAML (e.g., k8s manifests)
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
---
apiVersion: v1
kind: Service
metadata:
name: my-svc
```

- Top-level keys from **all documents** are extracted (`apiVersion`, `kind`, `metadata` from both)
- The `---` separator is not confused with spec frontmatter delimiters — frontmatter parsing only applies to `.spec.md` files
- Duplicate keys across documents are deduplicated in the symbol list

> **Note:** SpecSync's YAML extractor uses regex-based parsing, not a full YAML parser. This means it works without any YAML library dependency but does not interpret YAML semantics like merge keys (`<<: *alias`) beyond anchor extraction.

---

## VS Code Extension

Install from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=corvidlabs.specsync) or search "SpecSync" in the Extensions panel.
Expand Down
3 changes: 2 additions & 1 deletion specs/cmd_import/cmd_import.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Implements the `specsync import` command. Imports specs from external systems (G

1. Supported sources: `github`, `jira`, `confluence`
2. GitHub import resolves repo from config, CLI flag, or git remote
3. Creates spec and companion files
3. Creates spec and companion files (tasks.md, context.md, requirements.md, testing.md); design.md is generated only when `companions.design` is enabled in config
4. Will not overwrite existing spec

## Behavioral Examples
Expand Down Expand Up @@ -72,3 +72,4 @@ Implements the `specsync import` command. Imports specs from external systems (G
| Date | Change |
|------|--------|
| 2026-04-09 | Initial spec |
| 2026-04-13 | Document testing.md and conditional design.md in companion generation |
5 changes: 3 additions & 2 deletions specs/cmd_new/cmd_new.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Implements the `specsync new` command. Quick-creates a minimal spec with auto-de

1. Auto-detects source files by scanning source dirs for module name matches
2. Extracts exports to pre-populate Public API tables
3. `--full` generates companion files via `generator::generate_companion_files()`
3. `--full` generates companion files (tasks.md, context.md, requirements.md, testing.md) via `generator::generate_companion_files_for_spec()`; design.md is included only when `companions.design` is enabled in config
4. Includes custom `chrono_lite_today()` for dates without chrono dependency
5. Will not overwrite existing spec

Expand All @@ -46,7 +46,7 @@ Implements the `specsync new` command. Quick-creates a minimal spec with auto-de

- **Given** `--full` flag
- **When** `cmd_new` runs
- **Then** creates spec.md, tasks.md, context.md, requirements.md
- **Then** creates spec.md, tasks.md, context.md, requirements.md, testing.md (and design.md if `companions.design` is enabled)

## Error Cases

Expand Down Expand Up @@ -77,3 +77,4 @@ Implements the `specsync new` command. Quick-creates a minimal spec with auto-de
| Date | Change |
|------|--------|
| 2026-04-09 | Initial spec |
| 2026-04-13 | Document testing.md and conditional design.md in companion generation |
5 changes: 3 additions & 2 deletions specs/cmd_scaffold/cmd_scaffold.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ Implements `specsync add-spec` and `specsync scaffold` commands. Creates new spe
1. Both scan source dirs for module name matches
2. `cmd_scaffold` supports custom templates and auto-appends to registry
3. Neither overwrites existing specs
4. Companion files always generated
4. Companion files (tasks.md, context.md, requirements.md, testing.md) are always generated; design.md is generated only when `companions.design` is enabled in config

## Behavioral Examples

### Scenario: Scaffold with auto-detection

- **Given** `src/auth.rs` exists
- **When** `cmd_add_spec(root, "auth")` runs
- **Then** creates spec with detected sources and companions
- **Then** creates spec with detected sources and companions (including design.md if `companions.design` is enabled)

## Error Cases

Expand Down Expand Up @@ -73,3 +73,4 @@ Implements `specsync add-spec` and `specsync scaffold` commands. Creates new spe
| Date | Change |
|------|--------|
| 2026-04-09 | Initial spec |
| 2026-04-13 | Document companions.design flag for conditional design.md generation |
3 changes: 2 additions & 1 deletion specs/cmd_wizard/cmd_wizard.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Implements the `specsync wizard` command — an interactive TUI wizard for creat
3. Refuses to overwrite existing spec (exits 1 if spec dir already exists)
4. Source file auto-detection scans `source_dirs` for files matching the module name/directory
5. Template types: Generic, API Endpoint, Data Model, Utility, UI Component — each adds template-specific sections
6. Companion files (tasks.md, context.md, requirements.md) are always generated
6. Companion files (tasks.md, context.md, requirements.md, testing.md) are always generated; design.md is generated only when `companions.design` is enabled in config
7. Shows a full preview of the spec before asking for write confirmation

## Behavioral Examples
Expand Down Expand Up @@ -79,3 +79,4 @@ Implements the `specsync wizard` command — an interactive TUI wizard for creat
| Date | Change |
|------|--------|
| 2026-04-09 | Initial spec |
| 2026-04-13 | Document testing.md and conditional design.md in companion generation |
4 changes: 2 additions & 2 deletions specs/exports/exports.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Each language backend exposes a single `extract_exports(content: &str) -> Vec<St
| C# | `csharp.rs` | `public class/struct/interface/enum/record/delegate` types and `public` members; handles `static`, `partial`, `sealed`, `abstract`, `virtual`, `override`, `async` modifiers |
| PHP | `php.rs` | `class/interface/trait/enum` types (always public); `public`/unqualified `function` and `const` declarations; skips `private`/`protected` members and `__` magic methods; handles `abstract`, `final`, `readonly`, `static` modifiers; strips `//`, `/* */`, and `#` comments |
| Ruby | `ruby.rs` | `class`/`module` declarations; top-level `def` (always public); class methods with visibility tracking (`public`→`private`→`protected`→`public` toggles); `CONSTANT` assignments; `attr_accessor`/`attr_reader`/`attr_writer` symbols; skips `_`-prefixed names and `initialize`; strips `#` and `=begin/=end` comments |
| YAML | `yaml.rs` | Top-level mapping keys from `.yaml`/`.yml` files; supports anchors and aliases |
| YAML | `yaml.rs` | Top-level mapping keys from `.yaml`/`.yml` files; named entries under well-known parent keys (e.g., `jobs.test`, `services.web`); YAML anchors (`&name`) |

## Invariants

Expand All @@ -106,7 +106,7 @@ Each language backend exposes a single `extract_exports(content: &str) -> Vec<St
16. Go backend deduplicates methods that might also match top-level declarations
17. PHP backend treats types (class/interface/trait/enum) as always public; methods and constants require `public` or unqualified visibility; `private`/`protected` are excluded; magic methods (`__construct`, `__toString`, etc.) are excluded
18. Ruby backend tracks visibility state via `public`/`private`/`protected` toggle statements; defaults to public; `initialize` is excluded; `_`-prefixed names are excluded; `attr_accessor`/`attr_reader`/`attr_writer` emit attribute names as symbols
19. YAML backend extracts top-level mapping keys; no test file patterns (YAML files are not test files)
19. YAML backend extracts top-level mapping keys, named entries under well-known parent keys (any indentation level), and anchors; no test file patterns (YAML files are not test files)

## Behavioral Examples

Expand Down
24 changes: 19 additions & 5 deletions specs/generator/generator.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Scaffolds spec files and companion files (tasks.md, context.md, requirements.md,
| `find_files_for_module` | `root, module_name, config` | `Vec<String>` | Find source files for a module by checking config definitions, subdirectories, then flat files |
| `generate_spec` | `module_name, source_files, root, specs_dir` | `String` | Generate a spec from a template (custom or language-aware default) |
| `generate_spec_from_custom_template` | `template_dir, module_name, source_files, root` | `String` | Generate a spec using files from a custom template directory |
| `generate_companion_files_from_template` | `spec_dir, module_name, template_dir` | `()` | Generate companion files from a custom template directory with fallback to defaults |
| `generate_companion_files_from_template` | `spec_dir, module_name, template_dir, design_enabled` | `()` | Generate companion files from a custom template directory with fallback to defaults; creates design.md only when `design_enabled` is true |

## Invariants

Expand All @@ -39,24 +39,37 @@ Scaffolds spec files and companion files (tasks.md, context.md, requirements.md,
3. Template generation fills in module name, version (1), status (draft), and discovered source files
4. Module title is derived from the module name with dashes converted to title case (e.g. "api-gateway" -> "Api Gateway")
5. Companion files (tasks.md, context.md, requirements.md, testing.md, and design.md when enabled) are only created if they don't already exist
6. AI generation falls back to template on failure (with a warning to stderr)
7. Source file paths in frontmatter are relative to the project root
8. Module source files are discovered by checking subdirectory-based modules first, then flat files
6. The design.md template includes its own YAML frontmatter with `spec:` (back-reference to the parent spec) and `sources:` (list of design asset references — Figma URLs, image paths, etc.). This frontmatter is companion-level metadata, not parsed by the spec validation pipeline
7. AI generation falls back to template on failure (with a warning to stderr)
8. Source file paths in frontmatter are relative to the project root
9. Module source files are discovered by checking subdirectory-based modules first, then flat files

## Behavioral Examples

### Scenario: Generate spec for unspecced module

- **Given** a module "auth" with source files in `src/auth/` and no existing spec
- **When** `generate_specs_for_unspecced_modules` is called
- **Then** creates `specs/auth/auth.spec.md`, `specs/auth/tasks.md`, `specs/auth/context.md`, `specs/auth/requirements.md`, and `specs/auth/testing.md`
- **Then** creates `specs/auth/auth.spec.md`, `specs/auth/tasks.md`, `specs/auth/context.md`, `specs/auth/requirements.md`, `specs/auth/testing.md`, and `specs/auth/design.md` if `companions.design` is enabled in config

### Scenario: Skip existing spec

- **Given** a module "auth" that already has `specs/auth/auth.spec.md`
- **When** `generate_specs_for_unspecced_modules` is called
- **Then** skips the module, returns 0

### Scenario: Design companion opt-in

- **Given** `companions.design` is enabled in config
- **When** `generate_companion_files_for_spec` is called for module "dashboard"
- **Then** creates design.md with YAML frontmatter (`spec: dashboard.spec.md`, `sources: []`) and sections for Layout, Components, Tokens, Assets

### Scenario: Design companion disabled by default

- **Given** no `companions.design` config (default: false)
- **When** `generate_companion_files_for_spec` is called
- **Then** creates tasks.md, context.md, requirements.md, testing.md but NOT design.md

### Scenario: AI generation fallback

- **Given** an AI provider that fails with an error
Expand Down Expand Up @@ -96,3 +109,4 @@ Scaffolds spec files and companion files (tasks.md, context.md, requirements.md,
| 2026-03-25 | Initial spec |
| 2026-04-07 | Document find_files_for_module, generate_spec, generate_spec_from_custom_template, generate_companion_files_from_template |
| 2026-04-12 | Update companion files list to include requirements.md, testing.md, and opt-in design.md; add design_enabled parameter |
| 2026-04-13 | Fix generate_companion_files_from_template signature to include design_enabled; update scenario for conditional design.md |
9 changes: 8 additions & 1 deletion specs/hash_cache/hash_cache.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Uses SHA-256 content hashing to track which spec files, companion files, and sou
3. Unreadable files are treated as "changed" (conservative — triggers re-validation)
4. SHA-256 is computed in 8KB chunks for memory efficiency on large files
5. Path keys are normalized for cross-platform consistency (forward slashes)
6. Companion file detection covers both naming conventions: plain (`requirements.md`) and prefixed (`{module}.req.md`)
6. Companion file detection covers all five companion types (requirements.md, context.md, tasks.md, testing.md, design.md) in both naming conventions: plain (`requirements.md`) and prefixed (`{module}.req.md`)
7. `update_cache` prunes entries for deleted files to prevent unbounded cache growth
8. `extract_frontmatter_files` uses quick string matching — does not invoke the full YAML parser

Expand Down Expand Up @@ -80,6 +80,12 @@ Uses SHA-256 content hashing to track which spec files, companion files, and sou
- **When** `classify_changes` is called for the parent spec
- **Then** returns `ChangeClassification` with `ChangeKind::Requirements`

### Scenario: Design or testing companion change detected

- **Given** `testing.md` or `design.md` has been modified
- **When** `classify_changes` is called for the parent spec
- **Then** returns `ChangeClassification` with `ChangeKind::Companion`

## Error Cases

| Condition | Behavior |
Expand Down Expand Up @@ -110,3 +116,4 @@ Uses SHA-256 content hashing to track which spec files, companion files, and sou
|------|--------|
| 2026-04-10 | Populated requirements.md with user stories, acceptance criteria, constraints, and out-of-scope items |
| 2026-04-06 | Initial spec for v3.3.0 |
| 2026-04-13 | Document design.md and testing.md in companion file detection list |
77 changes: 77 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,13 @@ pub fn config_to_toml(config: &SpecSyncConfig) -> String {
}
}

// Companions section
if config.companions.design {
lines.push(String::new());
lines.push("[companions]".to_string());
lines.push("design = true".to_string());
}

lines.push(String::new()); // trailing newline
lines.join("\n")
}
Expand Down Expand Up @@ -552,6 +559,7 @@ const KNOWN_JSON_KEYS: &[&str] = &[
"github",
"enforcement",
"lifecycle",
"companions",
];

fn load_json_config(config_path: &Path, root: &Path) -> SpecSyncConfig {
Expand Down Expand Up @@ -642,6 +650,10 @@ fn load_toml_config(config_path: &Path, root: &Path) -> SpecSyncConfig {
parse_toml_lifecycle_key(key, value, &mut config.lifecycle);
continue;
}
"companions" => {
parse_toml_companions_key(key, value, &mut config.companions);
continue;
}
s if s.starts_with("lifecycle.") => {
parse_toml_lifecycle_nested(s, key, value, &mut config.lifecycle);
continue;
Expand Down Expand Up @@ -822,6 +834,20 @@ fn parse_toml_github_key(key: &str, value: &str, config: &mut SpecSyncConfig) {
}
}

/// Parse a key=value pair inside a `[companions]` TOML section.
fn parse_toml_companions_key(
key: &str,
value: &str,
companions: &mut crate::types::CompanionConfig,
) {
match key {
"design" => companions.design = parse_toml_bool(value),
_ => {
eprintln!("Warning: unknown key \"{key}\" in [companions] section (ignored)");
}
}
}

/// Parse a key=value pair inside a `[lifecycle]` TOML section.
fn parse_toml_lifecycle_key(key: &str, value: &str, lc: &mut crate::types::LifecycleConfig) {
match key {
Expand Down Expand Up @@ -1321,4 +1347,55 @@ verify_issues = false
let caps = pattern.captures("CREATE TABLE users (id INT)").unwrap();
assert_eq!(&caps[1], "users");
}

// --- companions config ---

#[test]
fn test_toml_companions_design_enabled() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
fs::create_dir_all(root.join(".specsync")).unwrap();
fs::write(
root.join(".specsync/config.toml"),
"specs_dir = \"specs\"\nsource_dirs = [\"src\"]\n\n[companions]\ndesign = true\n",
)
.unwrap();
let config = load_config(root);
assert!(
config.companions.design,
"design companion should be enabled"
);
}

#[test]
fn test_toml_companions_design_default_false() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
fs::create_dir_all(root.join(".specsync")).unwrap();
fs::write(
root.join(".specsync/config.toml"),
"specs_dir = \"specs\"\nsource_dirs = [\"src\"]\n",
)
.unwrap();
let config = load_config(root);
assert!(
!config.companions.design,
"design companion should default to false"
);
}

#[test]
fn test_config_to_toml_roundtrips_companions() {
let mut config = SpecSyncConfig::default();
config.companions.design = true;
let toml_str = config_to_toml(&config);
assert!(
toml_str.contains("[companions]"),
"should contain [companions] section"
);
assert!(
toml_str.contains("design = true"),
"should contain design = true"
);
}
}
Loading
Loading