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
43 changes: 39 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,44 @@ jobs:
echo "$OUTPUT"
COVERAGE=$(echo "$OUTPUT" | grep -oP '[\d.]+% coverage' | grep -oP '[\d.]+')
echo "Coverage: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 40" | bc -l) )); then
echo "::error::Coverage ${COVERAGE}% is below 40% threshold"
if (( $(echo "$COVERAGE < 43" | bc -l) )); then
echo "::error::Coverage ${COVERAGE}% is below 43% threshold"
exit 1
fi

site:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
working-directory: site
run: bun install --frozen-lockfile
- name: Run site tests
working-directory: site
run: bun test
- name: Lint site
working-directory: site
run: bun run lint
- name: Build site
working-directory: site
run: bun run build

vscode-extension:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
working-directory: vscode-extension
run: bun install --frozen-lockfile
- name: Compile extension
working-directory: vscode-extension
run: bun run compile
- name: Package extension
working-directory: vscode-extension
run: bun run package

validate-action:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -94,12 +127,12 @@ jobs:
echo 'EOF'
} >> "$GITHUB_OUTPUT"
# Fail the step if check fails
cargo run -- check --strict
cargo run -- check --strict --require-coverage 100 --force

corvid-pet:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && always()
needs: [test, fmt, validate-action, spec-check, audit, coverage]
needs: [test, fmt, validate-action, spec-check, audit, coverage, site, vscode-extension]
steps:
- uses: actions/checkout@v4

Expand All @@ -112,6 +145,8 @@ jobs:
CHECK_SPEC: "Spec Validation=${{ needs.spec-check.result }}"
CHECK_AUDIT: "Dependency Audit=${{ needs.audit.result }}"
CHECK_COVERAGE: "Code Coverage=${{ needs.coverage.result }}"
CHECK_SITE: "Docs Site=${{ needs.site.result }}"
CHECK_VSCODE: "VS Code Extension=${{ needs.vscode-extension.result }}"
REPORT_SPEC: ${{ needs.spec-check.outputs.body }}
run: |
OVERALL="success"
Expand Down
2 changes: 1 addition & 1 deletion .specsync/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
specs_dir = "specs"
source_dirs = ["src"]
exclude_dirs = ["__tests__"]
exclude_patterns = ["**/__tests__/**", "**/*.test.ts", "**/*.spec.ts"]
exclude_patterns = ["**/__tests__/**", "**/*.test.ts", "**/*.spec.ts", "**/tests.rs"]
required_sections = ["Purpose", "Public API", "Invariants", "Behavioral Examples", "Error Cases", "Dependencies", "Change Log"]
enforcement = "strict"
ai_command = "claude -p --output-format text"
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Enforcement is **strict** — CI and pre-commit hooks will block on any spec vio
Specs follow a lifecycle from creation through archival:

1. **Requirements** — Create `requirements.md` in the spec directory. These are high-level acceptance criteria and user stories. They are permanent invariants, not tasks.
2. **Spec creation** — Run `specsync scaffold <name>` or `specsync new <name>` to create the spec, companion files, and detect source files. Fill in the spec before writing code.
2. **Spec creation** — Run `specsync scaffold <name>` or `specsync new <name>` to create the spec, companion files, and detect source files. Complete the spec before writing code.
3. **Active development** — The spec (`*.spec.md`) is the detailed contract. Keep it in sync with code changes. Use `tasks.md` for work items, `context.md` for architectural decisions.
4. **Working specs** — Specs with `status: draft` are in-progress. Promote to `status: stable` once the module's API is settled.
5. **Maintenance** — Run `specsync check --strict` to catch drift. Use `specsync compact` to keep changelogs manageable.
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [4.3.4] - 2026-06-07

### Security

- **GitHub Action command execution hardened** — marketplace action now builds `specsync` invocations as bash argv arrays instead of shell strings, eliminating `eval` around user-provided `args`.
- **Release checksum verification fails closed** — downloaded release archives now require matching `.sha256` files before extraction.

### Fixed

- **Action input validation** — `require-coverage` is validated as an integer from 0 to 100 before command execution.
- **MCP generated-spec test assertion** — replaced a tautological unsigned comparison with a meaningful generated-spec count assertion.
- **VS Code extension license packaging** — extension package includes the MIT license file so VSIX builds are complete.

### CI

- **Repo-wide validation expanded** — CI now builds/tests/lints the Astro docs site and compiles/packages the VS Code extension.
- **Spec gate requires full coverage** — project spec CI now runs `check --strict --require-coverage 100 --force`.
- **Coverage threshold raised** — tarpaulin minimum coverage increased from 40% to 43%.
- **Fledge tasks expanded** — repository lanes now cover Rust, specs, docs, extension packaging, and audit checks.
- **Known transitive audit warning tracked** — `RUSTSEC-2024-0384` is ignored explicitly while it remains pulled in through `notify`.

### Specs

- **Utility helpers specced** — added a dedicated spec for `src/util.rs`.
- **Companion files completed** — backfilled `testing.md` companions and missing `tasks.md`/`context.md` files for legacy specs.

## [v4.3.3] - 2026-05-18

### Fixed
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.3.3"
version = "4.3.4"
edition = "2024"
rust-version = "1.88"
description = "Bidirectional spec-to-code validation with schema column checking — 11 languages, single binary"
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ specsync [command] [flags]
| `changelog <range>` | Generate changelog of spec changes between two git refs |
| `comment` | Post spec-sync check summary as a PR comment (same validation pipeline as `check`). `--pr N` to post, omit to print |
| `import <source> <id>` | Import specs from GitHub Issues, Jira, or Confluence |
| `wizard` | Interactive step-by-step guided spec creation |
| `wizard` | Interactive guided spec creation |
| `resolve` | Verify `depends_on` references exist. `--remote` fetches registries from GitHub |
| `init-registry` | Generate `specsync-registry.toml` from existing specs |
| `score` | Quality-score spec files (0–100) with improvement suggestions |
Expand Down Expand Up @@ -430,10 +430,10 @@ spec: auth.spec.md
---

## User Stories
- As a [role], I want [feature] so that [benefit]
- As a maintainer, I want this module's contract captured clearly so changes can be reviewed against stable behavior.

## Acceptance Criteria
- [ ] <!-- TODO: define acceptance criteria -->
- Define acceptance criteria from the module's source behavior and user-facing responsibilities.

## Constraints
<!-- Non-functional requirements, performance targets, compliance needs -->
Expand All @@ -453,7 +453,7 @@ spec: auth.spec.md
- [ ] <!-- Implementation checklist -->

## Gaps
<!-- Uncovered areas, missing edge cases -->
Record concrete coverage gaps or edge cases that still need tests.

## Review Sign-offs
- **Product**: pending
Expand Down Expand Up @@ -610,7 +610,7 @@ Available on the [GitHub Marketplace](https://github.com/marketplace/actions/spe
| `strict` | `false` | Treat warnings as errors |
| `require-coverage` | `0` | Minimum file coverage % |
| `root` | `.` | Project root directory |
| `args` | `''` | Extra CLI arguments |
| `args` | `''` | Extra whitespace-separated CLI arguments; shell quoting is not supported |
| `comment` | `false` | Post spec drift results as a PR comment (requires `pull_request` event) |
| `token` | `${{ github.token }}` | GitHub token for posting PR comments (needs write permissions) |

Expand Down Expand Up @@ -819,7 +819,7 @@ specsync coverage # See what's still missing

### Template mode (default)

Uses your custom template (`specs/_template.spec.md`) or the built-in default. Generates frontmatter + stubbed sections with TODOs.
Uses your custom template (`specs/_template.spec.md`) or the built-in default. Generates frontmatter plus guided starter sections for review.

### AI mode (`--provider`)

Expand Down Expand Up @@ -881,9 +881,9 @@ specsync check --fix # Add undocumented exports as stub rows
specsync check --fix --json # Same, with structured JSON output
```

When `--fix` is used, any export found in code but missing from the spec gets appended as a stub row (`| \`name\` | | | *TODO* |`) to the Public API table. If no `## Public API` section exists, one is created. Already-documented exports are never duplicated.
When `--fix` is used, any export found in code but missing from the spec gets appended as a review row with the symbol name and a description prompt. If no `## Public API` section exists, one is created. Already-documented exports are never duplicated.

This turns spec maintenance from manual table editing into a review-and-refine workflow — run `--fix`, then fill in the descriptions.
This turns spec maintenance from manual table editing into a review-and-refine workflow — run `--fix`, then replace the generated prompts with final descriptions.

### `diff`: Track API changes across commits

Expand Down
71 changes: 40 additions & 31 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ inputs:
required: false
default: '.'
args:
description: 'Additional arguments to pass to specsync check'
description: 'Additional whitespace-separated arguments to pass to specsync check (shell quoting is not supported)'
required: false
default: ''
lifecycle-enforce:
Expand Down Expand Up @@ -87,34 +87,30 @@ runs:
ARCHIVE="specsync-${OS}-${ARCH}.exe.zip"
curl -fsSL "${BASE_URL}/${ARCHIVE}" -o "${INSTALL_DIR}/specsync.zip"

# Verify checksum if available
if curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" -o "${INSTALL_DIR}/specsync.sha256" 2>/dev/null; then
echo "Verifying checksum..."
cd "$INSTALL_DIR"
EXPECTED=$(awk '{print $1}' specsync.sha256)
ACTUAL=$(shasum -a 256 specsync.zip | awk '{print $1}')
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "::error::Checksum verification failed! Expected: $EXPECTED, Got: $ACTUAL"
exit 1
fi
echo "::notice::Checksum verified"
cd -
curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" -o "${INSTALL_DIR}/specsync.sha256"
echo "Verifying checksum..."
cd "$INSTALL_DIR"
EXPECTED=$(awk '{print $1}' specsync.sha256)
ACTUAL=$(shasum -a 256 specsync.zip | awk '{print $1}')
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "::error::Checksum verification failed! Expected: $EXPECTED, Got: $ACTUAL"
exit 1
fi
echo "::notice::Checksum verified"
cd -

unzip -o "${INSTALL_DIR}/specsync.zip" -d "$INSTALL_DIR"
mv "${INSTALL_DIR}/specsync-${OS}-${ARCH}.exe" "${INSTALL_DIR}/specsync.exe"
else
ARCHIVE="specsync-${OS}-${ARCH}.tar.gz"
curl -fsSL "${BASE_URL}/${ARCHIVE}" -o "${INSTALL_DIR}/${ARCHIVE}"

# Verify checksum if available
if curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" -o "${INSTALL_DIR}/${ARCHIVE}.sha256" 2>/dev/null; then
echo "Verifying checksum..."
cd "$INSTALL_DIR"
shasum -a 256 -c "${ARCHIVE}.sha256"
echo "::notice::Checksum verified"
cd -
fi
curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" -o "${INSTALL_DIR}/${ARCHIVE}.sha256"
echo "Verifying checksum..."
cd "$INSTALL_DIR"
shasum -a 256 -c "${ARCHIVE}.sha256"
echo "::notice::Checksum verified"
cd -

tar xz -C "$INSTALL_DIR" -f "${INSTALL_DIR}/${ARCHIVE}"
mv "${INSTALL_DIR}/specsync-${OS}-${ARCH}" "${INSTALL_DIR}/specsync"
Expand All @@ -138,26 +134,39 @@ runs:
run: |
set -euo pipefail

if ! [[ "$INPUT_REQUIRE_COVERAGE" =~ ^[0-9]+$ ]] || [ "$INPUT_REQUIRE_COVERAGE" -gt 100 ]; then
echo "::error::require-coverage must be an integer from 0 to 100"
exit 1
fi

# Always use --force in CI — hash cache is not committed, so
# every CI run validates all specs from scratch.
CMD="specsync check --force"
CMD=(specsync check --force)

if [ "$INPUT_STRICT" = "true" ]; then
CMD="$CMD --strict"
CMD+=(--strict)
fi

if [ "$INPUT_REQUIRE_COVERAGE" != "0" ]; then
CMD="$CMD --require-coverage $INPUT_REQUIRE_COVERAGE"
CMD+=(--require-coverage "$INPUT_REQUIRE_COVERAGE")
fi

if [ -n "$INPUT_ARGS" ]; then
CMD="$CMD $INPUT_ARGS"
# INPUT_ARGS is intentionally split on whitespace only across all lines.
# Shell quoting, substitutions, pipes, and other shell syntax are not evaluated.
NORMALIZED_ARGS="${INPUT_ARGS//$'\r'/ }"
NORMALIZED_ARGS="${NORMALIZED_ARGS//$'\n'/ }"
NORMALIZED_ARGS="${NORMALIZED_ARGS//$'\t'/ }"
read -r -a EXTRA_ARGS <<< "$NORMALIZED_ARGS"
CMD+=("${EXTRA_ARGS[@]}")
fi

echo "::group::SpecSync Check"
echo "Running: $CMD"
printf 'Running:'
printf ' %q' "${CMD[@]}"
printf '\n'
EXIT_CODE=0
eval "$CMD" || EXIT_CODE=$?
"${CMD[@]}" || EXIT_CODE=$?
echo "::endgroup::"

# If lifecycle enforcement is enabled, run it (may override exit code)
Expand All @@ -171,14 +180,14 @@ runs:
# Uses the same `specsync comment` pipeline as our own CI workflow
# for identical output between the marketplace action and direct usage.
if [ "$INPUT_COMMENT" = "true" ]; then
COMMENT_CMD="specsync comment"
COMMENT_CMD=(specsync comment)
if [ "$INPUT_STRICT" = "true" ]; then
COMMENT_CMD="$COMMENT_CMD --strict"
COMMENT_CMD+=(--strict)
fi
if [ "$INPUT_REQUIRE_COVERAGE" != "0" ]; then
COMMENT_CMD="$COMMENT_CMD --require-coverage $INPUT_REQUIRE_COVERAGE"
COMMENT_CMD+=(--require-coverage "$INPUT_REQUIRE_COVERAGE")
fi
COMMENT_OUTPUT=$(eval "$COMMENT_CMD" 2>/dev/null) || true
COMMENT_OUTPUT=$("${COMMENT_CMD[@]}" 2>/dev/null) || true
{
echo "SPECSYNC_MARKDOWN<<SPECSYNC_EOF"
echo "$COMMENT_OUTPUT"
Expand Down
41 changes: 39 additions & 2 deletions fledge.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,56 @@ cmd = "cargo clippy -- -D warnings"
cmd = "cargo fmt --check"

[tasks.audit]
cmd = "cargo audit"
cmd = "cargo audit --ignore RUSTSEC-2024-0384"

[tasks.check-types]
cmd = "cargo check"

[tasks.spec-check]
cmd = "cargo run -- check --strict --require-coverage 100 --force"

[tasks.docs-test]
cmd = "cd site && bun test"

[tasks.docs-lint]
cmd = "cd site && bun run lint"

[tasks.docs-build]
cmd = "cd site && bun run build"

[tasks.vscode-compile]
cmd = "cd vscode-extension && bun run compile"

[tasks.vscode-package]
cmd = "cd vscode-extension && bun run package"

[lanes.check]
description = "Quick pre-commit check"
steps = [{ parallel = ["fmt", "lint"] }, "test"]

[lanes.ci]
description = "Full CI pipeline"
steps = ["fmt", "lint", "test", "build"]
steps = [
"fmt",
"lint",
"test",
"build",
"audit",
"spec-check",
{ parallel = ["docs-test", "docs-lint", "docs-build", "vscode-compile", "vscode-package"] },
]

[lanes.pre-commit]
description = "Fast gate before committing"
steps = [{ parallel = ["fmt", "lint"] }, "check-types"]

[lanes.repo]
description = "Full repository validation, including docs and editor extension"
steps = [
{ parallel = ["fmt", "lint", "check-types"] },
"test",
"build",
"audit",
"spec-check",
{ parallel = ["docs-test", "docs-lint", "docs-build", "vscode-compile", "vscode-package"] },
]
Loading
Loading