diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..5565b78 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,199 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 +# Copyright (c) 2025 conflow contributors +# +# GitLab CI/CD Configuration for conflow +# RSR-Compliant Pipeline + +stages: + - check + - test + - build + - compliance + - release + +variables: + CARGO_HOME: ${CI_PROJECT_DIR}/.cargo + RUSTFLAGS: "-D warnings" + +# Cache cargo dependencies +.cargo-cache: &cargo-cache + cache: + key: ${CI_JOB_NAME} + paths: + - .cargo/ + - target/ + +# ----------------------------------------------------------------------------- +# Check Stage +# ----------------------------------------------------------------------------- + +format: + stage: check + image: rust:latest + <<: *cargo-cache + script: + - rustup component add rustfmt + - cargo fmt -- --check + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' + +lint: + stage: check + image: rust:latest + <<: *cargo-cache + script: + - rustup component add clippy + - cargo clippy --all-targets --all-features -- -D warnings + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' + +audit: + stage: check + image: rust:latest + <<: *cargo-cache + script: + - cargo install cargo-audit + - cargo audit + allow_failure: true + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' + +# ----------------------------------------------------------------------------- +# Test Stage +# ----------------------------------------------------------------------------- + +test: + stage: test + image: rust:latest + <<: *cargo-cache + script: + - cargo test --all-features + coverage: '/^\d+.\d+% coverage/' + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' + +# ----------------------------------------------------------------------------- +# Build Stage +# ----------------------------------------------------------------------------- + +build:debug: + stage: build + image: rust:latest + <<: *cargo-cache + script: + - cargo build --all-features + artifacts: + paths: + - target/debug/conflow + expire_in: 1 day + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + +build:release: + stage: build + image: rust:latest + <<: *cargo-cache + script: + - cargo build --release --all-features + artifacts: + paths: + - target/release/conflow + expire_in: 1 week + rules: + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' + - if: '$CI_COMMIT_TAG' + +# ----------------------------------------------------------------------------- +# Compliance Stage +# ----------------------------------------------------------------------------- + +rsr-compliance: + stage: compliance + image: rust:latest + <<: *cargo-cache + script: + - cargo build --release + - ./target/release/conflow rsr check --format json > rsr-report.json || true + - cat rsr-report.json + artifacts: + paths: + - rsr-report.json + reports: + codequality: rsr-report.json + allow_failure: true + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' + +spdx-check: + stage: compliance + image: alpine:latest + script: + - | + echo "Checking SPDX headers..." + missing=0 + for file in $(find src -name "*.rs"); do + if ! head -1 "$file" | grep -q "SPDX-License-Identifier"; then + echo "Missing SPDX header: $file" + missing=$((missing + 1)) + fi + done + if [ $missing -gt 0 ]; then + echo "ERROR: $missing files missing SPDX headers" + exit 1 + fi + echo "All source files have SPDX headers" + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' + +# ----------------------------------------------------------------------------- +# Release Stage +# ----------------------------------------------------------------------------- + +publish:crates: + stage: release + image: rust:latest + <<: *cargo-cache + script: + - cargo publish --dry-run + rules: + - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/' + when: manual + +release: + stage: release + image: registry.gitlab.com/gitlab-org/release-cli:latest + script: + - echo "Creating release for $CI_COMMIT_TAG" + release: + tag_name: $CI_COMMIT_TAG + description: "Release $CI_COMMIT_TAG" + assets: + links: + - name: "Linux Binary" + url: "${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/target/release/conflow?job=build:release" + rules: + - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/' + +# ----------------------------------------------------------------------------- +# Documentation +# ----------------------------------------------------------------------------- + +pages: + stage: release + image: rust:latest + <<: *cargo-cache + script: + - cargo doc --no-deps --all-features + - mv target/doc public + - echo '' > public/index.html + artifacts: + paths: + - public + rules: + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' diff --git a/.well-known/dnt-policy.txt b/.well-known/dnt-policy.txt new file mode 100644 index 0000000..d7b7411 --- /dev/null +++ b/.well-known/dnt-policy.txt @@ -0,0 +1,13 @@ +# Do Not Track Policy +# See: https://www.eff.org/dnt-policy + +This is a command-line application that: +- Does NOT collect any user data +- Does NOT send telemetry +- Does NOT track usage +- Does NOT connect to external services (offline-first design) + +All operations are performed locally on your machine. + +Status: No tracking whatsoever +Effective: 2025-01-01 diff --git a/.well-known/humans.txt b/.well-known/humans.txt new file mode 100644 index 0000000..852ec28 --- /dev/null +++ b/.well-known/humans.txt @@ -0,0 +1,23 @@ +/* TEAM */ +Lead Developer: Jonathan D.A. Jewell +Contact: hyperpolymath [at] proton.me +GitLab: @hyperpolymath +Location: Global + +/* CONTRIBUTORS */ +See MAINTAINERS.md and git log for full contributor list. + +/* THANKS */ +The Rust Community +CUE Lang Team +Nickel Lang Team +Rhodium Standard Repository Framework +Campaign for Cooler Coding and Programming (CCCP) + +/* SITE */ +Last update: 2025-01-01 +Language: English +Standards: RSR Silver Compliance +Doctype: Rust CLI Application +Components: Rust, CUE, Nickel, Nix +IDE: Various (VS Code, Vim, Emacs, Helix) diff --git a/.well-known/security.txt b/.well-known/security.txt new file mode 100644 index 0000000..34fcb26 --- /dev/null +++ b/.well-known/security.txt @@ -0,0 +1,13 @@ +# Security Policy for conflow +# See: https://securitytxt.org/ + +Contact: mailto:security@conflow.dev +Expires: 2026-01-01T00:00:00.000Z +Encryption: https://gitlab.com/hyperpolymath/conflow/-/blob/main/.well-known/pgp-key.txt +Preferred-Languages: en +Canonical: https://gitlab.com/hyperpolymath/conflow/-/raw/main/.well-known/security.txt +Policy: https://gitlab.com/hyperpolymath/conflow/-/blob/main/SECURITY.md + +# Acknowledgments +# We thank all security researchers who responsibly disclose vulnerabilities. +# Hall of Fame: https://gitlab.com/hyperpolymath/conflow/-/blob/main/SECURITY.md#acknowledgments diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..13b76c6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,224 @@ +# CLAUDE.md - AI Assistant Guidance for conflow + +This document provides guidance for AI assistants working with the conflow codebase. + +## Project Overview + +**conflow** is a Configuration Flow Orchestrator that intelligently orchestrates +CUE, Nickel, and configuration validation workflows. + +### Key Concepts + +- **Pipeline**: A sequence of stages defined in `.conflow.yaml` +- **Stage**: A single step that runs a tool (CUE, Nickel, or shell) +- **Executor**: Implements tool-specific execution logic +- **Cache**: Content-addressed caching to avoid redundant work +- **RSR Integration**: Rhodium Standard Repository compliance checking + +## Architecture + +``` +src/ +├── main.rs # CLI entry point +├── lib.rs # Library exports +├── cli/ # Command handlers +│ ├── mod.rs # CLI definitions (clap) +│ ├── init.rs # `conflow init` +│ ├── analyze.rs # `conflow analyze` +│ ├── run.rs # `conflow run` +│ ├── validate.rs # `conflow validate` +│ ├── watch.rs # `conflow watch` +│ ├── graph.rs # `conflow graph` +│ ├── cache.rs # `conflow cache` +│ └── rsr.rs # `conflow rsr` +├── pipeline/ # Pipeline orchestration +│ ├── definition.rs # Pipeline, Stage, Tool types +│ ├── dag.rs # Dependency graph +│ ├── executor.rs # Pipeline execution +│ └── validation.rs # Pipeline validation +├── executors/ # Tool executors +│ ├── cue.rs # CUE executor +│ ├── nickel.rs # Nickel executor +│ └── shell.rs # Shell executor +├── cache/ # Caching system +│ ├── filesystem.rs # File-based cache +│ └── hash.rs # Content hashing (BLAKE3) +├── analyzer/ # Config analysis +│ ├── complexity.rs # Complexity metrics +│ ├── config_detector.rs # Format detection +│ └── recommender.rs # Tool recommendations +├── rsr/ # RSR integration +│ ├── compliance.rs # Compliance checking +│ ├── requirements.rs # RSR requirements +│ ├── schemas.rs # Schema registry +│ ├── hooks.rs # External integration +│ ├── remediation.rs # Auto-fix +│ ├── badges.rs # Badge generation +│ ├── diff.rs # Compliance diffs +│ ├── config.rs # .rsr.yaml loading +│ └── templates.rs # Template generation +├── errors/ # Error handling +│ ├── mod.rs # Error types (miette) +│ └── educational.rs # Helpful error messages +└── utils/ # Utilities + ├── colors.rs # Terminal colors + └── spinner.rs # Progress indicators +``` + +## Key Files + +### `.conflow.yaml` Format + +```yaml +version: "1" +name: pipeline-name + +stages: + - name: stage-name + tool: + type: cue | nickel | shell + command: vet | export | eval | + # Tool-specific options... + input: | from_stage: + output: + depends_on: [] + description: Optional description + +cache: + enabled: true + directory: .conflow-cache +``` + +### Important Types + +```rust +// Pipeline definition (src/pipeline/definition.rs) +pub struct Pipeline { + pub version: String, + pub name: String, + pub stages: Vec, + pub cache: Option, +} + +// Stage definition +pub struct Stage { + pub name: String, + pub tool: Tool, + pub input: Input, + pub output: Option, + pub depends_on: Vec, + pub description: Option, +} + +// Tool variants +pub enum Tool { + Cue { command: CueCommand, schemas: Vec, ... }, + Nickel { command: NickelCommand, format: OutputFormat, ... }, + Shell { command: String, shell: Option }, +} +``` + +## Development Guidelines + +### Building + +```bash +cargo build # Debug build +cargo build --release # Release build +cargo test # Run tests +cargo clippy # Lint +cargo fmt # Format +``` + +### Testing + +- Unit tests: `cargo test` +- Integration tests: `cargo test --test '*'` +- Specific test: `cargo test test_name` + +### Adding a New Executor + +1. Create `src/executors/new_tool.rs` +2. Implement `Executor` trait +3. Add to `src/executors/mod.rs` +4. Add `Tool::NewTool` variant in `src/pipeline/definition.rs` +5. Handle in executor dispatch + +### Adding a New CLI Command + +1. Add variant to `Commands` enum in `src/cli/mod.rs` +2. Create `src/cli/command.rs` with `run()` function +3. Add dispatch in `src/main.rs` + +## Code Style + +- Use `cargo fmt` for formatting +- Add SPDX headers to all source files +- Document public APIs +- Handle errors explicitly (no `.unwrap()` in library code) +- Prefer `miette` for user-facing errors + +## Common Tasks + +### Running a pipeline +```rust +use conflow::pipeline::{Pipeline, PipelineExecutor, ExecutionOptions}; + +let pipeline = Pipeline::from_file(".conflow.yaml")?; +let executor = PipelineExecutor::new(pipeline); +let results = executor.run(ExecutionOptions::default()).await?; +``` + +### Checking RSR compliance +```rust +use conflow::rsr::ComplianceChecker; + +let checker = ComplianceChecker::new(); +let report = checker.check(project_root)?; +println!("Level: {:?}, Score: {:.0}%", report.level, report.score * 100.0); +``` + +### Generating from template +```rust +use conflow::rsr::TemplateGenerator; + +let generator = TemplateGenerator::new(); +let result = generator.generate("kubernetes", target_dir, &variables)?; +``` + +## RSR Compliance + +This project aims for RSR Silver compliance: + +- [x] Nix flake for reproducible builds +- [x] Justfile for task running +- [x] Dual MIT/Apache-2.0 license +- [x] Comprehensive documentation +- [x] TPCF contribution framework +- [x] Security policy +- [x] Code of Conduct + +## Troubleshooting + +### Common Issues + +1. **CUE/Nickel not found**: Ensure they're in PATH or use Nix +2. **Cache issues**: Run `conflow cache clear` +3. **Pipeline validation errors**: Check `conflow validate` + +### Debug Logging + +```bash +RUST_LOG=conflow=debug conflow run +``` + +## Links + +- Repository: https://gitlab.com/hyperpolymath/conflow +- RSR Standards: https://gitlab.com/hyperpolymath/rhodium-standard-repositories +- CUE: https://cuelang.org +- Nickel: https://nickel-lang.org + +--- + +*This file follows RSR standards for AI assistant guidance.* diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2a0a791 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,144 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Emotional Safety + +In alignment with RSR principles, we specifically commit to: + +* **Reversibility**: Mistakes can be undone; no permanent shame +* **Safe Experimentation**: Trying new approaches is encouraged +* **No Blame Culture**: Focus on problems, not people +* **Constructive Critique**: Feedback targets code, not character + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at: + +* Email: conduct@conflow.dev +* GitLab: Confidential issue with ~conduct label + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..eb2f823 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,209 @@ +# Contributing to conflow + +Thank you for your interest in contributing to conflow! This document provides +guidelines and information for contributors. + +## Tri-Perimeter Contribution Framework (TPCF) + +conflow uses a graduated trust model based on the RSR Tri-Perimeter Contribution +Framework: + +### Perimeter 1: Core (Maintainers Only) + +Changes to critical infrastructure require maintainer review and approval: + +- Build system (`Cargo.toml`, `flake.nix`, `justfile`) +- CI/CD configuration (`.gitlab-ci.yml`) +- Security-sensitive code (`src/executors/shell.rs`) +- Release processes + +### Perimeter 2: Expert (Trusted Contributors) + +Experienced contributors may work on: + +- New executor implementations +- Pipeline validation logic +- Cache algorithms +- RSR integration features +- Performance optimizations + +**Requirements**: Previous accepted contributions, demonstrated expertise + +### Perimeter 3: Community (Open to All) + +Everyone is welcome to contribute: + +- Documentation improvements +- Bug reports and fixes +- Test coverage +- Example pipelines +- Translations +- Issue triage + +## Getting Started + +### Prerequisites + +- Rust 1.75+ (install via rustup) +- Nix (optional, for reproducible builds) +- CUE and Nickel (for integration tests) + +### Development Setup + +```bash +# Clone the repository +git clone https://gitlab.com/hyperpolymath/conflow.git +cd conflow + +# Option 1: Use Nix (recommended) +nix develop + +# Option 2: Manual setup +cargo build +cargo test +``` + +### Running Tests + +```bash +# All tests +cargo test + +# Specific test +cargo test test_name + +# With output +cargo test -- --nocapture +``` + +## Contribution Process + +### 1. Find or Create an Issue + +- Check existing issues first +- Create a new issue for bugs or features +- Wait for maintainer feedback on large changes + +### 2. Fork and Branch + +```bash +git checkout -b feature/my-feature +# or +git checkout -b fix/issue-123 +``` + +### 3. Make Changes + +- Follow the code style (run `cargo fmt`) +- Add tests for new functionality +- Update documentation as needed +- Add SPDX headers to new files: + +```rust +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors +``` + +### 4. Commit + +Follow conventional commits: + +``` +feat: add new cue validation option +fix: handle empty pipeline gracefully +docs: update CLI usage examples +test: add cache invalidation tests +refactor: simplify stage execution +``` + +### 5. Submit Merge Request + +- Fill out the MR template +- Link related issues +- Ensure CI passes +- Request review + +## Code Style + +### Rust Guidelines + +- Use `cargo fmt` for formatting +- Use `cargo clippy` for linting +- Prefer explicit error handling over `.unwrap()` +- Document public APIs with doc comments +- Keep functions focused and small + +### Documentation + +- Use clear, concise language +- Include code examples where helpful +- Update README for user-facing changes +- Add inline comments for complex logic + +## Testing Requirements + +### Unit Tests + +- All new functions should have tests +- Test edge cases and error conditions +- Use descriptive test names + +### Integration Tests + +- Test CLI commands in `tests/` +- Test with real CUE/Nickel files +- Verify cache behavior + +### Test Coverage + +We aim for >80% coverage on core modules. + +## Review Process + +### What Reviewers Look For + +1. **Correctness**: Does the code work as intended? +2. **Tests**: Are there adequate tests? +3. **Documentation**: Is it documented? +4. **Style**: Does it follow conventions? +5. **Security**: Any security implications? + +### Review Timeline + +- Initial response: 2-3 business days +- Full review: 1 week for small changes +- Large changes may take longer + +## Community + +### Communication Channels + +- GitLab Issues: Bug reports and feature requests +- Merge Requests: Code discussions +- Email: maintainers@conflow.dev + +### Meetings + +- No regular meetings currently +- Ad-hoc discussions as needed + +## Recognition + +Contributors are recognized in: + +- `MAINTAINERS.md` for significant contributions +- Release notes for merged changes +- `humans.txt` for all contributors + +## License + +By contributing, you agree that your contributions will be licensed under the +same MIT OR Apache-2.0 dual license as the project. + +## Questions? + +Don't hesitate to ask! Open an issue or reach out to maintainers. + +--- + +*This contributing guide follows RSR standards and TPCF principles.* diff --git a/FUNDING.yml b/FUNDING.yml new file mode 100644 index 0000000..6e692da --- /dev/null +++ b/FUNDING.yml @@ -0,0 +1,31 @@ +# Funding information for conflow +# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository + +# Primary funding platforms +github: hyperpolymath +open_collective: conflow + +# Alternative platforms +# ko_fi: hyperpolymath +# patreon: hyperpolymath +# liberapay: hyperpolymath + +# Custom funding links +custom: + - https://gitlab.com/hyperpolymath + +# Funding goals and transparency +# +# Current Goals: +# - Infrastructure costs (CI/CD, hosting): $50/month +# - Development time: Variable +# - Security audits: As needed +# +# How funds are used: +# - 100% goes to project development and maintenance +# - All expenses are documented publicly +# - No profit distribution (community project) +# +# Transparency: +# - Financial reports published quarterly (when applicable) +# - All sponsors acknowledged (with permission) diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..3f1f5cf --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,120 @@ +# Governance + +This document describes the governance model for conflow. + +## Project Structure + +### Roles + +#### Maintainers + +Maintainers have full access to the repository and are responsible for: + +- Reviewing and merging contributions +- Release management +- Security response +- Strategic direction +- Community health + +Current maintainers are listed in [MAINTAINERS.md](MAINTAINERS.md). + +#### Contributors + +Anyone who has had a contribution merged. Contributors are recognized in +release notes and `humans.txt`. + +#### Community Members + +Anyone who participates in issues, discussions, or uses the project. + +## Decision Making + +### Consensus-Based + +We aim for consensus on all significant decisions: + +1. **Proposal**: Open an issue describing the change +2. **Discussion**: Community feedback period (minimum 1 week for major changes) +3. **Decision**: Maintainers evaluate feedback and decide +4. **Documentation**: Decision is documented in the issue + +### Lazy Consensus + +For minor changes (typos, small fixes), maintainers may merge without +extended discussion. If anyone objects, the change can be reverted and +discussed. + +### Voting + +If consensus cannot be reached: + +- Each maintainer gets one vote +- Simple majority wins +- Ties are broken by the lead maintainer +- Voting period: 1 week minimum + +## Becoming a Maintainer + +### Path to Maintainership + +1. Sustained, high-quality contributions over 6+ months +2. Demonstrated understanding of project goals +3. Positive community interactions +4. Nomination by existing maintainer +5. Approval by majority of maintainers + +### Maintainer Responsibilities + +- Review contributions in a timely manner +- Participate in security response +- Uphold Code of Conduct +- Mentor new contributors +- Participate in governance decisions + +### Stepping Down + +Maintainers may step down at any time by notifying other maintainers. +Inactive maintainers (6+ months no activity) may be moved to emeritus status. + +## Changes to Governance + +This governance model can be amended by: + +1. Opening an issue with proposed changes +2. Minimum 2-week discussion period +3. Approval by 2/3 of maintainers + +## Conflict Resolution + +### Technical Disputes + +1. Discuss in issue/MR +2. Seek input from additional maintainers +3. If unresolved, maintainer vote + +### Code of Conduct Violations + +See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for enforcement procedures. + +### Governance Disputes + +Escalate to full maintainer group for resolution. + +## RSR Alignment + +This governance model aligns with RSR principles: + +- **Emotional Safety**: No-blame culture, safe to make mistakes +- **Community Over Ego**: Consensus-based decisions +- **Transparency**: All decisions documented publicly +- **Accountability**: Clear roles and responsibilities + +## Contact + +- Governance questions: governance@conflow.dev +- General inquiries: maintainers@conflow.dev + +--- + +*Effective Date: 2025-01-01* +*Last Updated: 2025-01-01* diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..8b4b049 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,185 @@ +Dual License: MIT OR Apache-2.0 + +This project is dual-licensed under either: + +- MIT License (see below) +- Apache License, Version 2.0 (see below) + +at your option. + +================================================================================ +MIT License +================================================================================ + +Copyright (c) 2025 Jonathan D.A. Jewell and conflow contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +================================================================================ +Apache License, Version 2.0 +================================================================================ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work. + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright + owner or by an individual or Legal Entity authorized to submit on + behalf of the copyright owner. + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. + + You may add Your own attribution notices within Derivative Works + that You distribute, alongside or as an addendum to the NOTICE text + from the Work, provided that such additional attribution notices + cannot be construed as modifying the License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..4bd373d --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,57 @@ +# Maintainers + +This file lists the maintainers of conflow. + +## Current Maintainers + +### Lead Maintainer + +- **Jonathan D.A. Jewell** (@hyperpolymath) + - GitLab: https://gitlab.com/hyperpolymath + - Role: Project founder, lead maintainer + - Areas: All areas + +## Emeritus Maintainers + +*None yet* + +## Becoming a Maintainer + +See [GOVERNANCE.md](GOVERNANCE.md) for the path to maintainership. + +## Responsibilities + +Maintainers are expected to: + +1. **Review Contributions** + - Respond to MRs within 1 week + - Provide constructive feedback + - Merge approved changes + +2. **Triage Issues** + - Label and prioritize issues + - Close stale/duplicate issues + - Guide contributors + +3. **Security Response** + - Monitor security reports + - Coordinate vulnerability fixes + - Manage disclosure timeline + +4. **Community Health** + - Enforce Code of Conduct + - Welcome new contributors + - Foster inclusive environment + +5. **Release Management** + - Prepare release notes + - Tag releases + - Update documentation + +## Contact + +For maintainer-specific inquiries: maintainers@conflow.dev + +--- + +*This file is updated when maintainers are added or removed.* diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..aca5821 --- /dev/null +++ b/README.adoc @@ -0,0 +1,246 @@ += conflow - Configuration Flow Orchestrator +:toc: left +:toclevels: 3 +:icons: font +:source-highlighter: rouge + +Intelligently orchestrate CUE, Nickel, and configuration validation workflows. + +image:https://img.shields.io/badge/RSR-Silver-silver[RSR Compliance, link=https://gitlab.com/hyperpolymath/rhodium-standard-repositories] +image:https://img.shields.io/badge/License-MIT%20OR%20Apache--2.0-blue[License] +image:https://img.shields.io/badge/Rust-1.75+-orange[Rust Version] + +== Why conflow? + +*Problem:* You have configuration files and you're not sure whether to use CUE, Nickel, or both. + +*Solution:* conflow analyzes your configs, recommends the right tool, and orchestrates the entire pipeline. + +[source,bash] +---- +# Instead of: +nickel export config.ncl > temp.json +cue vet schema.cue temp.json +cue export schema.cue --out yaml > deploy.yaml +rm temp.json + +# Just: +conflow run +---- + +== Features + +* *Intelligent analysis* - Recommends CUE vs Nickel based on complexity +* *Pipeline orchestration* - Chain tools with dependency management +* *Smart caching* - Only re-run what changed +* *Educational* - Learn why certain tools fit certain problems +* *Type-safe* - Catch errors before deployment +* *RSR Integration* - Full Rhodium Standard Repository compliance checking + +== Quick Start + +[source,bash] +---- +# Install +cargo install conflow + +# Initialize +conflow init my-project + +# Analyze existing configs +conflow analyze config.yaml + +# Run pipeline +conflow run +---- + +== Example Pipeline + +[source,yaml] +---- +# .conflow.yaml +version: "1" +name: "k8s-deployment" + +stages: + - name: "generate" + tool: + type: nickel + command: export + file: config.ncl + output: generated/config.json + + - name: "validate" + tool: + type: cue + command: vet + schemas: [schemas/k8s.cue] + input: + from_stage: generate + depends_on: [generate] + + - name: "export" + tool: + type: cue + command: export + out_format: yaml + input: + from_stage: generate + depends_on: [validate] + output: deploy/k8s.yaml +---- + +[source,bash] +---- +$ conflow run +✓ generate (0.08s) +✓ validate (0.05s) +✓ export (0.03s) + +Pipeline completed in 0.16s +---- + +== When to Use What? + +=== Use CUE when: + +* ✅ Validating configuration +* ✅ Expressing constraints +* ✅ Merging configurations +* ✅ Simple transformations + +=== Use Nickel when: + +* ✅ Generating configurations +* ✅ Complex logic needed +* ✅ Functions and abstraction +* ✅ DRY configuration + +=== Use Both when: + +* ✅ Nickel generates → CUE validates +* ✅ Complex generation + strict validation + +== Commands + +[cols="1,2"] +|=== +|Command |Description + +|`conflow init [--template ]` +|Initialize project + +|`conflow analyze ` +|Analyze config files + +|`conflow run [--stage ]` +|Execute pipeline + +|`conflow watch` +|Watch mode + +|`conflow validate` +|Validate pipeline + +|`conflow graph [--format ]` +|Show pipeline graph + +|`conflow cache stats` +|Cache statistics + +|`conflow cache clear` +|Clear cache + +|`conflow rsr check` +|Check RSR compliance + +|`conflow rsr requirements` +|List RSR requirements +|=== + +== Templates + +[source,bash] +---- +conflow init --template cue-validation # Simple CUE validation +conflow init --template nickel-generation # Nickel config generation +conflow init --template full-pipeline # Generate → validate → export +conflow init --template kubernetes # Kubernetes manifests +conflow init --template multi-env # Multi-environment configs +---- + +== RSR Compliance + +conflow includes full RSR (Rhodium Standard Repository) integration: + +* *Compliance checking* - Validate against RSR requirements +* *Auto-remediation* - Automatically fix common issues +* *Badge generation* - Generate compliance badges for CI +* *Diff reports* - Track compliance changes over time + +[source,bash] +---- +# Check compliance +conflow rsr check + +# Auto-fix issues +conflow rsr check --fix + +# Generate badge +conflow rsr check --badge badge.svg +---- + +== Development + +[source,bash] +---- +# Using Nix (recommended) +nix develop + +# Using just +just build # Build +just test # Run tests +just check # Run all checks +just install # Install locally +---- + +== Documentation + +* link:CLAUDE.md[CLAUDE.md] - AI assistant guidance +* link:CONTRIBUTING.md[CONTRIBUTING.md] - Contribution guidelines +* link:SECURITY.md[SECURITY.md] - Security policy +* link:GOVERNANCE.md[GOVERNANCE.md] - Project governance +* link:CODE_OF_CONDUCT.md[CODE_OF_CONDUCT.md] - Code of conduct + +== RSR Standards + +This project follows link:https://gitlab.com/hyperpolymath/rhodium-standard-repositories[Rhodium Standard Repository] guidelines: + +* ✅ Memory-safe language (Rust) +* ✅ Offline-first design +* ✅ Reproducible builds (Nix) +* ✅ Comprehensive documentation +* ✅ SPDX license headers +* ✅ Security policy +* ✅ TPCF contribution framework + +== License + +This project is dual-licensed under: + +* MIT License +* Apache License, Version 2.0 + +See link:LICENSE.txt[LICENSE.txt] for details. + +== Contributing + +Contributions are welcome! Please read our link:CONTRIBUTING.md[Contributing Guide] first. + +== Links + +* *Repository:* https://gitlab.com/hyperpolymath/conflow +* *Issues:* https://gitlab.com/hyperpolymath/conflow/-/issues +* *CUE:* https://cuelang.org +* *Nickel:* https://nickel-lang.org +* *RSR:* https://gitlab.com/hyperpolymath/rhodium-standard-repositories diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4f99460 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,92 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 0.1.x | :white_check_mark: | + +## Reporting a Vulnerability + +We take security vulnerabilities seriously. If you discover a security issue, +please report it responsibly. + +### How to Report + +1. **Do NOT** open a public issue for security vulnerabilities +2. Email security concerns to: `security@conflow.dev` (or create a confidential issue) +3. Include as much detail as possible: + - Description of the vulnerability + - Steps to reproduce + - Potential impact + - Suggested fix (if any) + +### What to Expect + +- **Acknowledgment**: Within 48 hours of your report +- **Initial Assessment**: Within 7 days +- **Resolution Timeline**: Depends on severity + - Critical: 24-48 hours + - High: 7 days + - Medium: 30 days + - Low: 90 days + +### Disclosure Policy + +- We follow coordinated disclosure +- We will credit reporters (unless anonymity is requested) +- We aim to fix vulnerabilities before public disclosure + +## Security Measures + +### Build Security + +- All releases are built with `cargo build --release` +- Dependencies are audited using `cargo audit` +- Binary stripping enabled to reduce attack surface + +### Supply Chain Security + +- Dependencies are pinned via `Cargo.lock` +- Minimal dependency footprint +- No runtime network access required (offline-first design) + +### Code Security + +- Written in Rust for memory safety +- No `unsafe` blocks in core functionality +- Input validation on all user-provided data +- Path traversal protection in file operations + +## Security-Related Configuration + +### Safe Defaults + +conflow is designed with security-conscious defaults: + +- No automatic code execution without explicit pipeline definition +- Cache is local-only (no network sync) +- No telemetry or data collection +- Sandboxed execution where possible + +### Permissions + +conflow requires: +- Read access to configuration files +- Write access to output directories and cache +- Execute access for CUE and Nickel binaries + +## Known Limitations + +- Pipeline definitions can execute arbitrary shell commands via the `shell` tool type +- Users should review `.conflow.yaml` files from untrusted sources before running + +## Security Contacts + +- Primary: security@conflow.dev +- GitLab Issues: Use confidential issue feature +- PGP Key: Available upon request + +## Acknowledgments + +We thank all security researchers who responsibly disclose vulnerabilities. diff --git a/src/analyzer/complexity.rs b/src/analyzer/complexity.rs index 801cd28..2225ad5 100644 --- a/src/analyzer/complexity.rs +++ b/src/analyzer/complexity.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Complexity analysis for configuration files use super::ConfigFormat; diff --git a/src/analyzer/config_detector.rs b/src/analyzer/config_detector.rs index 0b403b3..61a03c4 100644 --- a/src/analyzer/config_detector.rs +++ b/src/analyzer/config_detector.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Configuration format detection use std::path::Path; diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index 75ccddf..14a9b13 100644 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Configuration analyzer //! //! Analyzes configuration files and recommends appropriate tools. diff --git a/src/analyzer/patterns.rs b/src/analyzer/patterns.rs index f133891..9b25572 100644 --- a/src/analyzer/patterns.rs +++ b/src/analyzer/patterns.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Pattern recognition for configuration files //! //! Identifies common patterns in configuration files to aid in tool selection. diff --git a/src/analyzer/recommender.rs b/src/analyzer/recommender.rs index ec781ec..75714e4 100644 --- a/src/analyzer/recommender.rs +++ b/src/analyzer/recommender.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Tool recommendation engine //! //! Recommends the appropriate tool (CUE or Nickel) based on complexity analysis. diff --git a/src/cache/filesystem.rs b/src/cache/filesystem.rs index d685899..8f02c8a 100644 --- a/src/cache/filesystem.rs +++ b/src/cache/filesystem.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Filesystem-based cache implementation //! //! Stores cache entries as JSON files in a cache directory. diff --git a/src/cache/hash.rs b/src/cache/hash.rs index 487d6d4..f35a2ee 100644 --- a/src/cache/hash.rs +++ b/src/cache/hash.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Content hashing for cache keys //! //! Uses BLAKE3 for fast, secure content hashing. diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 4a71b16..50c4358 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Caching layer for pipeline results //! //! Provides file-based caching to avoid redundant stage executions. diff --git a/src/cli/analyze.rs b/src/cli/analyze.rs index d127190..0a6b9e1 100644 --- a/src/cli/analyze.rs +++ b/src/cli/analyze.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Analyze command - analyze configuration files and recommend tools use colored::Colorize; diff --git a/src/cli/cache.rs b/src/cli/cache.rs index 95d89e6..4215cb7 100644 --- a/src/cli/cache.rs +++ b/src/cli/cache.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Cache command - manage the cache use colored::Colorize; diff --git a/src/cli/graph.rs b/src/cli/graph.rs index 5b19089..939a0b5 100644 --- a/src/cli/graph.rs +++ b/src/cli/graph.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Graph command - visualize pipeline as a graph use miette::Result; diff --git a/src/cli/init.rs b/src/cli/init.rs index dbe40da..56230a1 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Init command - create a new conflow project use colored::Colorize; diff --git a/src/cli/mod.rs b/src/cli/mod.rs index a2182df..6d16948 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! CLI command definitions and handlers //! //! Defines the command-line interface for conflow. diff --git a/src/cli/run.rs b/src/cli/run.rs index 55383e3..f9590e7 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Run command - execute the pipeline use colored::Colorize; diff --git a/src/cli/validate.rs b/src/cli/validate.rs index 25c2fee..bfc2e94 100644 --- a/src/cli/validate.rs +++ b/src/cli/validate.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Validate command - check pipeline configuration use colored::Colorize; diff --git a/src/cli/watch.rs b/src/cli/watch.rs index 8f45aba..61263bf 100644 --- a/src/cli/watch.rs +++ b/src/cli/watch.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Watch command - re-run pipeline on file changes use colored::Colorize; diff --git a/src/errors/educational.rs b/src/errors/educational.rs index a771736..11c0f79 100644 --- a/src/errors/educational.rs +++ b/src/errors/educational.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Educational error messages //! //! Provides detailed, helpful explanations for common errors diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 4981d4f..5c92eb2 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Error types with educational messages //! //! conflow provides helpful, educational error messages that guide users diff --git a/src/errors/recovery.rs b/src/errors/recovery.rs index 616f541..5690112 100644 --- a/src/errors/recovery.rs +++ b/src/errors/recovery.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Error recovery suggestions //! //! Provides actionable suggestions for recovering from errors. diff --git a/src/executors/cue.rs b/src/executors/cue.rs index 24e8d88..6254380 100644 --- a/src/executors/cue.rs +++ b/src/executors/cue.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! CUE executor //! //! Executes CUE commands (vet, export, eval, fmt, def). diff --git a/src/executors/mod.rs b/src/executors/mod.rs index 486e7be..9ac6fe6 100644 --- a/src/executors/mod.rs +++ b/src/executors/mod.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Tool executors //! //! This module provides the executor trait and implementations diff --git a/src/executors/nickel.rs b/src/executors/nickel.rs index 7e54a70..8cff3a9 100644 --- a/src/executors/nickel.rs +++ b/src/executors/nickel.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Nickel executor //! //! Executes Nickel commands (export, typecheck, query, format). diff --git a/src/executors/shell.rs b/src/executors/shell.rs index 96609f6..8972c1c 100644 --- a/src/executors/shell.rs +++ b/src/executors/shell.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Shell executor //! //! Executes arbitrary shell commands. diff --git a/src/lib.rs b/src/lib.rs index a4792f3..4807055 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! # conflow - Configuration Flow Orchestrator //! //! `conflow` intelligently orchestrates CUE, Nickel, and configuration validation workflows. diff --git a/src/main.rs b/src/main.rs index 42d732c..76f223f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! conflow - Configuration Flow Orchestrator //! //! Intelligently orchestrate CUE, Nickel, and configuration validation workflows. diff --git a/src/pipeline/dag.rs b/src/pipeline/dag.rs index 3b7c91f..b35d2e9 100644 --- a/src/pipeline/dag.rs +++ b/src/pipeline/dag.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! DAG (Directed Acyclic Graph) builder for pipeline dependencies //! //! Builds and validates dependency graphs for pipeline stages, diff --git a/src/pipeline/definition.rs b/src/pipeline/definition.rs index 5ba32c6..d3d326b 100644 --- a/src/pipeline/definition.rs +++ b/src/pipeline/definition.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Pipeline definition structures //! //! Defines the schema for .conflow.yaml files. diff --git a/src/pipeline/executor.rs b/src/pipeline/executor.rs index f12e8f8..a4f3366 100644 --- a/src/pipeline/executor.rs +++ b/src/pipeline/executor.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Pipeline executor //! //! Orchestrates the execution of pipeline stages in dependency order. diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index 07bd863..4bbe446 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Pipeline definitions and types //! //! This module defines the core data structures for conflow pipelines, diff --git a/src/pipeline/validation.rs b/src/pipeline/validation.rs index db0e5d8..206b251 100644 --- a/src/pipeline/validation.rs +++ b/src/pipeline/validation.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Pipeline validation //! //! Validates pipeline configuration before execution. diff --git a/src/rsr/badges.rs b/src/rsr/badges.rs new file mode 100644 index 0000000..5e30ff2 --- /dev/null +++ b/src/rsr/badges.rs @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + +//! Compliance badge generation +//! +//! Generates SVG badges for CI pipelines showing compliance status. + +use super::compliance::{ComplianceLevel, ComplianceReport}; + +/// Badge style +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BadgeStyle { + /// Flat style (shields.io flat) + Flat, + /// Flat square style + FlatSquare, + /// Plastic style (rounded) + Plastic, + /// For the badge style + ForTheBadge, +} + +impl BadgeStyle { + pub fn as_str(&self) -> &'static str { + match self { + Self::Flat => "flat", + Self::FlatSquare => "flat-square", + Self::Plastic => "plastic", + Self::ForTheBadge => "for-the-badge", + } + } +} + +impl Default for BadgeStyle { + fn default() -> Self { + Self::Flat + } +} + +/// Badge generator +#[derive(Clone)] +pub struct BadgeGenerator { + style: BadgeStyle, + label: String, +} + +impl BadgeGenerator { + /// Create a new badge generator + pub fn new() -> Self { + Self { + style: BadgeStyle::default(), + label: "RSR".into(), + } + } + + /// Set badge style + pub fn style(mut self, style: BadgeStyle) -> Self { + self.style = style; + self + } + + /// Set label text + pub fn label(mut self, label: impl Into) -> Self { + self.label = label.into(); + self + } + + /// Generate SVG badge from compliance report + pub fn generate(&self, report: &ComplianceReport) -> String { + let (status, color) = self.level_to_status_color(report.level); + let score = format!("{:.0}%", report.score * 100.0); + + self.generate_svg(&self.label, &status, color, Some(&score)) + } + + /// Generate a simple level badge + pub fn generate_level(&self, level: ComplianceLevel) -> String { + let (status, color) = self.level_to_status_color(level); + self.generate_svg(&self.label, &status, color, None) + } + + /// Generate badge with custom status + pub fn generate_custom(&self, label: &str, status: &str, color: &str) -> String { + self.generate_svg(label, status, color, None) + } + + fn level_to_status_color(&self, level: ComplianceLevel) -> (String, &'static str) { + match level { + ComplianceLevel::Excellent => ("excellent".into(), "#4c1"), + ComplianceLevel::Good => ("good".into(), "#97ca00"), + ComplianceLevel::Basic => ("basic".into(), "#dfb317"), + ComplianceLevel::NonCompliant => ("non-compliant".into(), "#e05d44"), + } + } + + fn generate_svg(&self, label: &str, status: &str, color: &str, score: Option<&str>) -> String { + let full_status = if let Some(s) = score { + format!("{} ({})", status, s) + } else { + status.to_string() + }; + + match self.style { + BadgeStyle::Flat => self.flat_badge(label, &full_status, color), + BadgeStyle::FlatSquare => self.flat_square_badge(label, &full_status, color), + BadgeStyle::Plastic => self.plastic_badge(label, &full_status, color), + BadgeStyle::ForTheBadge => self.for_the_badge(label, &full_status, color), + } + } + + fn flat_badge(&self, label: &str, status: &str, color: &str) -> String { + let label_width = self.text_width(label) + 10; + let status_width = self.text_width(status) + 10; + let total_width = label_width + status_width; + let label_x = label_width / 2; + let status_x = label_width + status_width / 2; + + format!( + r##" + {label}: {status} + + + + + + + + + + + + + + + {label} + + {status} + +"## + ) + } + + fn flat_square_badge(&self, label: &str, status: &str, color: &str) -> String { + let label_width = self.text_width(label) + 10; + let status_width = self.text_width(status) + 10; + let total_width = label_width + status_width; + let label_x = label_width / 2; + let status_x = label_width + status_width / 2; + + format!( + r##" + {label}: {status} + + + + + + {label} + {status} + +"## + ) + } + + fn plastic_badge(&self, label: &str, status: &str, color: &str) -> String { + let label_width = self.text_width(label) + 10; + let status_width = self.text_width(status) + 10; + let total_width = label_width + status_width; + let label_x = label_width / 2; + let status_x = label_width + status_width / 2; + + format!( + r##" + {label}: {status} + + + + + + + + + + + + + + + + {label} + {status} + +"## + ) + } + + fn for_the_badge(&self, label: &str, status: &str, color: &str) -> String { + let label_upper = label.to_uppercase(); + let status_upper = status.to_uppercase(); + + let label_width = self.text_width_large(&label_upper) + 20; + let status_width = self.text_width_large(&status_upper) + 20; + let total_width = label_width + status_width; + let label_x = label_width / 2; + let status_x = label_width + status_width / 2; + let label_text_width = label_width - 20; + let status_text_width = status_width - 20; + + format!( + r##" + {label}: {status} + + + + + + {label_upper} + {status_upper} + +"## + ) + } + + fn text_width(&self, text: &str) -> usize { + // Approximate character width for 11px Verdana + text.len() * 6 + 4 + } + + fn text_width_large(&self, text: &str) -> usize { + // Approximate character width for larger text + text.len() * 8 + 4 + } +} + +impl Default for BadgeGenerator { + fn default() -> Self { + Self::new() + } +} + +/// Generate shields.io compatible URL +pub fn shields_io_url(report: &ComplianceReport) -> String { + let (message, color) = match report.level { + ComplianceLevel::Excellent => ("excellent", "brightgreen"), + ComplianceLevel::Good => ("good", "green"), + ComplianceLevel::Basic => ("basic", "yellow"), + ComplianceLevel::NonCompliant => ("non--compliant", "red"), + }; + + let score = format!("{:.0}%25", report.score * 100.0); + + format!( + "https://img.shields.io/badge/RSR-{}%20({})-{}", + message, score, color + ) +} + +/// Generate markdown badge +pub fn markdown_badge(report: &ComplianceReport, link: Option<&str>) -> String { + let url = shields_io_url(report); + let alt = format!("RSR Compliance: {:?}", report.level); + + if let Some(link) = link { + format!("[![{}]({})]({})", alt, url, link) + } else { + format!("![{}]({})", alt, url) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rsr::compliance::ComplianceStats; + + fn sample_report(level: ComplianceLevel, score: f64) -> ComplianceReport { + ComplianceReport { + level, + score, + requirements: vec![], + stats: ComplianceStats::default(), + } + } + + #[test] + fn test_generate_badge() { + let generator = BadgeGenerator::new(); + let report = sample_report(ComplianceLevel::Excellent, 0.95); + + let svg = generator.generate(&report); + assert!(svg.contains(", +} + +fn default_version() -> String { + "1".to_string() +} + +/// Project configuration +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ProjectConfig { + /// Project name + pub name: Option, + + /// Project description + pub description: Option, + + /// Project tier (1-4, higher is more strict) + pub tier: Option, + + /// Project tags + #[serde(default)] + pub tags: Vec, +} + +/// Requirements configuration +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RequirementsConfig { + /// Skip specific requirements + #[serde(default)] + pub skip: Vec, + + /// Custom requirement definitions + #[serde(default)] + pub custom: Vec, + + /// Override requirement classes + #[serde(default)] + pub overrides: HashMap, + + /// Import requirements from external files + #[serde(default)] + pub imports: Vec, +} + +/// Override for a requirement +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RequirementOverride { + /// Override class + pub class: Option, + + /// Skip this requirement + #[serde(default)] + pub skip: bool, + + /// Reason for override + pub reason: Option, +} + +/// Integration settings +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct IntegrationsConfig { + /// conflow integration + #[serde(default)] + pub conflow: ConflowIntegration, + + /// CI integration + #[serde(default)] + pub ci: CiIntegration, + + /// Notification settings + #[serde(default)] + pub notifications: NotificationSettings, +} + +/// conflow integration settings +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConflowIntegration { + /// Enable conflow integration + #[serde(default = "default_true")] + pub enabled: bool, + + /// Custom pipeline file + pub pipeline: Option, + + /// Run before compliance check + #[serde(default)] + pub run_before_check: bool, +} + +fn default_true() -> bool { + true +} + +impl Default for ConflowIntegration { + fn default() -> Self { + Self { + enabled: true, + pipeline: None, + run_before_check: false, + } + } +} + +/// CI integration settings +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CiIntegration { + /// CI provider + pub provider: Option, + + /// Path to CI config + pub config: Option, + + /// Fail CI on non-compliance + #[serde(default)] + pub fail_on_noncompliant: bool, + + /// Generate badges + #[serde(default)] + pub generate_badges: bool, +} + +/// CI Provider +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CiProvider { + GitHub, + GitLab, + Jenkins, + CircleCI, + Travis, + Azure, +} + +/// Notification settings +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct NotificationSettings { + /// Notify on regression + #[serde(default)] + pub on_regression: bool, + + /// Notify on improvement + #[serde(default)] + pub on_improvement: bool, + + /// Slack webhook + pub slack_webhook: Option, + + /// Email addresses + #[serde(default)] + pub emails: Vec, +} + +/// Compliance configuration +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ComplianceConfig { + /// Target compliance level + pub target_level: Option, + + /// Exceptions to requirements + #[serde(default)] + pub exceptions: Vec, + + /// History tracking + #[serde(default)] + pub track_history: bool, + + /// History file path + pub history_file: Option, +} + +/// Target compliance level +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TargetLevel { + Basic, + Good, + Excellent, +} + +/// Exception to a requirement +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComplianceException { + /// Requirement ID + pub requirement: String, + + /// Reason for exception + pub reason: String, + + /// Expiration date (ISO 8601) + pub expires: Option, + + /// Approved by + pub approved_by: Option, +} + +/// Reference to a schema +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SchemaReference { + /// Schema ID + pub id: String, + + /// Path to schema file + pub path: PathBuf, + + /// Schema type + pub schema_type: Option, +} + +impl RsrConfig { + /// Load from file + pub fn load(path: &Path) -> Result { + if !path.exists() { + return Ok(Self::default()); + } + + let content = std::fs::read_to_string(path).map_err(|e| ConflowError::Io { + message: e.to_string(), + })?; + + serde_yaml::from_str(&content).map_err(|e| ConflowError::Yaml { + message: e.to_string(), + }) + } + + /// Load from project directory (looks for .rsr.yaml) + pub fn load_from_project(project_root: &Path) -> Result { + let config_path = project_root.join(".rsr.yaml"); + Self::load(&config_path) + } + + /// Save to file + pub fn save(&self, path: &Path) -> Result<(), ConflowError> { + let content = serde_yaml::to_string(self).map_err(|e| ConflowError::Yaml { + message: e.to_string(), + })?; + + std::fs::write(path, content).map_err(|e| ConflowError::Io { + message: e.to_string(), + })?; + + Ok(()) + } + + /// Check if a requirement should be skipped + pub fn should_skip(&self, requirement_id: &str) -> bool { + // Check direct skip list + if self.requirements.skip.contains(&requirement_id.to_string()) { + return true; + } + + // Check overrides + if let Some(override_cfg) = self.requirements.overrides.get(requirement_id) { + if override_cfg.skip { + return true; + } + } + + // Check exceptions + for exception in &self.compliance.exceptions { + if exception.requirement == requirement_id { + // Check if exception is still valid + if let Some(ref expires) = exception.expires { + if let Ok(expiry) = chrono::DateTime::parse_from_rfc3339(expires) { + if expiry > chrono::Utc::now() { + return true; + } + } + } else { + // No expiry, always valid + return true; + } + } + } + + false + } + + /// Get class override for a requirement + pub fn class_override(&self, requirement_id: &str) -> Option { + self.requirements + .overrides + .get(requirement_id) + .and_then(|o| o.class) + } + + /// Get all custom requirements + pub fn custom_requirements(&self) -> &[RsrRequirement] { + &self.requirements.custom + } + + /// Load imported requirements + pub fn load_imports(&self, base_path: &Path) -> Result, ConflowError> { + let mut requirements = Vec::new(); + + for import_path in &self.requirements.imports { + let full_path = base_path.join(import_path); + let content = std::fs::read_to_string(&full_path).map_err(|e| ConflowError::Io { + message: format!("Failed to load import {}: {}", import_path.display(), e), + })?; + + let imported: Vec = + serde_yaml::from_str(&content).map_err(|e| ConflowError::Yaml { + message: e.to_string(), + })?; + + requirements.extend(imported); + } + + Ok(requirements) + } +} + +impl Default for RsrConfig { + fn default() -> Self { + Self { + version: default_version(), + project: ProjectConfig::default(), + requirements: RequirementsConfig::default(), + integrations: IntegrationsConfig::default(), + compliance: ComplianceConfig::default(), + schemas: Vec::new(), + } + } +} + +/// Generate a default .rsr.yaml configuration +pub fn generate_default_config(project_name: &str) -> String { + format!( + r#"# RSR Configuration +# See: https://rsr.dev/docs/config + +version: "1" + +project: + name: "{}" + # description: "Project description" + # tier: 2 # 1-4, higher is more strict + +requirements: + # Skip specific requirements + skip: [] + + # Override requirement classes + overrides: {{}} + # RSR-CONFIG-001: + # class: advisory + # reason: "Not applicable for this project" + + # Import custom requirements + imports: [] + # - .rsr/custom-requirements.yaml + +integrations: + conflow: + enabled: true + # pipeline: .conflow.yaml + # run_before_check: false + + ci: + # provider: github + fail_on_noncompliant: false + generate_badges: true + +compliance: + target_level: good + track_history: true + # history_file: .rsr/history.json + + exceptions: [] + # - requirement: RSR-CONFIG-003 + # reason: "Single environment project" + # expires: "2025-12-31T00:00:00Z" +"#, + project_name + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_load_default() { + let temp = TempDir::new().unwrap(); + let config = RsrConfig::load_from_project(temp.path()).unwrap(); + + assert_eq!(config.version, "1"); + } + + #[test] + fn test_load_config() { + let temp = TempDir::new().unwrap(); + + std::fs::write( + temp.path().join(".rsr.yaml"), + r#" +version: "1" +project: + name: test-project + tier: 2 +requirements: + skip: + - RSR-CONFIG-003 +"#, + ) + .unwrap(); + + let config = RsrConfig::load_from_project(temp.path()).unwrap(); + + assert_eq!(config.project.name, Some("test-project".into())); + assert_eq!(config.project.tier, Some(2)); + assert!(config.should_skip("RSR-CONFIG-003")); + assert!(!config.should_skip("RSR-CONFIG-001")); + } + + #[test] + fn test_exception_handling() { + let config = RsrConfig { + compliance: ComplianceConfig { + exceptions: vec![ + ComplianceException { + requirement: "RSR-001".into(), + reason: "Test".into(), + expires: None, + approved_by: None, + }, + ComplianceException { + requirement: "RSR-002".into(), + reason: "Test".into(), + expires: Some("2020-01-01T00:00:00Z".into()), // Expired + approved_by: None, + }, + ], + ..Default::default() + }, + ..Default::default() + }; + + assert!(config.should_skip("RSR-001")); // No expiry + assert!(!config.should_skip("RSR-002")); // Expired + } + + #[test] + fn test_generate_default() { + let config = generate_default_config("my-project"); + assert!(config.contains("my-project")); + assert!(config.contains("version:")); + } +} diff --git a/src/rsr/diff.rs b/src/rsr/diff.rs new file mode 100644 index 0000000..16e1f87 --- /dev/null +++ b/src/rsr/diff.rs @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + +//! Compliance diff reports +//! +//! Track changes between compliance runs and generate diff reports. + +use std::collections::HashMap; +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use super::compliance::{ComplianceLevel, ComplianceReport, RequirementResult}; +use crate::ConflowError; + +/// Diff between two compliance reports +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComplianceDiff { + /// Previous report timestamp + pub previous_timestamp: Option, + + /// Current report timestamp + pub current_timestamp: String, + + /// Level change + pub level_change: LevelChange, + + /// Score change + pub score_change: ScoreChange, + + /// Requirements that changed status + pub requirement_changes: Vec, + + /// Summary statistics + pub summary: DiffSummary, +} + +/// Level change between reports +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LevelChange { + pub previous: Option, + pub current: ComplianceLevel, + pub direction: ChangeDirection, +} + +/// Score change between reports +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScoreChange { + pub previous: Option, + pub current: f64, + pub delta: f64, + pub percentage_change: f64, +} + +/// Direction of change +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ChangeDirection { + Improved, + Degraded, + Unchanged, + New, +} + +/// Change in a specific requirement +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RequirementChange { + pub requirement_id: String, + pub previous_met: Option, + pub current_met: bool, + pub change_type: RequirementChangeType, +} + +/// Type of requirement change +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum RequirementChangeType { + /// Was failing, now passing + Fixed, + /// Was passing, now failing + Regressed, + /// New requirement added + New, + /// Requirement removed + Removed, + /// No change + Unchanged, +} + +/// Summary of diff +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct DiffSummary { + pub total_requirements: usize, + pub fixed: usize, + pub regressed: usize, + pub new_passing: usize, + pub new_failing: usize, + pub unchanged_passing: usize, + pub unchanged_failing: usize, +} + +/// Compliance history storage +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ComplianceHistory { + /// History entries, newest first + pub entries: Vec, +} + +/// A single history entry +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HistoryEntry { + pub timestamp: String, + pub level: ComplianceLevel, + pub score: f64, + pub requirements: HashMap, + pub git_commit: Option, +} + +impl ComplianceHistory { + /// Create new empty history + pub fn new() -> Self { + Self::default() + } + + /// Load history from file + pub fn load(path: &Path) -> Result { + if !path.exists() { + return Ok(Self::new()); + } + + let content = std::fs::read_to_string(path).map_err(|e| ConflowError::Io { + message: e.to_string(), + })?; + + serde_json::from_str(&content).map_err(|e| ConflowError::Json { + message: e.to_string(), + }) + } + + /// Save history to file + pub fn save(&self, path: &Path) -> Result<(), ConflowError> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| ConflowError::Io { + message: e.to_string(), + })?; + } + + let content = serde_json::to_string_pretty(self).map_err(|e| ConflowError::Json { + message: e.to_string(), + })?; + + std::fs::write(path, content).map_err(|e| ConflowError::Io { + message: e.to_string(), + })?; + + Ok(()) + } + + /// Add a new entry from a compliance report + pub fn add_entry(&mut self, report: &ComplianceReport, git_commit: Option) { + let requirements: HashMap = report + .requirements + .iter() + .map(|r| (r.requirement_id.clone(), r.met)) + .collect(); + + let entry = HistoryEntry { + timestamp: chrono::Utc::now().to_rfc3339(), + level: report.level, + score: report.score, + requirements, + git_commit, + }; + + self.entries.insert(0, entry); + + // Keep only last 100 entries + if self.entries.len() > 100 { + self.entries.truncate(100); + } + } + + /// Get the most recent entry + pub fn latest(&self) -> Option<&HistoryEntry> { + self.entries.first() + } + + /// Get the previous entry (second most recent) + pub fn previous(&self) -> Option<&HistoryEntry> { + self.entries.get(1) + } + + /// Generate diff between latest and previous + pub fn diff_latest(&self) -> Option { + let current = self.latest()?; + let previous = self.previous(); + + Some(Self::diff_entries(previous, current)) + } + + /// Generate diff between any two entries + pub fn diff_entries(previous: Option<&HistoryEntry>, current: &HistoryEntry) -> ComplianceDiff { + let level_change = LevelChange { + previous: previous.map(|p| p.level), + current: current.level, + direction: match previous { + None => ChangeDirection::New, + Some(p) if current.level > p.level => ChangeDirection::Improved, + Some(p) if current.level < p.level => ChangeDirection::Degraded, + _ => ChangeDirection::Unchanged, + }, + }; + + let score_change = ScoreChange { + previous: previous.map(|p| p.score), + current: current.score, + delta: current.score - previous.map(|p| p.score).unwrap_or(0.0), + percentage_change: previous + .map(|p| { + if p.score > 0.0 { + ((current.score - p.score) / p.score) * 100.0 + } else { + 0.0 + } + }) + .unwrap_or(0.0), + }; + + let mut requirement_changes = Vec::new(); + let mut summary = DiffSummary::default(); + + // Collect all requirement IDs + let mut all_ids: Vec = current.requirements.keys().cloned().collect(); + if let Some(prev) = previous { + for id in prev.requirements.keys() { + if !all_ids.contains(id) { + all_ids.push(id.clone()); + } + } + } + + for id in all_ids { + let current_met = current.requirements.get(&id).copied(); + let previous_met = previous.and_then(|p| p.requirements.get(&id).copied()); + + let change_type = match (previous_met, current_met) { + (None, Some(true)) => RequirementChangeType::New, + (None, Some(false)) => RequirementChangeType::New, + (None, None) => continue, + (Some(_), None) => RequirementChangeType::Removed, + (Some(false), Some(true)) => RequirementChangeType::Fixed, + (Some(true), Some(false)) => RequirementChangeType::Regressed, + (Some(true), Some(true)) => RequirementChangeType::Unchanged, + (Some(false), Some(false)) => RequirementChangeType::Unchanged, + }; + + // Update summary + summary.total_requirements += 1; + match change_type { + RequirementChangeType::Fixed => summary.fixed += 1, + RequirementChangeType::Regressed => summary.regressed += 1, + RequirementChangeType::New => { + if current_met.unwrap_or(false) { + summary.new_passing += 1; + } else { + summary.new_failing += 1; + } + } + RequirementChangeType::Unchanged => { + if current_met.unwrap_or(false) { + summary.unchanged_passing += 1; + } else { + summary.unchanged_failing += 1; + } + } + RequirementChangeType::Removed => {} + } + + requirement_changes.push(RequirementChange { + requirement_id: id, + previous_met, + current_met: current_met.unwrap_or(false), + change_type, + }); + } + + ComplianceDiff { + previous_timestamp: previous.map(|p| p.timestamp.clone()), + current_timestamp: current.timestamp.clone(), + level_change, + score_change, + requirement_changes, + summary, + } + } + + /// Get trend over time + pub fn trend(&self, count: usize) -> Vec<(String, f64)> { + self.entries + .iter() + .take(count) + .map(|e| (e.timestamp.clone(), e.score)) + .collect() + } +} + +/// Diff reporter for CLI output +pub struct DiffReporter; + +impl DiffReporter { + /// Format diff for CLI output + pub fn format_text(diff: &ComplianceDiff) -> String { + let mut output = String::new(); + + output.push_str("Compliance Diff Report\n"); + output.push_str(&"═".repeat(50)); + output.push('\n'); + + // Level change + let level_emoji = match diff.level_change.direction { + ChangeDirection::Improved => "📈", + ChangeDirection::Degraded => "📉", + ChangeDirection::Unchanged => "➡️", + ChangeDirection::New => "🆕", + }; + + output.push_str(&format!( + "\nLevel: {} {:?} → {:?}\n", + level_emoji, + diff.level_change.previous.unwrap_or(ComplianceLevel::NonCompliant), + diff.level_change.current + )); + + // Score change + let score_sign = if diff.score_change.delta >= 0.0 { "+" } else { "" }; + output.push_str(&format!( + "Score: {:.0}% ({}{:.1}%)\n", + diff.score_change.current * 100.0, + score_sign, + diff.score_change.delta * 100.0 + )); + + // Summary + output.push_str("\nSummary:\n"); + if diff.summary.fixed > 0 { + output.push_str(&format!(" ✅ {} fixed\n", diff.summary.fixed)); + } + if diff.summary.regressed > 0 { + output.push_str(&format!(" ❌ {} regressed\n", diff.summary.regressed)); + } + if diff.summary.new_passing > 0 { + output.push_str(&format!(" 🆕 {} new (passing)\n", diff.summary.new_passing)); + } + if diff.summary.new_failing > 0 { + output.push_str(&format!(" 🆕 {} new (failing)\n", diff.summary.new_failing)); + } + + // Requirement details + let changes: Vec<_> = diff + .requirement_changes + .iter() + .filter(|c| c.change_type != RequirementChangeType::Unchanged) + .collect(); + + if !changes.is_empty() { + output.push_str("\nChanges:\n"); + for change in changes { + let icon = match change.change_type { + RequirementChangeType::Fixed => "✅", + RequirementChangeType::Regressed => "❌", + RequirementChangeType::New => "🆕", + RequirementChangeType::Removed => "🗑️", + RequirementChangeType::Unchanged => "➡️", + }; + output.push_str(&format!(" {} {}\n", icon, change.requirement_id)); + } + } + + output + } + + /// Format diff as JSON + pub fn format_json(diff: &ComplianceDiff) -> Result { + serde_json::to_string_pretty(diff).map_err(|e| ConflowError::Json { + message: e.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rsr::compliance::ComplianceStats; + + fn sample_report(level: ComplianceLevel, score: f64, requirements: Vec<(&str, bool)>) -> ComplianceReport { + ComplianceReport { + level, + score, + requirements: requirements + .into_iter() + .map(|(id, met)| RequirementResult { + requirement_id: id.to_string(), + met, + details: vec![], + remediation: None, + }) + .collect(), + stats: ComplianceStats::default(), + } + } + + #[test] + fn test_history_add_and_diff() { + let mut history = ComplianceHistory::new(); + + // Add first report + let report1 = sample_report( + ComplianceLevel::Basic, + 0.6, + vec![("RSR-001", false), ("RSR-002", true)], + ); + history.add_entry(&report1, None); + + // Add second report (improved) + let report2 = sample_report( + ComplianceLevel::Good, + 0.8, + vec![("RSR-001", true), ("RSR-002", true)], + ); + history.add_entry(&report2, None); + + let diff = history.diff_latest().unwrap(); + + assert_eq!(diff.level_change.direction, ChangeDirection::Improved); + assert!(diff.score_change.delta > 0.0); + assert_eq!(diff.summary.fixed, 1); + } + + #[test] + fn test_diff_regression() { + let mut history = ComplianceHistory::new(); + + let report1 = sample_report( + ComplianceLevel::Good, + 0.8, + vec![("RSR-001", true), ("RSR-002", true)], + ); + history.add_entry(&report1, None); + + let report2 = sample_report( + ComplianceLevel::Basic, + 0.5, + vec![("RSR-001", false), ("RSR-002", true)], + ); + history.add_entry(&report2, None); + + let diff = history.diff_latest().unwrap(); + + assert_eq!(diff.level_change.direction, ChangeDirection::Degraded); + assert!(diff.score_change.delta < 0.0); + assert_eq!(diff.summary.regressed, 1); + } + + #[test] + fn test_format_text() { + let mut history = ComplianceHistory::new(); + + let report1 = sample_report(ComplianceLevel::Basic, 0.5, vec![("RSR-001", false)]); + history.add_entry(&report1, None); + + let report2 = sample_report(ComplianceLevel::Good, 0.8, vec![("RSR-001", true)]); + history.add_entry(&report2, None); + + let diff = history.diff_latest().unwrap(); + let text = DiffReporter::format_text(&diff); + + assert!(text.contains("Compliance Diff Report")); + assert!(text.contains("fixed")); + } +} diff --git a/src/rsr/remediation.rs b/src/rsr/remediation.rs new file mode 100644 index 0000000..9acf20a --- /dev/null +++ b/src/rsr/remediation.rs @@ -0,0 +1,569 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + +//! Auto-remediation for RSR requirements +//! +//! Automatically fixes failing requirements where possible. + +use std::path::Path; + +use crate::ConflowError; + +use super::compliance::RequirementResult; +use super::requirements::{RsrRequirement, RsrRequirementRegistry}; + +/// Result of an auto-remediation attempt +#[derive(Debug, Clone)] +pub struct RemediationResult { + /// Requirement ID + pub requirement_id: String, + + /// Whether remediation was successful + pub success: bool, + + /// Actions taken + pub actions: Vec, + + /// Error message if failed + pub error: Option, +} + +/// A single remediation action +#[derive(Debug, Clone)] +pub struct RemediationAction { + /// Description of the action + pub description: String, + + /// Whether this action was completed + pub completed: bool, + + /// Files created or modified + pub files_affected: Vec, +} + +/// Auto-remediation engine +pub struct AutoRemediator { + registry: RsrRequirementRegistry, + dry_run: bool, +} + +impl AutoRemediator { + /// Create a new auto-remediator + pub fn new() -> Self { + Self { + registry: RsrRequirementRegistry::new(), + dry_run: false, + } + } + + /// Create with custom registry + pub fn with_registry(registry: RsrRequirementRegistry) -> Self { + Self { + registry, + dry_run: false, + } + } + + /// Set dry run mode (don't actually modify files) + pub fn dry_run(mut self, dry_run: bool) -> Self { + self.dry_run = dry_run; + self + } + + /// Attempt to remediate a failing requirement + pub fn remediate( + &self, + result: &RequirementResult, + project_root: &Path, + ) -> Result { + let requirement = self + .registry + .get(&result.requirement_id) + .ok_or_else(|| ConflowError::ExecutionFailed { + message: format!("Unknown requirement: {}", result.requirement_id), + help: None, + })?; + + if !requirement.remediation.auto_fix { + return Ok(RemediationResult { + requirement_id: result.requirement_id.clone(), + success: false, + actions: vec![], + error: Some("Auto-fix not available for this requirement".into()), + }); + } + + let mut actions = Vec::new(); + + // Remediate based on requirement type + match result.requirement_id.as_str() { + "RSR-CONFIG-001" => { + actions.extend(self.remediate_config_001(project_root)?); + } + "RSR-CONFIG-002" => { + actions.extend(self.remediate_config_002(project_root)?); + } + "RSR-CONFIG-003" => { + actions.extend(self.remediate_config_003(project_root)?); + } + "RSR-CONFIG-004" => { + actions.extend(self.remediate_config_004(project_root)?); + } + _ => { + // Try generic remediation + actions.extend(self.remediate_generic(requirement, project_root)?); + } + } + + let all_completed = actions.iter().all(|a| a.completed); + + Ok(RemediationResult { + requirement_id: result.requirement_id.clone(), + success: all_completed, + actions, + error: if all_completed { + None + } else { + Some("Some remediation actions failed".into()) + }, + }) + } + + /// Remediate RSR-CONFIG-001: Configuration validation + fn remediate_config_001(&self, project_root: &Path) -> Result, ConflowError> { + let mut actions = Vec::new(); + + // Create schemas directory + let schemas_dir = project_root.join("schemas"); + if !schemas_dir.exists() { + if !self.dry_run { + std::fs::create_dir_all(&schemas_dir)?; + } + actions.push(RemediationAction { + description: "Create schemas directory".into(), + completed: true, + files_affected: vec!["schemas/".into()], + }); + } + + // Create basic CUE schema + let schema_path = schemas_dir.join("config.cue"); + if !schema_path.exists() { + let schema_content = r#"// Configuration Schema +package config + +#Config: { + // Add your configuration fields here + version?: string + name?: string + + // Example: environment settings + environment?: "development" | "staging" | "production" + + // Example: feature flags + features?: [string]: bool +} +"#; + if !self.dry_run { + std::fs::write(&schema_path, schema_content)?; + } + actions.push(RemediationAction { + description: "Create CUE schema template".into(), + completed: true, + files_affected: vec!["schemas/config.cue".into()], + }); + } + + Ok(actions) + } + + /// Remediate RSR-CONFIG-002: Configuration pipeline + fn remediate_config_002(&self, project_root: &Path) -> Result, ConflowError> { + let mut actions = Vec::new(); + + let pipeline_path = project_root.join(".conflow.yaml"); + if !pipeline_path.exists() { + let pipeline_content = r#"# conflow pipeline configuration +# Generated by RSR auto-remediation + +version: "1" +name: config-pipeline + +# Pipeline stages +stages: + # Validate configuration files + - name: validate + tool: + type: cue + command: vet + schemas: + - schemas/config.cue + input: + - "config/*.yaml" + - "config/*.json" + description: Validate configuration against schema + +# Optional: Enable caching +cache: + enabled: true + directory: .conflow-cache +"#; + if !self.dry_run { + std::fs::write(&pipeline_path, pipeline_content)?; + } + actions.push(RemediationAction { + description: "Create .conflow.yaml pipeline".into(), + completed: true, + files_affected: vec![".conflow.yaml".into()], + }); + } + + // Create config directory if it doesn't exist + let config_dir = project_root.join("config"); + if !config_dir.exists() { + if !self.dry_run { + std::fs::create_dir_all(&config_dir)?; + } + actions.push(RemediationAction { + description: "Create config directory".into(), + completed: true, + files_affected: vec!["config/".into()], + }); + + // Create example config + let example_config = config_dir.join("example.yaml"); + if !self.dry_run { + std::fs::write( + &example_config, + "# Example configuration\nversion: \"1.0\"\nname: my-app\nenvironment: development\n", + )?; + } + actions.push(RemediationAction { + description: "Create example configuration".into(), + completed: true, + files_affected: vec!["config/example.yaml".into()], + }); + } + + Ok(actions) + } + + /// Remediate RSR-CONFIG-003: Multi-environment configuration + fn remediate_config_003(&self, project_root: &Path) -> Result, ConflowError> { + let mut actions = Vec::new(); + + // Create environments directory + let env_dir = project_root.join("environments"); + if !env_dir.exists() { + if !self.dry_run { + std::fs::create_dir_all(&env_dir)?; + } + actions.push(RemediationAction { + description: "Create environments directory".into(), + completed: true, + files_affected: vec!["environments/".into()], + }); + } + + // Create base Nickel config + let base_path = env_dir.join("base.ncl"); + if !base_path.exists() { + let base_content = r#"# Base configuration +# Override these values per environment + +{ + app_name = "my-application", + version = "1.0.0", + + # Default settings + log_level = "info", + debug = false, + + # Feature flags + features = { + new_ui = false, + analytics = true, + }, + + # Database settings (to be overridden) + database = { + host = "localhost", + port = 5432, + pool_size = 10, + }, +} +"#; + if !self.dry_run { + std::fs::write(&base_path, base_content)?; + } + actions.push(RemediationAction { + description: "Create base Nickel configuration".into(), + completed: true, + files_affected: vec!["environments/base.ncl".into()], + }); + } + + // Create environment-specific overrides + for env in &["development", "staging", "production"] { + let env_path = env_dir.join(format!("{}.ncl", env)); + if !env_path.exists() { + let env_content = format!( + r#"# {} environment configuration +let base = import "base.ncl" in + +base & {{ + environment = "{}", + {} +}} +"#, + env, + env, + match *env { + "development" => "debug = true,\n log_level = \"debug\",", + "staging" => "log_level = \"info\",\n features.new_ui = true,", + "production" => "log_level = \"warn\",\n database.pool_size = 50,", + _ => "", + } + ); + if !self.dry_run { + std::fs::write(&env_path, env_content)?; + } + actions.push(RemediationAction { + description: format!("Create {} environment config", env), + completed: true, + files_affected: vec![format!("environments/{}.ncl", env)], + }); + } + } + + // Update .conflow.yaml to include environment generation + let pipeline_path = project_root.join(".conflow.yaml"); + if pipeline_path.exists() { + let content = std::fs::read_to_string(&pipeline_path)?; + if !content.contains("generate-") { + // Append environment generation stages + let addition = r#" + # Generate environment-specific configs + - name: generate-dev + tool: + type: nickel + command: export + format: yaml + input: environments/development.ncl + output: dist/config.development.yaml + description: Generate development config + + - name: generate-staging + tool: + type: nickel + command: export + format: yaml + input: environments/staging.ncl + output: dist/config.staging.yaml + description: Generate staging config + + - name: generate-production + tool: + type: nickel + command: export + format: yaml + input: environments/production.ncl + output: dist/config.production.yaml + description: Generate production config +"#; + if !self.dry_run { + let new_content = content + addition; + std::fs::write(&pipeline_path, new_content)?; + } + actions.push(RemediationAction { + description: "Add environment generation stages to pipeline".into(), + completed: true, + files_affected: vec![".conflow.yaml".into()], + }); + } + } + + Ok(actions) + } + + /// Remediate RSR-CONFIG-004: Configuration caching + fn remediate_config_004(&self, project_root: &Path) -> Result, ConflowError> { + let mut actions = Vec::new(); + + let pipeline_path = project_root.join(".conflow.yaml"); + if pipeline_path.exists() { + let content = std::fs::read_to_string(&pipeline_path)?; + + if !content.contains("cache:") { + // Add cache configuration + let cache_config = r#" +# Caching configuration +cache: + enabled: true + directory: .conflow-cache +"#; + if !self.dry_run { + let new_content = content + cache_config; + std::fs::write(&pipeline_path, new_content)?; + } + actions.push(RemediationAction { + description: "Enable caching in pipeline".into(), + completed: true, + files_affected: vec![".conflow.yaml".into()], + }); + } + } + + // Add cache directory to .gitignore + let gitignore_path = project_root.join(".gitignore"); + let gitignore_exists = gitignore_path.exists(); + let needs_cache_entry = if gitignore_exists { + let content = std::fs::read_to_string(&gitignore_path)?; + !content.contains(".conflow-cache") + } else { + true + }; + + if needs_cache_entry { + let addition = "\n# conflow cache\n.conflow-cache/\n"; + if !self.dry_run { + if gitignore_exists { + let content = std::fs::read_to_string(&gitignore_path)?; + std::fs::write(&gitignore_path, content + addition)?; + } else { + std::fs::write(&gitignore_path, addition)?; + } + } + actions.push(RemediationAction { + description: "Add cache directory to .gitignore".into(), + completed: true, + files_affected: vec![".gitignore".into()], + }); + } + + Ok(actions) + } + + /// Generic remediation based on requirement definition + fn remediate_generic( + &self, + requirement: &RsrRequirement, + project_root: &Path, + ) -> Result, ConflowError> { + let mut actions = Vec::new(); + + // Create required files + for file in &requirement.validation.file_exists { + let path = project_root.join(file); + if !path.exists() { + // Create parent directories + if let Some(parent) = path.parent() { + if !parent.exists() && !self.dry_run { + std::fs::create_dir_all(parent)?; + } + } + + // Create empty file or use template + if !self.dry_run { + std::fs::write(&path, "")?; + } + + actions.push(RemediationAction { + description: format!("Create required file: {}", file.display()), + completed: true, + files_affected: vec![file.display().to_string()], + }); + } + } + + // Remove forbidden files + for file in &requirement.validation.file_absent { + let path = project_root.join(file); + if path.exists() { + if !self.dry_run { + if path.is_dir() { + std::fs::remove_dir_all(&path)?; + } else { + std::fs::remove_file(&path)?; + } + } + + actions.push(RemediationAction { + description: format!("Remove forbidden file: {}", file.display()), + completed: true, + files_affected: vec![file.display().to_string()], + }); + } + } + + Ok(actions) + } + + /// Remediate multiple failing requirements + pub fn remediate_all( + &self, + results: &[RequirementResult], + project_root: &Path, + ) -> Result, ConflowError> { + let failed: Vec<_> = results.iter().filter(|r| !r.met).collect(); + let mut remediation_results = Vec::new(); + + for result in failed { + let remediation = self.remediate(result, project_root)?; + remediation_results.push(remediation); + } + + Ok(remediation_results) + } +} + +impl Default for AutoRemediator { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_remediate_config_002() { + let temp = TempDir::new().unwrap(); + let remediator = AutoRemediator::new(); + + let result = RequirementResult { + requirement_id: "RSR-CONFIG-002".into(), + met: false, + details: vec![], + remediation: None, + }; + + let remediation = remediator.remediate(&result, temp.path()).unwrap(); + assert!(remediation.success); + assert!(!remediation.actions.is_empty()); + + // Check that .conflow.yaml was created + assert!(temp.path().join(".conflow.yaml").exists()); + } + + #[test] + fn test_dry_run() { + let temp = TempDir::new().unwrap(); + let remediator = AutoRemediator::new().dry_run(true); + + let result = RequirementResult { + requirement_id: "RSR-CONFIG-002".into(), + met: false, + details: vec![], + remediation: None, + }; + + let remediation = remediator.remediate(&result, temp.path()).unwrap(); + assert!(remediation.success); + + // File should NOT be created in dry run + assert!(!temp.path().join(".conflow.yaml").exists()); + } +} diff --git a/src/rsr/templates.rs b/src/rsr/templates.rs new file mode 100644 index 0000000..172a2ef --- /dev/null +++ b/src/rsr/templates.rs @@ -0,0 +1,1119 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + +//! Template generation for compliant configurations +//! +//! Generate RSR-compliant configuration structures from templates. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::ConflowError; + +/// Template type +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum TemplateType { + /// Simple CUE validation pipeline + CueValidation, + /// Nickel generation pipeline + NickelGeneration, + /// Full pipeline (generate, validate, export) + FullPipeline, + /// Multi-environment configuration + MultiEnv, + /// Kubernetes configuration + Kubernetes, + /// Terraform configuration + Terraform, + /// Helm chart + Helm, + /// Docker Compose + DockerCompose, + /// Custom template + Custom, +} + +impl TemplateType { + pub fn as_str(&self) -> &'static str { + match self { + Self::CueValidation => "cue-validation", + Self::NickelGeneration => "nickel-generation", + Self::FullPipeline => "full-pipeline", + Self::MultiEnv => "multi-env", + Self::Kubernetes => "kubernetes", + Self::Terraform => "terraform", + Self::Helm => "helm", + Self::DockerCompose => "docker-compose", + Self::Custom => "custom", + } + } + + pub fn description(&self) -> &'static str { + match self { + Self::CueValidation => "Simple CUE schema validation", + Self::NickelGeneration => "Programmatic config generation with Nickel", + Self::FullPipeline => "Generate, validate, and export pipeline", + Self::MultiEnv => "Multi-environment configuration management", + Self::Kubernetes => "Kubernetes manifest validation", + Self::Terraform => "Terraform configuration validation", + Self::Helm => "Helm chart configuration", + Self::DockerCompose => "Docker Compose configuration", + Self::Custom => "Custom template", + } + } +} + +/// Template definition +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Template { + /// Template name + pub name: String, + + /// Template type + pub template_type: TemplateType, + + /// Description + pub description: String, + + /// Files to generate + pub files: Vec, + + /// Directories to create + pub directories: Vec, + + /// Variables that can be customized + pub variables: HashMap, +} + +/// A file in a template +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TemplateFile { + /// Target path (relative to project root) + pub path: String, + + /// File content + pub content: String, + + /// Whether this file should be overwritten if it exists + #[serde(default)] + pub overwrite: bool, +} + +/// A variable in a template +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TemplateVariable { + /// Variable description + pub description: String, + + /// Default value + pub default: String, + + /// Whether this variable is required + #[serde(default)] + pub required: bool, +} + +/// Template generator +pub struct TemplateGenerator { + templates: HashMap, +} + +impl TemplateGenerator { + /// Create a new template generator + pub fn new() -> Self { + let mut generator = Self { + templates: HashMap::new(), + }; + + generator.register_builtin_templates(); + generator + } + + /// Register built-in templates + fn register_builtin_templates(&mut self) { + // CUE Validation template + self.templates.insert( + "cue-validation".into(), + Template { + name: "cue-validation".into(), + template_type: TemplateType::CueValidation, + description: "Simple CUE schema validation".into(), + directories: vec!["schemas".into(), "config".into()], + files: vec![ + TemplateFile { + path: ".conflow.yaml".into(), + content: TEMPLATE_CUE_VALIDATION_PIPELINE.into(), + overwrite: false, + }, + TemplateFile { + path: "schemas/config.cue".into(), + content: TEMPLATE_CUE_SCHEMA.into(), + overwrite: false, + }, + TemplateFile { + path: "config/example.yaml".into(), + content: TEMPLATE_EXAMPLE_CONFIG.into(), + overwrite: false, + }, + ], + variables: HashMap::from([ + ( + "project_name".into(), + TemplateVariable { + description: "Project name".into(), + default: "my-project".into(), + required: true, + }, + ), + ]), + }, + ); + + // Nickel Generation template + self.templates.insert( + "nickel-generation".into(), + Template { + name: "nickel-generation".into(), + template_type: TemplateType::NickelGeneration, + description: "Programmatic config generation with Nickel".into(), + directories: vec!["nickel".into(), "dist".into()], + files: vec![ + TemplateFile { + path: ".conflow.yaml".into(), + content: TEMPLATE_NICKEL_PIPELINE.into(), + overwrite: false, + }, + TemplateFile { + path: "nickel/config.ncl".into(), + content: TEMPLATE_NICKEL_CONFIG.into(), + overwrite: false, + }, + ], + variables: HashMap::from([ + ( + "project_name".into(), + TemplateVariable { + description: "Project name".into(), + default: "my-project".into(), + required: true, + }, + ), + ]), + }, + ); + + // Full Pipeline template + self.templates.insert( + "full-pipeline".into(), + Template { + name: "full-pipeline".into(), + template_type: TemplateType::FullPipeline, + description: "Generate, validate, and export pipeline".into(), + directories: vec!["schemas".into(), "nickel".into(), "dist".into()], + files: vec![ + TemplateFile { + path: ".conflow.yaml".into(), + content: TEMPLATE_FULL_PIPELINE.into(), + overwrite: false, + }, + TemplateFile { + path: "schemas/config.cue".into(), + content: TEMPLATE_CUE_SCHEMA.into(), + overwrite: false, + }, + TemplateFile { + path: "nickel/config.ncl".into(), + content: TEMPLATE_NICKEL_CONFIG.into(), + overwrite: false, + }, + ], + variables: HashMap::new(), + }, + ); + + // Multi-environment template + self.templates.insert( + "multi-env".into(), + Template { + name: "multi-env".into(), + template_type: TemplateType::MultiEnv, + description: "Multi-environment configuration management".into(), + directories: vec![ + "environments".into(), + "schemas".into(), + "dist".into(), + ], + files: vec![ + TemplateFile { + path: ".conflow.yaml".into(), + content: TEMPLATE_MULTI_ENV_PIPELINE.into(), + overwrite: false, + }, + TemplateFile { + path: "environments/base.ncl".into(), + content: TEMPLATE_MULTI_ENV_BASE.into(), + overwrite: false, + }, + TemplateFile { + path: "environments/development.ncl".into(), + content: TEMPLATE_MULTI_ENV_DEV.into(), + overwrite: false, + }, + TemplateFile { + path: "environments/staging.ncl".into(), + content: TEMPLATE_MULTI_ENV_STAGING.into(), + overwrite: false, + }, + TemplateFile { + path: "environments/production.ncl".into(), + content: TEMPLATE_MULTI_ENV_PROD.into(), + overwrite: false, + }, + ], + variables: HashMap::new(), + }, + ); + + // Kubernetes template + self.templates.insert( + "kubernetes".into(), + Template { + name: "kubernetes".into(), + template_type: TemplateType::Kubernetes, + description: "Kubernetes manifest validation".into(), + directories: vec!["k8s".into(), "schemas".into()], + files: vec![ + TemplateFile { + path: ".conflow.yaml".into(), + content: TEMPLATE_K8S_PIPELINE.into(), + overwrite: false, + }, + TemplateFile { + path: "schemas/k8s.cue".into(), + content: TEMPLATE_K8S_SCHEMA.into(), + overwrite: false, + }, + TemplateFile { + path: "k8s/deployment.yaml".into(), + content: TEMPLATE_K8S_DEPLOYMENT.into(), + overwrite: false, + }, + ], + variables: HashMap::from([ + ( + "app_name".into(), + TemplateVariable { + description: "Application name".into(), + default: "my-app".into(), + required: true, + }, + ), + ]), + }, + ); + + // Terraform template + self.templates.insert( + "terraform".into(), + Template { + name: "terraform".into(), + template_type: TemplateType::Terraform, + description: "Terraform configuration validation".into(), + directories: vec!["terraform".into(), "schemas".into()], + files: vec![ + TemplateFile { + path: ".conflow.yaml".into(), + content: TEMPLATE_TERRAFORM_PIPELINE.into(), + overwrite: false, + }, + TemplateFile { + path: "schemas/tfvars.cue".into(), + content: TEMPLATE_TERRAFORM_SCHEMA.into(), + overwrite: false, + }, + ], + variables: HashMap::new(), + }, + ); + + // Helm template + self.templates.insert( + "helm".into(), + Template { + name: "helm".into(), + template_type: TemplateType::Helm, + description: "Helm chart configuration".into(), + directories: vec!["chart".into(), "schemas".into()], + files: vec![ + TemplateFile { + path: ".conflow.yaml".into(), + content: TEMPLATE_HELM_PIPELINE.into(), + overwrite: false, + }, + TemplateFile { + path: "schemas/values.cue".into(), + content: TEMPLATE_HELM_VALUES_SCHEMA.into(), + overwrite: false, + }, + TemplateFile { + path: "chart/values.yaml".into(), + content: TEMPLATE_HELM_VALUES.into(), + overwrite: false, + }, + ], + variables: HashMap::new(), + }, + ); + + // Docker Compose template + self.templates.insert( + "docker-compose".into(), + Template { + name: "docker-compose".into(), + template_type: TemplateType::DockerCompose, + description: "Docker Compose configuration".into(), + directories: vec!["schemas".into()], + files: vec![ + TemplateFile { + path: ".conflow.yaml".into(), + content: TEMPLATE_COMPOSE_PIPELINE.into(), + overwrite: false, + }, + TemplateFile { + path: "schemas/compose.cue".into(), + content: TEMPLATE_COMPOSE_SCHEMA.into(), + overwrite: false, + }, + ], + variables: HashMap::new(), + }, + ); + } + + /// Get a template by name + pub fn get(&self, name: &str) -> Option<&Template> { + self.templates.get(name) + } + + /// List all templates + pub fn list(&self) -> impl Iterator { + self.templates.values() + } + + /// Generate template files in target directory + pub fn generate( + &self, + template_name: &str, + target_dir: &Path, + variables: &HashMap, + ) -> Result { + let template = self.get(template_name).ok_or_else(|| ConflowError::ExecutionFailed { + message: format!("Template not found: {}", template_name), + help: Some(format!( + "Available templates: {}", + self.templates.keys().cloned().collect::>().join(", ") + )), + })?; + + let mut result = GenerationResult { + template_name: template_name.to_string(), + files_created: vec![], + files_skipped: vec![], + directories_created: vec![], + }; + + // Create directories + for dir in &template.directories { + let path = target_dir.join(dir); + if !path.exists() { + std::fs::create_dir_all(&path)?; + result.directories_created.push(dir.clone()); + } + } + + // Generate files + for file in &template.files { + let path = target_dir.join(&file.path); + + if path.exists() && !file.overwrite { + result.files_skipped.push(file.path.clone()); + continue; + } + + // Apply variable substitution + let content = self.substitute_variables(&file.content, variables); + + // Create parent directories if needed + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + std::fs::write(&path, content)?; + result.files_created.push(file.path.clone()); + } + + Ok(result) + } + + /// Substitute variables in content + fn substitute_variables(&self, content: &str, variables: &HashMap) -> String { + let mut result = content.to_string(); + + for (key, value) in variables { + let placeholder = format!("{{{{ {} }}}}", key); + result = result.replace(&placeholder, value); + + // Also support ${var} syntax + let alt_placeholder = format!("${{{}}}", key); + result = result.replace(&alt_placeholder, value); + } + + result + } + + /// Register a custom template + pub fn register(&mut self, template: Template) { + self.templates.insert(template.name.clone(), template); + } + + /// Load templates from a directory + pub fn load_from_dir(&mut self, dir: &Path) -> Result { + if !dir.exists() { + return Ok(0); + } + + let mut count = 0; + + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + + if path.extension().and_then(|s| s.to_str()) == Some("yaml") { + let content = std::fs::read_to_string(&path)?; + let template: Template = serde_yaml::from_str(&content).map_err(|e| { + ConflowError::Yaml { + message: e.to_string(), + } + })?; + + self.templates.insert(template.name.clone(), template); + count += 1; + } + } + + Ok(count) + } +} + +impl Default for TemplateGenerator { + fn default() -> Self { + Self::new() + } +} + +/// Result of template generation +#[derive(Debug, Clone)] +pub struct GenerationResult { + pub template_name: String, + pub files_created: Vec, + pub files_skipped: Vec, + pub directories_created: Vec, +} + +// Template content strings + +const TEMPLATE_CUE_VALIDATION_PIPELINE: &str = r#"# CUE Validation Pipeline +# Generated by conflow + +version: "1" +name: {{ project_name }} + +stages: + - name: validate + tool: + type: cue + command: vet + schemas: + - schemas/config.cue + input: + - "config/*.yaml" + - "config/*.json" + description: Validate configuration against CUE schema + +cache: + enabled: true + directory: .conflow-cache +"#; + +const TEMPLATE_CUE_SCHEMA: &str = r#"// Configuration Schema +package config + +#Config: { + // Application version + version: string & =~"^[0-9]+\\.[0-9]+\\.[0-9]+$" + + // Application name + name: string & !="" + + // Environment + environment?: "development" | "staging" | "production" + + // Logging configuration + logging?: { + level: "debug" | "info" | "warn" | "error" + format?: "json" | "text" + } + + // Feature flags + features?: [string]: bool +} +"#; + +const TEMPLATE_EXAMPLE_CONFIG: &str = r#"# Example configuration +version: "1.0.0" +name: my-application +environment: development +logging: + level: info + format: json +features: + new_ui: false + analytics: true +"#; + +const TEMPLATE_NICKEL_PIPELINE: &str = r#"# Nickel Generation Pipeline +# Generated by conflow + +version: "1" +name: {{ project_name }} + +stages: + - name: generate + tool: + type: nickel + command: export + format: yaml + input: nickel/config.ncl + output: dist/config.yaml + description: Generate configuration from Nickel + +cache: + enabled: true + directory: .conflow-cache +"#; + +const TEMPLATE_NICKEL_CONFIG: &str = r#"# Configuration in Nickel +{ + version = "1.0.0", + name = "{{ project_name }}", + + # Default settings + environment = "development", + debug = false, + log_level = "info", + + # Database configuration + database = { + host = "localhost", + port = 5432, + name = "app_db", + pool_size = 10, + }, + + # Feature flags + features = { + new_ui = false, + analytics = true, + }, +} +"#; + +const TEMPLATE_FULL_PIPELINE: &str = r#"# Full Pipeline: Generate, Validate, Export +# Generated by conflow + +version: "1" +name: config-pipeline + +stages: + # Generate config from Nickel + - name: generate + tool: + type: nickel + command: export + format: yaml + input: nickel/config.ncl + output: dist/config.yaml + description: Generate configuration from Nickel + + # Validate generated config + - name: validate + tool: + type: cue + command: vet + schemas: + - schemas/config.cue + input: dist/config.yaml + depends_on: + - generate + description: Validate configuration against schema + + # Export as JSON + - name: export-json + tool: + type: shell + command: "yq -o json dist/config.yaml > dist/config.json" + depends_on: + - validate + description: Export as JSON + +cache: + enabled: true + directory: .conflow-cache +"#; + +const TEMPLATE_MULTI_ENV_PIPELINE: &str = r#"# Multi-Environment Pipeline +# Generated by conflow + +version: "1" +name: multi-env-config + +stages: + - name: generate-dev + tool: + type: nickel + command: export + format: yaml + input: environments/development.ncl + output: dist/config.development.yaml + description: Generate development config + + - name: generate-staging + tool: + type: nickel + command: export + format: yaml + input: environments/staging.ncl + output: dist/config.staging.yaml + description: Generate staging config + + - name: generate-production + tool: + type: nickel + command: export + format: yaml + input: environments/production.ncl + output: dist/config.production.yaml + description: Generate production config + +cache: + enabled: true + directory: .conflow-cache +"#; + +const TEMPLATE_MULTI_ENV_BASE: &str = r#"# Base configuration +# Override these values per environment +{ + app_name = "my-application", + version = "1.0.0", + + log_level = "info", + debug = false, + + features = { + new_ui = false, + analytics = true, + }, + + database = { + host = "localhost", + port = 5432, + pool_size = 10, + }, +} +"#; + +const TEMPLATE_MULTI_ENV_DEV: &str = r#"# Development environment +let base = import "base.ncl" in + +base & { + environment = "development", + debug = true, + log_level = "debug", +} +"#; + +const TEMPLATE_MULTI_ENV_STAGING: &str = r#"# Staging environment +let base = import "base.ncl" in + +base & { + environment = "staging", + log_level = "info", + features.new_ui = true, + database.host = "staging-db.internal", +} +"#; + +const TEMPLATE_MULTI_ENV_PROD: &str = r#"# Production environment +let base = import "base.ncl" in + +base & { + environment = "production", + log_level = "warn", + database = base.database & { + host = "prod-db.internal", + pool_size = 50, + }, +} +"#; + +const TEMPLATE_K8S_PIPELINE: &str = r#"# Kubernetes Validation Pipeline +# Generated by conflow + +version: "1" +name: k8s-validation + +stages: + - name: validate + tool: + type: cue + command: vet + schemas: + - schemas/k8s.cue + input: + - "k8s/*.yaml" + description: Validate Kubernetes manifests + +cache: + enabled: true + directory: .conflow-cache +"#; + +const TEMPLATE_K8S_SCHEMA: &str = r#"// Kubernetes Schema +package k8s + +#Deployment: { + apiVersion: "apps/v1" + kind: "Deployment" + metadata: #Metadata + spec: #DeploymentSpec +} + +#Metadata: { + name: string & !="" + namespace?: string + labels?: [string]: string + annotations?: [string]: string +} + +#DeploymentSpec: { + replicas?: int & >=0 + selector: #Selector + template: #PodTemplateSpec +} + +#Selector: { + matchLabels: [string]: string +} + +#PodTemplateSpec: { + metadata: #Metadata + spec: #PodSpec +} + +#PodSpec: { + containers: [...#Container] +} + +#Container: { + name: string & !="" + image: string & !="" + ports?: [...#ContainerPort] + resources?: #Resources +} + +#ContainerPort: { + containerPort: int & >=1 & <=65535 + protocol?: "TCP" | "UDP" +} + +#Resources: { + limits?: { + cpu?: string + memory?: string + } + requests?: { + cpu?: string + memory?: string + } +} +"#; + +const TEMPLATE_K8S_DEPLOYMENT: &str = r#"apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ app_name }} + labels: + app: {{ app_name }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ app_name }} + template: + metadata: + name: {{ app_name }} + labels: + app: {{ app_name }} + spec: + containers: + - name: {{ app_name }} + image: {{ app_name }}:latest + ports: + - containerPort: 8080 + resources: + limits: + cpu: "100m" + memory: "128Mi" + requests: + cpu: "50m" + memory: "64Mi" +"#; + +const TEMPLATE_TERRAFORM_PIPELINE: &str = r#"# Terraform Validation Pipeline +# Generated by conflow + +version: "1" +name: terraform-validation + +stages: + - name: validate-vars + tool: + type: cue + command: vet + schemas: + - schemas/tfvars.cue + input: + - "terraform/*.tfvars.json" + description: Validate Terraform variables + +cache: + enabled: true + directory: .conflow-cache +"#; + +const TEMPLATE_TERRAFORM_SCHEMA: &str = r#"// Terraform Variables Schema +package terraform + +#Variables: { + // AWS region + region: string + + // Environment name + environment: "dev" | "staging" | "prod" + + // Instance type + instance_type?: string | *"t3.micro" + + // Enable monitoring + monitoring?: bool | *true + + // Tags + tags?: [string]: string +} +"#; + +const TEMPLATE_HELM_PIPELINE: &str = r#"# Helm Values Validation Pipeline +# Generated by conflow + +version: "1" +name: helm-validation + +stages: + - name: validate-values + tool: + type: cue + command: vet + schemas: + - schemas/values.cue + input: + - "chart/values.yaml" + description: Validate Helm values + +cache: + enabled: true + directory: .conflow-cache +"#; + +const TEMPLATE_HELM_VALUES_SCHEMA: &str = r#"// Helm Values Schema +package helm + +#Values: { + // Replica count + replicaCount: int & >=1 + + // Image configuration + image: { + repository: string + tag: string | *"latest" + pullPolicy: "Always" | "IfNotPresent" | "Never" + } + + // Service configuration + service: { + type: "ClusterIP" | "NodePort" | "LoadBalancer" + port: int & >=1 & <=65535 + } + + // Resource limits + resources?: { + limits?: { + cpu?: string + memory?: string + } + requests?: { + cpu?: string + memory?: string + } + } +} +"#; + +const TEMPLATE_HELM_VALUES: &str = r#"# Default values +replicaCount: 1 + +image: + repository: nginx + tag: latest + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 80 + +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi +"#; + +const TEMPLATE_COMPOSE_PIPELINE: &str = r#"# Docker Compose Validation Pipeline +# Generated by conflow + +version: "1" +name: compose-validation + +stages: + - name: validate + tool: + type: cue + command: vet + schemas: + - schemas/compose.cue + input: + - "docker-compose*.yaml" + description: Validate Docker Compose configuration + +cache: + enabled: true + directory: .conflow-cache +"#; + +const TEMPLATE_COMPOSE_SCHEMA: &str = r#"// Docker Compose Schema +package compose + +#Compose: { + version?: string + services: [string]: #Service + volumes?: [string]: #Volume + networks?: [string]: #Network +} + +#Service: { + image?: string + build?: string | #Build + ports?: [...string] + environment?: [...string] | [string]: string + volumes?: [...string] + depends_on?: [...string] + networks?: [...string] + restart?: "no" | "always" | "on-failure" | "unless-stopped" +} + +#Build: { + context: string + dockerfile?: string + args?: [string]: string +} + +#Volume: { + driver?: string + external?: bool +} + +#Network: { + driver?: string + external?: bool +} +"#; + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_list_templates() { + let generator = TemplateGenerator::new(); + let templates: Vec<_> = generator.list().collect(); + + assert!(templates.len() >= 6); + assert!(templates.iter().any(|t| t.name == "cue-validation")); + assert!(templates.iter().any(|t| t.name == "kubernetes")); + } + + #[test] + fn test_generate_template() { + let temp = TempDir::new().unwrap(); + let generator = TemplateGenerator::new(); + + let mut variables = HashMap::new(); + variables.insert("project_name".to_string(), "test-project".to_string()); + + let result = generator + .generate("cue-validation", temp.path(), &variables) + .unwrap(); + + assert!(!result.files_created.is_empty()); + assert!(temp.path().join(".conflow.yaml").exists()); + assert!(temp.path().join("schemas/config.cue").exists()); + + // Check variable substitution + let content = std::fs::read_to_string(temp.path().join(".conflow.yaml")).unwrap(); + assert!(content.contains("test-project")); + } + + #[test] + fn test_generate_kubernetes_template() { + let temp = TempDir::new().unwrap(); + let generator = TemplateGenerator::new(); + + let mut variables = HashMap::new(); + variables.insert("app_name".to_string(), "my-app".to_string()); + + let result = generator + .generate("kubernetes", temp.path(), &variables) + .unwrap(); + + assert!(!result.files_created.is_empty()); + assert!(temp.path().join("k8s/deployment.yaml").exists()); + + let content = std::fs::read_to_string(temp.path().join("k8s/deployment.yaml")).unwrap(); + assert!(content.contains("my-app")); + } +} diff --git a/src/utils/colors.rs b/src/utils/colors.rs index cf8836e..a8a9b3d 100644 --- a/src/utils/colors.rs +++ b/src/utils/colors.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Terminal color utilities //! //! Provides consistent color schemes across the CLI. diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 9ed6112..eb5773d 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Utility modules //! //! Common utilities for the conflow CLI. diff --git a/src/utils/spinner.rs b/src/utils/spinner.rs index bfd9589..c0c975c 100644 --- a/src/utils/spinner.rs +++ b/src/utils/spinner.rs @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2025 conflow contributors + //! Progress spinner utilities //! //! Provides progress indicators for long-running operations.