From d5660ba8ca2213ac05ef8e5bb1156867457d5a33 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 19:29:18 +0200 Subject: [PATCH] chore: apply project standards templates --- .editorconfig | 16 ++ .gitattributes | 17 ++ .githooks/README.md | 21 ++ .githooks/pre-commit | 40 ++++ .gitignore | 11 + AGENTS.md | 88 ++++++++ docs/DEFINITION_OF_DONE.md | 52 +++++ scripts/reconcile-project-standards.sh | 274 +++++++++++++++++++++++++ 8 files changed, 519 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .githooks/README.md create mode 100644 .githooks/pre-commit create mode 100644 AGENTS.md create mode 100644 docs/DEFINITION_OF_DONE.md create mode 100755 scripts/reconcile-project-standards.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fb18251 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.go] +indent_style = tab +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..826bbe8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +* text=auto + +# Ensure LF for shell scripts and common source/docs files +*.sh text eol=lf +*.bash text eol=lf +*.zsh text eol=lf +*.go text eol=lf +*.mod text eol=lf +*.sum text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +Makefile text eol=lf + +# Keep native Windows script formats +*.bat text eol=crlf +*.cmd text eol=crlf diff --git a/.githooks/README.md b/.githooks/README.md new file mode 100644 index 0000000..ad95056 --- /dev/null +++ b/.githooks/README.md @@ -0,0 +1,21 @@ +# Git Hooks + +This repository standard uses a project-local hooks directory: + +- `.githooks/pre-commit` + +Activate it once per repository: + +```sh +git config core.hooksPath .githooks +``` + +The pre-commit hook validates for staged `.sh` files: + +- executable bit in Git index (`100755`) +- LF line endings (no CRLF) + +The pre-commit hook also validates staged `.md` files with `markdownlint`: + +- no `markdownlint` errors or problems +- requires `markdownlint` CLI in PATH (for example via `npm install --global markdownlint-cli`) diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..df1a085 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,40 @@ +#!/usr/bin/env sh +set -eu + +failed=0 +cr=$(printf '\r') + +staged_shell_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.sh$' || true) +staged_markdown_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.md$' || true) + +for file in $staged_shell_files; do + mode=$(git ls-files --stage -- "$file" | awk '{print $1}') + if [ "$mode" != "100755" ]; then + echo "ERROR: $file is not executable in Git index. Run: git add --chmod=+x $file" >&2 + failed=1 + fi + + if git show ":$file" | grep -q "$cr"; then + echo "ERROR: $file contains CRLF in staged content. Use LF line endings." >&2 + failed=1 + fi +done + +if [ -n "$staged_markdown_files" ]; then + if ! command -v markdownlint >/dev/null 2>&1; then + echo "ERROR: markdownlint is required to validate staged Markdown files (.md)." >&2 + echo "Install with npm: npm install --global markdownlint-cli" >&2 + failed=1 + else + # Validate the staged markdown files currently present in the working tree. + # This keeps the hook simple and fast for standard project usage. + if ! markdownlint $staged_markdown_files; then + failed=1 + fi + fi +fi + +if [ "$failed" -ne 0 ]; then + echo "Pre-commit check failed." >&2 + exit 1 +fi diff --git a/.gitignore b/.gitignore index f4d432a..8ffa3ab 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,14 @@ # Dependency directories (remove the comment below to include it) # vendor/ +.build/ +bin/ +dist/ +tmp/ +coverage/ +coverage.out +*.coverprofile +.DS_Store +Thumbs.db +.vscode/ +.idea/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..32fd1c9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,88 @@ +# 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. +5. For Go projects, use `github.com/smartystreets/goconvey` as the standard test library. + +## Build Artifacts and Reports +1. Builder logs and generated reports must be created under `.build/`. +2. The `.build/` directory must be excluded from version control via `.gitignore`. + +## Git and Script Standards +1. Shell scripts (`*.sh`) must use LF line endings. +2. Shell scripts committed to the repository must be executable in Git index (mode `100755`). +3. When adding a new shell script, set execute permissions before commit: `git add --chmod=+x path/to/script.sh`. + +## Git Bash Execution Defaults +1. Repository maintenance scripts are executed with Git Bash shell on Windows. +2. Default repository root is `~/git`. +3. The repository root can be overridden via `GO_GIT_ROOT`. +4. The standards repository defaults to `$ROOT_PATH/project-standards`. +5. The standards repository path can be overridden via `GO_PROJECT_STANDARDS`. + +## 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. +- For Go projects, tests use `github.com/smartystreets/goconvey`. +- Automated test coverage is at least 80%. + +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. SonarQube status +- No SonarQube errors are present. + +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. +- [ ] Go tests use `github.com/smartystreets/goconvey`. +- [ ] Automated test coverage is at least 80%. +- [ ] Functionality is documented. +- [ ] Documentation is in English. +- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`). +- [ ] No SonarQube errors are present. +- [ ] No critical regressions found. diff --git a/docs/DEFINITION_OF_DONE.md b/docs/DEFINITION_OF_DONE.md new file mode 100644 index 0000000..a47d72f --- /dev/null +++ b/docs/DEFINITION_OF_DONE.md @@ -0,0 +1,52 @@ +# 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. +- For Go projects, tests use `github.com/smartystreets/goconvey`. +- Automated test coverage is at least 80%. + +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. SonarQube status +- No SonarQube errors are present. + +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. +- [ ] Go tests use `github.com/smartystreets/goconvey`. +- [ ] Automated test coverage is at least 80%. +- [ ] Functionality is documented. +- [ ] Documentation is in English. +- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`). +- [ ] No SonarQube errors are present. +- [ ] No critical regressions found. diff --git a/scripts/reconcile-project-standards.sh b/scripts/reconcile-project-standards.sh new file mode 100755 index 0000000..aa8cf9c --- /dev/null +++ b/scripts/reconcile-project-standards.sh @@ -0,0 +1,274 @@ +#!/usr/bin/env sh +set -eu + +ROOT_PATH="${GO_GIT_ROOT:-$HOME/git}" +STANDARDS_REPO="" +STANDARDS_REPO_SET=0 +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 + +Environment: + GO_GIT_ROOT Overrides default root path (default: ~/git) + GO_PROJECT_STANDARDS Overrides standards repository path + +Default resolution: + ROOT_PATH defaults to GO_GIT_ROOT or ~/git. + STANDARDS_REPO defaults to GO_PROJECT_STANDARDS or $ROOT_PATH/project-standards. +EOF +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --root) + ROOT_PATH=$2 + shift 2 + ;; + --standards-repo) + STANDARDS_REPO=$2 + STANDARDS_REPO_SET=1 + 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 [ "$STANDARDS_REPO_SET" -ne 1 ]; then + if [ -n "${GO_PROJECT_STANDARDS:-}" ]; then + STANDARDS_REPO="$GO_PROJECT_STANDARDS" + else + STANDARDS_REPO="$ROOT_PATH/project-standards" + fi +fi + +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" +GITIGNORE_TEMPLATE="$STANDARDS_REPO/templates/.gitignore.base" +GITATTRIBUTES_TEMPLATE="$STANDARDS_REPO/templates/.gitattributes.base" +EDITORCONFIG_TEMPLATE="$STANDARDS_REPO/templates/.editorconfig.base" +PRECOMMIT_TEMPLATE="$STANDARDS_REPO/templates/pre-commit.base.sh" +HOOKS_README_TEMPLATE="$STANDARDS_REPO/templates/.githooks.README.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 + +if [ ! -f "$GITIGNORE_TEMPLATE" ]; then + echo "gitignore template not found: $GITIGNORE_TEMPLATE" >&2 + exit 1 +fi + +if [ ! -f "$GITATTRIBUTES_TEMPLATE" ]; then + echo "gitattributes template not found: $GITATTRIBUTES_TEMPLATE" >&2 + exit 1 +fi + +if [ ! -f "$EDITORCONFIG_TEMPLATE" ]; then + echo "editorconfig template not found: $EDITORCONFIG_TEMPLATE" >&2 + exit 1 +fi + +if [ ! -f "$PRECOMMIT_TEMPLATE" ]; then + echo "pre-commit hook template not found: $PRECOMMIT_TEMPLATE" >&2 + exit 1 +fi + +if [ ! -f "$HOOKS_README_TEMPLATE" ]; then + echo "hooks readme template not found: $HOOKS_README_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" +} + +ensure_gitignore_entries_from_template() { + template_path=$1 + gitignore_path=$2 + + missing=0 + + if [ ! -f "$gitignore_path" ]; then + if [ "$CHECK_ONLY" -eq 1 ]; then + printf "%s" "drift" + return 0 + fi + + : > "$gitignore_path" + fi + + while IFS= read -r entry; do + entry=$(printf '%s' "$entry" | tr -d '\r') + [ -n "$entry" ] || continue + case "$entry" in + \#*) + continue + ;; + esac + + if tr -d '\r' < "$gitignore_path" | grep -Fqx "$entry"; then + continue + fi + + missing=1 + if [ "$CHECK_ONLY" -ne 1 ]; then + printf '%s\n' "$entry" >> "$gitignore_path" + fi + done < "$template_path" + + if [ "$missing" -eq 0 ]; then + printf "%s" "ok" + return 0 + fi + + if [ "$CHECK_ONLY" -eq 1 ]; then + printf "%s" "drift" + else + printf "%s" "updated" + fi +} + +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" + gitattributes_target="$repo/.gitattributes" + editorconfig_target="$repo/.editorconfig" + precommit_target="$repo/.githooks/pre-commit" + hooks_readme_target="$repo/.githooks/README.md" + gitignore_target="$repo/.gitignore" + + agents_state=$(ensure_file "$AGENTS_TEMPLATE" "$agents_target") + dod_state=$(ensure_file "$DOD_TEMPLATE" "$dod_target") + gitattributes_state=$(ensure_file "$GITATTRIBUTES_TEMPLATE" "$gitattributes_target") + editorconfig_state=$(ensure_file "$EDITORCONFIG_TEMPLATE" "$editorconfig_target") + precommit_state=$(ensure_file "$PRECOMMIT_TEMPLATE" "$precommit_target") + hooks_readme_state=$(ensure_file "$HOOKS_README_TEMPLATE" "$hooks_readme_target") + gitignore_state=$(ensure_gitignore_entries_from_template "$GITIGNORE_TEMPLATE" "$gitignore_target") + + if [ "$agents_state" = "updated" ] || [ "$dod_state" = "updated" ] || [ "$gitattributes_state" = "updated" ] || [ "$editorconfig_state" = "updated" ] || [ "$precommit_state" = "updated" ] || [ "$hooks_readme_state" = "updated" ] || [ "$gitignore_state" = "updated" ]; then + updated=$((updated + 1)) + echo "UPDATED: $repo" + continue + fi + + if [ "$agents_state" = "drift" ] || [ "$dod_state" = "drift" ] || [ "$gitattributes_state" = "drift" ] || [ "$editorconfig_state" = "drift" ] || [ "$precommit_state" = "drift" ] || [ "$hooks_readme_state" = "drift" ] || [ "$gitignore_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