Skip to content

Commit ca6e7e3

Browse files
authored
Merge branch 'main' into feat/custom-validation-rules
2 parents 571cfcd + 1270132 commit ca6e7e3

28 files changed

Lines changed: 1956 additions & 68 deletions

Cargo.lock

Lines changed: 59 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ walkdir = "2"
2525
colored = "3"
2626
notify = "7"
2727
notify-debouncer-full = "0.4"
28+
tree-sitter = "0.24"
29+
tree-sitter-typescript = "0.23"
30+
tree-sitter-python = "0.23"
31+
tree-sitter-rust = "0.23"
2832
sha2 = "0.10"
2933
ureq = { version = "3", features = ["json"] }
3034
dialoguer = "0.11"

specs/cmd_check/cmd_check.spec.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ tracks: []
99
depends_on:
1010
- specs/commands/commands.spec.md
1111
- specs/ai/ai.spec.md
12+
- specs/git_utils/git_utils.spec.md
1213
- specs/hash_cache/hash_cache.spec.md
1314
- specs/ignore/ignore.spec.md
1415
- specs/output/output.spec.md

specs/cmd_report/cmd_report.spec.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ db_tables: []
88
tracks: []
99
depends_on:
1010
- specs/commands/commands.spec.md
11+
- specs/git_utils/git_utils.spec.md
1112
- specs/parser/parser.spec.md
1213
- specs/types/types.spec.md
1314
- specs/validator/validator.spec.md

specs/cmd_stale/cmd_stale.spec.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
module: cmd_stale
3+
version: 1
4+
status: stable
5+
files:
6+
- src/commands/stale.rs
7+
db_tables: []
8+
tracks:
9+
- 188
10+
depends_on:
11+
- specs/commands/commands.spec.md
12+
- specs/git_utils/git_utils.spec.md
13+
- specs/parser/parser.spec.md
14+
- specs/types/types.spec.md
15+
---
16+
17+
# Cmd Stale
18+
19+
## Purpose
20+
21+
Implements the `specsync stale` command — a focused staleness detection tool that identifies specs whose source files have diverged via git commit history. Reports which specs need updating, how many commits they are behind, and which specific source files have drifted. Supports text, JSON, markdown, and GitHub output formats.
22+
23+
## Public API
24+
25+
### Exported Functions
26+
27+
| Function | Parameters | Returns | Description |
28+
|----------|-----------|---------|-------------|
29+
| `cmd_stale` | `root: &Path, format: OutputFormat, threshold: usize` | `()` | Detect and report stale specs based on git commit distance |
30+
31+
## Invariants
32+
33+
1. Staleness is determined by `git_commits_between`: a spec is stale when any source file has >= `threshold` commits since the spec was last modified (default: 5)
34+
2. Specs with no `files` in frontmatter are counted as fresh (no source files to compare against)
35+
3. Specs not yet tracked by git (no commit history) are skipped and counted as fresh
36+
4. Results are sorted by most-stale-first (highest `max_commits_behind`)
37+
5. Exit code is 1 when any stale specs are found, 0 when all are fresh
38+
6. Requires a git repository — exits with error if `is_git_repo` returns false
39+
40+
## Behavioral Examples
41+
42+
### Scenario: All specs fresh
43+
44+
- **Given** all specs were updated after their source files
45+
- **When** `specsync stale` is run
46+
- **Then** prints "All specs are up to date" and exits 0
47+
48+
### Scenario: Spec behind source by 8 commits (threshold 5)
49+
50+
- **Given** module "auth" has source file `src/auth.rs` with 8 commits since spec was last updated
51+
- **When** `specsync stale --threshold 5` is run
52+
- **Then** reports auth as stale with "8 commits behind" and exits 1
53+
54+
### Scenario: JSON output
55+
56+
- **Given** 2 stale specs out of 10 total
57+
- **When** `specsync stale --format json` is run
58+
- **Then** outputs JSON with `total_specs: 10`, `stale_count: 2`, `stale_specs` array with per-file details
59+
60+
## Error Cases
61+
62+
| Condition | Behavior |
63+
|-----------|----------|
64+
| Not a git repository | Prints error, exits 1 |
65+
| Spec file unreadable | Skipped silently |
66+
| No frontmatter | Skipped silently |
67+
| Source file doesn't exist on disk | Skipped in commit distance check |
68+
69+
## Dependencies
70+
71+
- `git_utils` — git commit history queries
72+
- `parser` — frontmatter parsing for module name and files list
73+
- `commands``load_and_discover` for config and spec file discovery
74+
75+
## Change Log
76+
77+
| Date | Change |
78+
|------|--------|
79+
| 2026-04-10 | Initial — dedicated staleness detection command (closes #188) |

specs/cmd_stale/requirements.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
spec: cmd_stale.spec.md
3+
---
4+
5+
## User Stories
6+
7+
- As a developer, I want to know which specs have drifted from their source files so I can update them
8+
- As a CI operator, I want staleness checks integrated into the validation pipeline so drift is caught early
9+
10+
## Acceptance Criteria
11+
12+
- `specsync stale` lists specs whose source files have changed since the spec was last modified
13+
- Reports include commit count, changed file list, and last commit details
14+
- JSON output mode (`--format json`) produces machine-readable staleness data
15+
- `specsync check --stale` integrates drift warnings into the standard check pipeline
16+
- Exit code is non-zero when stale specs are detected (for CI usage)
17+
18+
## Constraints
19+
20+
- Must not panic on expected error conditions — return Results or print and exit
21+
- Must work with the project's Clap-based CLI argument parsing
22+
- Git operations must handle missing git repos gracefully (non-git directories)

specs/commands/commands.spec.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Shared command infrastructure used by all CLI subcommands. Provides config loadi
5959
| `report` | Per-module coverage report with staleness |
6060
| `resolve` | Resolve cross-project dependency refs |
6161
| `rules` | List active validation rules (built-in and custom) |
62+
| `stale` | Git-based staleness detection for spec drift |
6263
| `scaffold` | Full spec scaffolding with templates |
6364
| `score` | Spec quality scoring (0-100, A-F) |
6465
| `view` | Role-filtered spec rendering |
@@ -129,6 +130,7 @@ Shared command infrastructure used by all CLI subcommands. Provides config loadi
129130
| cmd_score | `load_and_discover`, `filter_specs` |
130131
| cmd_report | `load_and_discover` |
131132
| cmd_resolve | `load_and_discover` |
133+
| cmd_stale | `load_and_discover` |
132134
| cmd_diff | `load_and_discover` |
133135

134136
## Change Log

specs/exports/exports.spec.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ files:
1515
- src/exports/csharp.rs
1616
- src/exports/php.rs
1717
- src/exports/ruby.rs
18+
- src/exports/ast/mod.rs
19+
- src/exports/ast/typescript.rs
20+
- src/exports/ast/python.rs
21+
- src/exports/ast/rust_lang.rs
1822
db_tables: []
1923
tracks: [60]
2024
depends_on:
@@ -25,7 +29,7 @@ depends_on:
2529

2630
## Purpose
2731

28-
Language-aware export extraction from source files. Auto-detects the programming language from file extension and extracts public/exported symbol names using regex-based parsing (no AST required). Supports 11 languages: TypeScript/JS, Rust, Go, Python, Swift, Kotlin, Java, C#, Dart, PHP, and Ruby.
32+
Language-aware export extraction from source files. Auto-detects the programming language from file extension and extracts public/exported symbol names using regex-based parsing or tree-sitter AST analysis. Supports 11 languages: TypeScript/JS, Rust, Go, Python, Swift, Kotlin, Java, C#, Dart, PHP, and Ruby.
2933

3034
## Public API
3135

@@ -40,6 +44,23 @@ Language-aware export extraction from source files. Auto-detects the programming
4044
| `has_extension` | `file_path: &Path, extensions: &[String]` | `bool` | Check if file matches specific extensions, or any supported language if extensions is empty |
4145
| `extract_exports` | `content: &str` | `Vec<String>` | Per-language backend function that parses source text and returns exported symbol names (one per backend file) |
4246
| `extract_exports_with_resolver` | `content: &str, resolver: Option<&ImportResolver>` | `Vec<String>` | TypeScript-specific: extract exports with optional wildcard re-export resolution via file resolver callback |
47+
| `get_exported_symbols_full` | `file_path: &Path, level: ExportLevel, parse_mode: ParseMode` | `Vec<String>` | Extract exports with full control over granularity and parse mode (Regex or Ast) |
48+
49+
### Exported Modules
50+
51+
| Module | Source | Description |
52+
|--------|--------|-------------|
53+
| `ast` | `src/exports/mod.rs` | Tree-sitter based AST export extraction backends |
54+
55+
### Exported AST Sub-modules
56+
57+
Tree-sitter based export extraction backends for TypeScript, Python, and Rust. Used when `ParseMode::Ast` is selected. Falls back to regex extraction for unsupported languages or when AST parsing fails.
58+
59+
| Sub-module | File | Description |
60+
|------------|------|-------------|
61+
| `typescript` | `ast/typescript.rs` | Tree-sitter based TypeScript/JS export extraction with wildcard resolver support |
62+
| `python` | `ast/python.rs` | Tree-sitter based Python export extraction using `__all__` and top-level definitions |
63+
| `rust_lang` | `ast/rust_lang.rs` | Tree-sitter based Rust `pub` item extraction |
4364

4465
### Language Backend Functions
4566

specs/git_utils/git_utils.spec.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
module: git_utils
3+
version: 1
4+
status: stable
5+
files:
6+
- src/git_utils.rs
7+
db_tables: []
8+
tracks: []
9+
depends_on: []
10+
---
11+
12+
# Git Utils
13+
14+
## Purpose
15+
16+
Shared git utility functions for querying repository history. Provides commit hash lookup, commit distance counting, and git repository detection. Used by the `stale`, `report`, and `scoring` modules to determine spec freshness relative to source file changes.
17+
18+
## Public API
19+
20+
### Exported Functions
21+
22+
| Function | Parameters | Returns | Description |
23+
|----------|-----------|---------|-------------|
24+
| `git_last_commit_hash` | `root: &Path, file: &str` | `Option<String>` | Get the SHA hash of the last commit that touched a file |
25+
| `git_commits_between` | `root: &Path, spec_file: &str, source_file: &str` | `usize` | Count commits to source_file since spec_file was last modified |
26+
| `is_git_repo` | `root: &Path` | `bool` | Check if a directory is inside a git work tree |
27+
28+
### Exported Types
29+
30+
| Type | Kind | Description |
31+
|------|------|-------------|
32+
| `StaleInfo` | struct | Staleness summary for a single spec: path, module name, max commits behind, per-file details |
33+
34+
## Invariants
35+
36+
1. All git commands execute with `current_dir(root)` to ensure correct repository context
37+
2. Functions return safe defaults (None, 0, false) when git is unavailable or commands fail
38+
3. `git_commits_between` uses `git rev-list --count {spec_commit}..HEAD -- {source_file}` to count divergence
39+
4. `StaleInfo.source_details` only includes files with commits_behind > 0
40+
41+
## Behavioral Examples
42+
43+
### Scenario: File not tracked by git
44+
45+
- **Given** a file that has never been committed
46+
- **When** `git_last_commit_hash` is called
47+
- **Then** returns `None`
48+
49+
### Scenario: Source file changed after spec
50+
51+
- **Given** a spec last committed at commit A, and a source file with 3 commits after A
52+
- **When** `git_commits_between` is called
53+
- **Then** returns `3`
54+
55+
## Error Cases
56+
57+
| Condition | Behavior |
58+
|-----------|----------|
59+
| Not a git repository | `is_git_repo` returns false; other functions return safe defaults |
60+
| Git not installed | All functions return None/0/false |
61+
| File doesn't exist in git history | Returns None or 0 |
62+
63+
## Dependencies
64+
65+
None (only uses `std::process::Command` for git CLI calls).
66+
67+
## Change Log
68+
69+
| Date | Change |
70+
|------|--------|
71+
| 2026-04-10 | Initial — extracted from cmd_report for shared use by stale, report, and scoring |

specs/git_utils/requirements.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
spec: git_utils.spec.md
3+
---
4+
5+
## User Stories
6+
7+
- As a developer, I want git-aware spec tooling so that staleness and freshness are tracked automatically
8+
- As a module consumer, I want a clean API for git log queries without reimplementing git2 boilerplate
9+
10+
## Acceptance Criteria
11+
12+
- `commits_since` returns accurate commit counts for files since a given timestamp
13+
- `last_commit_for_file` returns the most recent commit touching a specific file
14+
- `changed_files_since` lists files modified since a reference point
15+
- All functions handle missing repos, untracked files, and shallow clones gracefully
16+
17+
## Constraints
18+
19+
- Must not panic on expected error conditions — return Results
20+
- Must use git2 (libgit2) for git operations, not shell commands
21+
- Must not hold repository locks longer than necessary

0 commit comments

Comments
 (0)