diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1aaa2e8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,63 @@ +# AGENTS.md + +## Purpose +This file defines the default working agreement for AI coding agents and contributors in this repository. + +## Documentation Language +Project language is English. All documentation, issues, pull requests, commit messages, and code comments should be written in English. +All newly created documentation in this project must be written in English. +All project documentation files must be stored under `docs/`, except `README.md` and `AGENTS.md`. + +## Working Principles +1. Keep changes minimal and focused on the requested task. +2. Preserve existing public APIs unless a breaking change is explicitly requested. +3. Prefer clear, maintainable code over clever shortcuts. +4. Do not modify unrelated files. +5. Never add secrets, credentials, or tokens to the repository. + +## Testing Expectations +1. Add or update tests for behavior changes. +2. Keep tests deterministic and fast. +3. Prefer table-driven tests where they improve readability. +4. Run relevant tests locally before finishing changes. + +## Definition of Done (DoD) + +### Purpose +The Definition of Done defines the minimum quality bar for every completed change in this repository. + +### Mandatory Criteria +1. Tests +- Every code change is covered by tests where applicable. +- New functionality includes new tests. +- Bug fixes include at least one regression test. + +1. Functional documentation +- Implemented functionality is documented. +- Public API-relevant changes are reflected in README and/or docs. + +1. Documentation standards +- Documentation is written in English. +- Documentation files are placed under `docs/`. +- Exceptions: `README.md` and `AGENTS.md` remain at repository root. + +### Technical Completion Criteria +1. Build and test status +- The project builds successfully. +- Relevant test commands run successfully. + +1. No unresolved critical issues +- No new blocking errors are introduced. +- Known non-blocking warnings are acceptable only if unrelated to the change or documented. + +1. Documentation structure +- Links to moved or newly added docs are valid. +- Documentation structure remains consistent with project rules. + +### Review Checklist (Quick) +- [ ] Change is implemented and meets acceptance criteria. +- [ ] Tests were added/updated and pass. +- [ ] Functionality is documented. +- [ ] Documentation is in English. +- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`). +- [ ] No critical regressions found. diff --git a/docs/DEFINITION_OF_DONE.md b/docs/DEFINITION_OF_DONE.md new file mode 100644 index 0000000..c254e82 --- /dev/null +++ b/docs/DEFINITION_OF_DONE.md @@ -0,0 +1,44 @@ +# Definition of Done (DoD) + +## Purpose + +This Definition of Done defines the minimum quality bar for every completed change in this repository. + +## Mandatory Criteria + +1. Tests +- Every code change is covered by tests where applicable. +- New functionality includes new tests. +- Bug fixes include at least one regression test. + +1. Functional documentation +- Implemented functionality is documented. +- Public API-relevant changes are reflected in README and/or docs. + +1. Documentation standards +- Documentation is written in English. +- Documentation files are placed under `docs/`. +- Exceptions: `README.md` and `AGENTS.md` remain at repository root. + +## Technical Completion Criteria + +1. Build and test status +- The project builds successfully. +- Relevant test commands run successfully. + +1. No unresolved critical issues +- No new blocking errors are introduced. +- Known non-blocking warnings are acceptable only if unrelated to the change or documented. + +1. Documentation links and structure +- Links to moved or newly added docs are valid. +- Documentation structure remains consistent with project rules. + +## Review Checklist (Quick) + +- [ ] Change is implemented and meets acceptance criteria. +- [ ] Tests were added/updated and pass. +- [ ] Functionality is documented. +- [ ] Documentation is in English. +- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`). +- [ ] No critical regressions found. diff --git a/scripts/Reconcile-ProjectStandards.ps1 b/scripts/Reconcile-ProjectStandards.ps1 new file mode 100644 index 0000000..63ee8f9 --- /dev/null +++ b/scripts/Reconcile-ProjectStandards.ps1 @@ -0,0 +1,121 @@ +[CmdletBinding()] +param( + [string]$RootPath = "C:\Users\stefan\git", + [string]$StandardsRepoPath = "C:\Users\stefan\git\project-standards", + [string[]]$Exclude = @('project-standards'), + [switch]$CheckOnly, + [switch]$Watch, + [int]$IntervalSeconds = 60 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$agentsTemplate = Join-Path $StandardsRepoPath 'templates/AGENTS.base.md' +$dodTemplate = Join-Path $StandardsRepoPath 'templates/DEFINITION_OF_DONE.base.md' + +if (-not (Test-Path -Path $agentsTemplate -PathType Leaf)) { + throw "AGENTS template not found: $agentsTemplate" +} + +if (-not (Test-Path -Path $dodTemplate -PathType Leaf)) { + throw "DoD template not found: $dodTemplate" +} + +if ($IntervalSeconds -lt 5) { + throw 'IntervalSeconds must be >= 5.' +} + +function Get-ContentHashOrMissing { + param([string]$Path) + + if (-not (Test-Path -Path $Path -PathType Leaf)) { + return '__MISSING__' + } + + return (Get-FileHash -Path $Path -Algorithm SHA256).Hash +} + +function Ensure-FileFromTemplate { + param( + [string]$Template, + [string]$Target, + [switch]$OnlyCheck + ) + + $templateHash = Get-ContentHashOrMissing -Path $Template + $targetHash = Get-ContentHashOrMissing -Path $Target + + if ($templateHash -eq $targetHash) { + return 'ok' + } + + if ($OnlyCheck) { + return 'drift' + } + + $parent = Split-Path -Parent $Target + if (-not (Test-Path -Path $parent -PathType Container)) { + New-Item -ItemType Directory -Path $parent | Out-Null + } + + Copy-Item -Path $Template -Destination $Target -Force + return 'updated' +} + +function Invoke-ReconcileOnce { + param([switch]$OnlyCheck) + + $resolvedRoot = (Resolve-Path -Path $RootPath -ErrorAction Stop).Path + $repos = Get-ChildItem -Path $resolvedRoot -Directory | Where-Object { + $Exclude -notcontains $_.Name + } + + $summary = [ordered]@{ + scanned = 0 + updated = 0 + drift = 0 + } + + foreach ($repo in $repos) { + $repoPath = $repo.FullName + $agentsTarget = Join-Path $repoPath 'AGENTS.md' + $dodTarget = Join-Path (Join-Path $repoPath 'docs') 'DEFINITION_OF_DONE.md' + + $summary.scanned++ + + $agentsState = Ensure-FileFromTemplate -Template $agentsTemplate -Target $agentsTarget -OnlyCheck:$OnlyCheck + $dodState = Ensure-FileFromTemplate -Template $dodTemplate -Target $dodTarget -OnlyCheck:$OnlyCheck + + if ($agentsState -eq 'updated' -or $dodState -eq 'updated') { + $summary.updated++ + Write-Host "UPDATED: $repoPath" + continue + } + + if ($agentsState -eq 'drift' -or $dodState -eq 'drift') { + $summary.drift++ + Write-Host "DRIFT: $repoPath" + continue + } + + Write-Host "OK: $repoPath" + } + + Write-Host "Summary -> scanned=$($summary.scanned), updated=$($summary.updated), drift=$($summary.drift)" + return $summary +} + +if ($Watch) { + while ($true) { + $now = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + Write-Host "[$now] Reconciling project standards..." + [void](Invoke-ReconcileOnce -OnlyCheck:$CheckOnly) + Start-Sleep -Seconds $IntervalSeconds + } +} + +$result = Invoke-ReconcileOnce -OnlyCheck:$CheckOnly +if ($CheckOnly -and $result.drift -gt 0) { + exit 1 +} diff --git a/scripts/reconcile-project-standards.sh b/scripts/reconcile-project-standards.sh new file mode 100644 index 0000000..0d280e6 --- /dev/null +++ b/scripts/reconcile-project-standards.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env sh +set -eu + +ROOT_PATH="/c/Users/stefan/git" +STANDARDS_REPO="/c/Users/stefan/git/project-standards" +EXCLUDE_NAME="project-standards" +CHECK_ONLY=0 +WATCH=0 +INTERVAL=60 + +usage() { + cat <<'EOF' +Usage: + reconcile-project-standards.sh [options] + +Options: + --root Root folder containing repositories + --standards-repo Path to project-standards repository + --exclude First-level directory name to skip (default: project-standards) + --check-only Check drift only, do not update files + --watch Continuously scan and reconcile + --interval Watch interval in seconds (default: 60) + -h, --help Show this help +EOF +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --root) + ROOT_PATH=$2 + shift 2 + ;; + --standards-repo) + STANDARDS_REPO=$2 + shift 2 + ;; + --exclude) + EXCLUDE_NAME=$2 + shift 2 + ;; + --check-only) + CHECK_ONLY=1 + shift + ;; + --watch) + WATCH=1 + shift + ;; + --interval) + INTERVAL=$2 + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [ "$INTERVAL" -lt 5 ]; then + echo "interval must be >= 5" >&2 + exit 1 +fi + +AGENTS_TEMPLATE="$STANDARDS_REPO/templates/AGENTS.base.md" +DOD_TEMPLATE="$STANDARDS_REPO/templates/DEFINITION_OF_DONE.base.md" + +if [ ! -f "$AGENTS_TEMPLATE" ]; then + echo "AGENTS template not found: $AGENTS_TEMPLATE" >&2 + exit 1 +fi + +if [ ! -f "$DOD_TEMPLATE" ]; then + echo "DoD template not found: $DOD_TEMPLATE" >&2 + exit 1 +fi + +hash_or_missing() { + path=$1 + if [ ! -f "$path" ]; then + printf "%s" "__MISSING__" + return 0 + fi + + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$path" | awk '{print $1}' + else + shasum -a 256 "$path" | awk '{print $1}' + fi +} + +ensure_file() { + template=$1 + target=$2 + + template_hash=$(hash_or_missing "$template") + target_hash=$(hash_or_missing "$target") + + if [ "$template_hash" = "$target_hash" ]; then + printf "%s" "ok" + return 0 + fi + + if [ "$CHECK_ONLY" -eq 1 ]; then + printf "%s" "drift" + return 0 + fi + + mkdir -p "$(dirname "$target")" + cp "$template" "$target" + printf "%s" "updated" +} + +run_once() { + scanned=0 + updated=0 + drift=0 + + for repo in "$ROOT_PATH"/*; do + [ -d "$repo" ] || continue + + name=$(basename "$repo") + [ "$name" = "$EXCLUDE_NAME" ] && continue + + scanned=$((scanned + 1)) + + agents_target="$repo/AGENTS.md" + dod_target="$repo/docs/DEFINITION_OF_DONE.md" + + agents_state=$(ensure_file "$AGENTS_TEMPLATE" "$agents_target") + dod_state=$(ensure_file "$DOD_TEMPLATE" "$dod_target") + + if [ "$agents_state" = "updated" ] || [ "$dod_state" = "updated" ]; then + updated=$((updated + 1)) + echo "UPDATED: $repo" + continue + fi + + if [ "$agents_state" = "drift" ] || [ "$dod_state" = "drift" ]; then + drift=$((drift + 1)) + echo "DRIFT: $repo" + continue + fi + + echo "OK: $repo" + done + + echo "Summary -> scanned=$scanned, updated=$updated, drift=$drift" + + if [ "$CHECK_ONLY" -eq 1 ] && [ "$drift" -gt 0 ]; then + return 1 + fi + + return 0 +} + +if [ "$WATCH" -eq 1 ]; then + while :; do + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Reconciling project standards..." + run_once || true + sleep "$INTERVAL" + done +else + run_once +fi diff --git a/utils_test.go b/utils_test.go index 5003eea..05960f3 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,7 +1,6 @@ package util import ( - "fmt" "os" "os/user" "path/filepath" @@ -67,7 +66,7 @@ func TestIsSuperUser(t *testing.T) { func TestGlobalConfigurationDirectoryWindows(t *testing.T) { if runtime.GOOS != "windows" { - t.Skip(fmt.Sprintf("Skipping on OS %s", runtime.GOOS)) + t.Skipf("Skipping on OS %s", runtime.GOOS) } appFolder := GetGlobalConfigurationDirectory("myapp") assert.Equal(t, filepath.Join(os.Getenv("APPDATA"), "myapp"), appFolder) @@ -75,7 +74,7 @@ func TestGlobalConfigurationDirectoryWindows(t *testing.T) { func TestGlobalConfigurationDirectoryLinux(t *testing.T) { if runtime.GOOS != "linux" { - t.Skip(fmt.Sprintf("Skipping on OS %s", runtime.GOOS)) + t.Skipf("Skipping on OS %s", runtime.GOOS) } appFolder := GetGlobalConfigurationDirectory("myapp") assert.Equal(t, "/etc/myapp", appFolder) @@ -83,8 +82,17 @@ func TestGlobalConfigurationDirectoryLinux(t *testing.T) { func TestGlobalConfigurationDirectoryMacOS(t *testing.T) { if runtime.GOOS != "darwin" { - t.Skip(fmt.Sprintf("Skipping on OS %s", runtime.GOOS)) + t.Skipf("Skipping on OS %s", runtime.GOOS) } appFolder := GetGlobalConfigurationDirectory("myapp") assert.Equal(t, "/etc/myapp", appFolder) } + +func TestMkDir(t *testing.T) { + path := "c:/tmp/bla/blub" + if !FileExists(path) { + err := os.MkdirAll(path, os.ModeDir) + assert.Nil(t, err) + assert.True(t, FileExists(path)) + } +}