From a7e9043ae47fcb66f07d09bb4394a204ca723894 Mon Sep 17 00:00:00 2001 From: Debian Date: Thu, 28 Aug 2025 15:01:24 +0000 Subject: [PATCH] [skip ci] chore(sync): maj hooks 4NK_template --- .cursor/rules/05-template-governance.mdc | 17 + .cursor/rules/98-explain-complex-commands | 5 + .cursor/rules/99-lint-markdow.mdc | 9 + .gitea/workflows/LOCAL_OVERRIDES.yml | 14 + .gitea/workflows/ci.yml | 499 +++++++++++++++++++--- .gitea/workflows/ci.yml.bak | 87 ++++ .gitea/workflows/template-sync.yml | 39 ++ .markdownlint.json | 14 + TEMPLATE_VERSION | 1 + docs/templates/API.md | 8 + docs/templates/ARCHITECTURE.md | 8 + docs/templates/CONFIGURATION.md | 6 + docs/templates/INDEX.md | 12 + docs/templates/OPEN_SOURCE_CHECKLIST.md | 7 + docs/templates/README.md | 29 ++ docs/templates/RELEASE_PLAN.md | 7 + docs/templates/SECURITY_AUDIT.md | 7 + docs/templates/TESTING.md | 6 + docs/templates/USAGE.md | 7 + scripts/checks/version_alignment.sh | 0 scripts/deploy/setup.sh | 145 +++++++ scripts/dev/run_container.sh | 15 + scripts/dev/run_project_ci.sh | 14 + scripts/env/ensure_env.sh | 42 ++ scripts/local/run_agents_for_project.sh | 51 +++ scripts/release/guard.sh | 0 scripts/scripts/auto-ssh-push.sh | 25 +- scripts/scripts/init-ssh-env.sh | 0 scripts/scripts/setup-ssh-ci.sh | 0 scripts/security/audit.sh | 37 ++ scripts/utils/check_md024.ps1 | 47 ++ 31 files changed, 1103 insertions(+), 55 deletions(-) create mode 100644 .cursor/rules/05-template-governance.mdc create mode 100644 .cursor/rules/98-explain-complex-commands create mode 100644 .cursor/rules/99-lint-markdow.mdc create mode 100644 .gitea/workflows/LOCAL_OVERRIDES.yml create mode 100644 .gitea/workflows/ci.yml.bak create mode 100644 .gitea/workflows/template-sync.yml create mode 100644 .markdownlint.json create mode 100644 TEMPLATE_VERSION create mode 100644 docs/templates/API.md create mode 100644 docs/templates/ARCHITECTURE.md create mode 100644 docs/templates/CONFIGURATION.md create mode 100644 docs/templates/INDEX.md create mode 100644 docs/templates/OPEN_SOURCE_CHECKLIST.md create mode 100644 docs/templates/README.md create mode 100644 docs/templates/RELEASE_PLAN.md create mode 100644 docs/templates/SECURITY_AUDIT.md create mode 100644 docs/templates/TESTING.md create mode 100644 docs/templates/USAGE.md mode change 100644 => 100755 scripts/checks/version_alignment.sh create mode 100755 scripts/deploy/setup.sh create mode 100755 scripts/dev/run_container.sh create mode 100755 scripts/dev/run_project_ci.sh create mode 100755 scripts/env/ensure_env.sh create mode 100755 scripts/local/run_agents_for_project.sh mode change 100644 => 100755 scripts/release/guard.sh mode change 100644 => 100755 scripts/scripts/auto-ssh-push.sh mode change 100644 => 100755 scripts/scripts/init-ssh-env.sh mode change 100644 => 100755 scripts/scripts/setup-ssh-ci.sh create mode 100755 scripts/security/audit.sh create mode 100644 scripts/utils/check_md024.ps1 diff --git a/.cursor/rules/05-template-governance.mdc b/.cursor/rules/05-template-governance.mdc new file mode 100644 index 0000000..72a0a64 --- /dev/null +++ b/.cursor/rules/05-template-governance.mdc @@ -0,0 +1,17 @@ +--- +alwaysApply: true +--- + +# Gouvernance du template 4NK + +[portée] +Assurer que chaque projet adapte intelligemment le template et que les améliorations génériques reviennent dans `4NK_template`. + +[directives] +- Conserver `security-audit` et `release-guard` dans tous projets. +- Adapter la CI, les docs et `AGENTS.md` au contexte local. +- En cas d'amélioration générique : ouvrir une issue "Template Feedback", prototyper, valider CI, mettre à jour `CHANGELOG.md`/`TEMPLATE_VERSION`. + +[validation] +- Refuser un push/tag si l'adaptation a retiré les vérifications minimales (sécurité, tests, build, version/changelog/tag). +- Exiger une documentation claire dans `docs/TEMPLATE_ADAPTATION.md` et `docs/TEMPLATE_FEEDBACK.md`. \ No newline at end of file diff --git a/.cursor/rules/98-explain-complex-commands b/.cursor/rules/98-explain-complex-commands new file mode 100644 index 0000000..610e6ca --- /dev/null +++ b/.cursor/rules/98-explain-complex-commands @@ -0,0 +1,5 @@ +--- +alwaysApply: true +--- + +quand tu fais une commande ou un requète complexe, explique là avant de la lancer \ No newline at end of file diff --git a/.cursor/rules/99-lint-markdow.mdc b/.cursor/rules/99-lint-markdow.mdc new file mode 100644 index 0000000..6924c29 --- /dev/null +++ b/.cursor/rules/99-lint-markdow.mdc @@ -0,0 +1,9 @@ +--- +description: +globs: +alwaysApply: true +--- + +# Lint + +respecter strictement les règles de lint du markdown diff --git a/.gitea/workflows/LOCAL_OVERRIDES.yml b/.gitea/workflows/LOCAL_OVERRIDES.yml new file mode 100644 index 0000000..12c8c45 --- /dev/null +++ b/.gitea/workflows/LOCAL_OVERRIDES.yml @@ -0,0 +1,14 @@ +# LOCAL_OVERRIDES.yml — dérogations locales contrôlées +overrides: + - path: ".gitea/workflows/ci.yml" + reason: "spécificité d’environnement" + owner: "@maintainer_handle" + expires: "2025-12-31" + - path: "scripts/auto-ssh-push.sh" + reason: "flux particulier temporaire" + owner: "@maintainer_handle" + expires: "2025-10-01" +policy: + allow_only_listed_paths: true + require_expiry: true + audit_in_ci: true diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 09a4f2d..1787dce 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,78 +1,121 @@ -name: CI - ihm_client +name: CI - 4NK Node on: push: branches: [ main, develop ] + tags: + - 'v*' pull_request: branches: [ main, develop ] +env: + RUST_VERSION: '1.70' + DOCKER_COMPOSE_VERSION: '2.20.0' + CI_SKIP: 'true' + jobs: - test: - runs-on: ubuntu-latest + # Job de vérification du code + code-quality: + name: Code Quality + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' }} steps: - name: Checkout code uses: actions/checkout@v3 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 with: - submodules: recursive + toolchain: ${{ env.RUST_VERSION }} + override: true - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Cache Rust dependencies + uses: actions/cache@v3 with: - node-version: '20' - cache: 'npm' + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- - - name: Install dependencies - run: npm ci - - - name: Run linting - run: npm run lint - - - name: Run type checking - run: npm run type-check - - - name: Run tests - run: npm run test - - - name: Build application - run: npm run build - - - name: Install Playwright browsers - run: npm run e2e:install - - - name: Run E2E tests - run: npm run test:e2e - - - name: Test Docker build (artefacts) + - name: Run clippy run: | - docker build -t ihm-client:dist . - docker image rm ihm-client:dist + cd sdk_relay + cargo clippy --all-targets --all-features -- -D warnings - security: - runs-on: ubuntu-latest - needs: test + - name: Run rustfmt + run: | + cd sdk_relay + cargo fmt --all -- --check + + - name: Check documentation + run: | + cd sdk_relay + cargo doc --no-deps + + - name: Check for TODO/FIXME + run: | + if grep -r "TODO\|FIXME" . --exclude-dir=.git --exclude-dir=target; then + echo "Found TODO/FIXME comments. Please address them." + exit 1 + fi + + # Job de tests unitaires + unit-tests: + name: Unit Tests + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' }} steps: - name: Checkout code uses: actions/checkout@v3 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Setup Rust + uses: actions-rs/toolchain@v1 with: - node-version: '20' + toolchain: ${{ env.RUST_VERSION }} + override: true - - name: Install dependencies - run: npm ci + - name: Cache Rust dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- - - name: Run security audit - run: npm audit --audit-level=moderate + - name: Run unit tests + run: | + cd sdk_relay + cargo test --lib --bins - - name: Check for known vulnerabilities - run: npm audit --audit-level=high + - name: Run integration tests + run: | + cd sdk_relay + cargo test --tests - integration-test: - runs-on: ubuntu-latest - needs: test + # Job de tests d'intégration + integration-tests: + name: Integration Tests + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' }} + + services: + docker: + image: docker:24.0.5 + options: >- + --health-cmd "docker info" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 2375:2375 steps: - name: Checkout code @@ -81,7 +124,363 @@ jobs: - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Build Docker artefacts + - name: Build Docker images run: | - docker build -t ihm-client:dist . - docker image rm ihm-client:dist + docker build -t 4nk-node-bitcoin ./bitcoin + docker build -t 4nk-node-blindbit ./blindbit + docker build -t 4nk-node-sdk-relay -f ./sdk_relay/Dockerfile .. + + - name: Run integration tests + run: | + # Tests de connectivité de base + ./tests/run_connectivity_tests.sh || true + + # Tests d'intégration + ./tests/run_integration_tests.sh || true + + - name: Upload test results + uses: actions/upload-artifact@v3 + if: always() + with: + name: test-results + path: | + tests/logs/ + tests/reports/ + retention-days: 7 + + # Job de tests de sécurité + security-tests: + name: Security Tests + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + override: true + + - name: Run cargo audit + run: | + cd sdk_relay + cargo audit --deny warnings + + - name: Check for secrets + run: | + # Vérifier les secrets potentiels + if grep -r "password\|secret\|key\|token" . --exclude-dir=.git --exclude-dir=target --exclude=*.md; then + echo "Potential secrets found. Please review." + exit 1 + fi + + - name: Check file permissions + run: | + # Vérifier les permissions sensibles + find . -type f -perm /0111 -name "*.conf" -o -name "*.key" -o -name "*.pem" | while read file; do + if [[ $(stat -c %a "$file") != "600" ]]; then + echo "Warning: $file has insecure permissions" + fi + done + + # Job de build et test Docker + docker-build: + name: Docker Build & Test + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' }} + + services: + docker: + image: docker:24.0.5 + options: >- + --health-cmd "docker info" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 2375:2375 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and test Bitcoin Core + run: | + docker build -t 4nk-node-bitcoin:test ./bitcoin + docker run --rm 4nk-node-bitcoin:test bitcoin-cli --version + + - name: Build and test Blindbit + run: | + docker build -t 4nk-node-blindbit:test ./blindbit + docker run --rm 4nk-node-blindbit:test --version || true + + - name: Build and test SDK Relay + run: | + docker build -t 4nk-node-sdk-relay:test -f ./sdk_relay/Dockerfile .. + docker run --rm 4nk-node-sdk-relay:test --version || true + + - name: Test Docker Compose + run: | + docker-compose config + docker-compose build --no-cache + + # Job de tests de documentation + documentation-tests: + name: Documentation Tests + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Check markdown links + run: | + # Vérification basique des liens markdown + find . -name "*.md" -exec grep -l "\[.*\](" {} \; | while read file; do + echo "Checking links in $file" + done + + markdownlint: + name: Markdown Lint + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Run markdownlint + run: | + npm --version || (curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs) + npx -y markdownlint-cli@0.42.0 "**/*.md" --ignore "archive/**" + + - name: Check documentation structure + run: | + # Vérifier la présence des fichiers de documentation essentiels + required_files=( + "README.md" + "LICENSE" + "CONTRIBUTING.md" + "CHANGELOG.md" + "CODE_OF_CONDUCT.md" + "SECURITY.md" + ) + + for file in "${required_files[@]}"; do + if [[ ! -f "$file" ]]; then + echo "Missing required documentation file: $file" + exit 1 + fi + done + + bash-required: + name: Bash Requirement + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Verify bash availability + run: | + if ! command -v bash >/dev/null 2>&1; then + echo "bash is required for agents and scripts"; exit 1; + fi + - name: Verify agents runner exists + run: | + if [ ! -f scripts/agents/run.sh ]; then + echo "scripts/agents/run.sh is missing"; exit 1; + fi + + agents-smoke: + name: Agents Smoke (no AI) + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Ensure agents scripts executable + run: | + chmod +x scripts/agents/*.sh || true + - name: Run agents without AI + env: + OPENAI_API_KEY: "" + run: | + scripts/agents/run.sh + - name: Upload agents reports + uses: actions/upload-artifact@v3 + with: + name: agents-reports + path: tests/reports/agents + + openia-agents: + name: Agents with OpenIA + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' && secrets.OPENAI_API_KEY != '' }} + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_MODEL: ${{ vars.OPENAI_MODEL }} + OPENAI_API_BASE: ${{ vars.OPENAI_API_BASE }} + OPENAI_TEMPERATURE: ${{ vars.OPENAI_TEMPERATURE }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Ensure agents scripts executable + run: | + chmod +x scripts/agents/*.sh || true + - name: Run agents with AI + run: | + scripts/agents/run.sh + - name: Upload agents reports + uses: actions/upload-artifact@v3 + with: + name: agents-reports-ai + path: tests/reports/agents + + deployment-checks: + name: Deployment Checks + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Validate deployment documentation + run: | + if [ ! -f docs/DEPLOYMENT.md ]; then + echo "Missing docs/DEPLOYMENT.md"; exit 1; fi + if [ ! -f docs/SSH_UPDATE.md ]; then + echo "Missing docs/SSH_UPDATE.md"; exit 1; fi + - name: Ensure tests directories exist + run: | + mkdir -p tests/logs tests/reports || true + echo "OK" + + security-audit: + name: Security Audit + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Ensure scripts executable + run: | + chmod +x scripts/security/audit.sh || true + - name: Run template security audit + run: | + if [ -f scripts/security/audit.sh ]; then + ./scripts/security/audit.sh + else + echo "No security audit script (ok)" + fi + + # Job de release guard (cohérence release) + release-guard: + name: Release Guard + runs-on: [self-hosted, linux] + needs: [code-quality, unit-tests, documentation-tests, markdownlint, security-audit, deployment-checks, bash-required] + if: ${{ env.CI_SKIP != 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Ensure guard scripts are executable + run: | + chmod +x scripts/release/guard.sh || true + chmod +x scripts/checks/version_alignment.sh || true + + - name: Version alignment check + run: | + if [ -f scripts/checks/version_alignment.sh ]; then + ./scripts/checks/version_alignment.sh + else + echo "No version alignment script (ok)" + fi + + - name: Release guard (CI verify) + env: + RELEASE_TYPE: ci-verify + run: | + if [ -f scripts/release/guard.sh ]; then + ./scripts/release/guard.sh + else + echo "No guard script (ok)" + fi + + release-create: + name: Create Release (Gitea API) + runs-on: ubuntu-latest + needs: [release-guard] + if: ${{ env.CI_SKIP != 'true' && startsWith(github.ref, 'refs/tags/') }} + env: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} + BASE_URL: ${{ vars.BASE_URL }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Validate token and publish release + run: | + set -e + if [ -z "${RELEASE_TOKEN}" ]; then + echo "RELEASE_TOKEN secret is missing" >&2; exit 1; fi + if [ -z "${BASE_URL}" ]; then + BASE_URL="https://git.4nkweb.com"; fi + TAG="${GITHUB_REF##*/}" + REPO="${GITHUB_REPOSITORY}" + OWNER="${REPO%%/*}" + NAME="${REPO##*/}" + echo "Publishing release ${TAG} to ${BASE_URL}/${OWNER}/${NAME}" + curl -sSf -X POST \ + -H "Authorization: token ${RELEASE_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"draft\":false,\"prerelease\":false}" \ + "${BASE_URL}/api/v1/repos/${OWNER}/${NAME}/releases" >/dev/null + echo "Release created" + + # Job de tests de performance + performance-tests: + name: Performance Tests + runs-on: [self-hosted, linux] + if: ${{ env.CI_SKIP != 'true' }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + override: true + + - name: Run performance tests + run: | + cd sdk_relay + cargo test --release --test performance_tests || true + + - name: Check memory usage + run: | + # Tests de base de consommation mémoire + echo "Performance tests completed" + + # Job de notification + notify: + name: Notify + runs-on: [self-hosted, linux] + needs: [code-quality, unit-tests, integration-tests, security-tests, docker-build, documentation-tests] + if: ${{ env.CI_SKIP != 'true' && always() }} + + steps: + - name: Notify success + if: needs.code-quality.result == 'success' && needs.unit-tests.result == 'success' && needs.integration-tests.result == 'success' && needs.security-tests.result == 'success' && needs.docker-build.result == 'success' && needs.documentation-tests.result == 'success' + run: | + echo "✅ All tests passed successfully!" + + - name: Notify failure + if: needs.code-quality.result == 'failure' || needs.unit-tests.result == 'failure' || needs.integration-tests.result == 'failure' || needs.security-tests.result == 'failure' || needs.docker-build.result == 'failure' || needs.documentation-tests.result == 'failure' + run: | + echo "❌ Some tests failed!" + exit 1 diff --git a/.gitea/workflows/ci.yml.bak b/.gitea/workflows/ci.yml.bak new file mode 100644 index 0000000..09a4f2d --- /dev/null +++ b/.gitea/workflows/ci.yml.bak @@ -0,0 +1,87 @@ +name: CI - ihm_client + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linting + run: npm run lint + + - name: Run type checking + run: npm run type-check + + - name: Run tests + run: npm run test + + - name: Build application + run: npm run build + + - name: Install Playwright browsers + run: npm run e2e:install + + - name: Run E2E tests + run: npm run test:e2e + + - name: Test Docker build (artefacts) + run: | + docker build -t ihm-client:dist . + docker image rm ihm-client:dist + + security: + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Run security audit + run: npm audit --audit-level=moderate + + - name: Check for known vulnerabilities + run: npm audit --audit-level=high + + integration-test: + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker artefacts + run: | + docker build -t ihm-client:dist . + docker image rm ihm-client:dist diff --git a/.gitea/workflows/template-sync.yml b/.gitea/workflows/template-sync.yml new file mode 100644 index 0000000..8bdfd05 --- /dev/null +++ b/.gitea/workflows/template-sync.yml @@ -0,0 +1,39 @@ +# .gitea/workflows/template-sync.yml — synchronisation et contrôles d’intégrité +name: 4NK Template Sync +on: + schedule: # planification régulière + - cron: "0 4 * * 1" # exécution hebdomadaire (UTC) + workflow_dispatch: {} # déclenchement manuel + +jobs: + check-and-sync: + runs-on: [self-hosted, linux] + steps: + - name: Lire TEMPLATE_VERSION et .4nk-sync.yml + # Doit charger ref courant, source_repo et périmètre paths + + - name: Récupérer la version publiée du template/4NK_rules + # Doit comparer TEMPLATE_VERSION avec ref amont + + - name: Créer branche de synchronisation si divergence + # Doit créer chore/template-sync- et préparer un commit + + - name: Synchroniser les chemins autoritatifs + # Doit mettre à jour .cursor/**, .gitea/**, AGENTS.md, scripts/**, docs/SSH_UPDATE.md + + - name: Contrôles post-sync (bloquants) + # 1) Vérifier présence et exécutable des scripts/*.sh + # 2) Vérifier mise à jour CHANGELOG.md et docs/INDEX.md + # 3) Vérifier docs/SSH_UPDATE.md si scripts/** a changé + # 4) Vérifier absence de secrets en clair dans scripts/** + # 5) Vérifier manifest_checksum si publié + + - name: Tests, lint, sécurité statique + # Doit exiger un état vert + + - name: Ouvrir PR de synchronisation + # Titre: "[template-sync] chore: aligner .cursor/.gitea/AGENTS.md/scripts" + # Doit inclure résumé des fichiers modifiés et la version appliquée + + - name: Mettre à jour TEMPLATE_VERSION (dans PR) + # Doit remplacer la valeur par la ref appliquée diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..56e5c35 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,14 @@ +{ + "MD013": { + "line_length": 200, + "code_blocks": false, + "tables": false, + "headings": false + }, + "MD007": { + "indent": 2 + }, + "MD024": { + "siblings_only": true + } +} diff --git a/TEMPLATE_VERSION b/TEMPLATE_VERSION new file mode 100644 index 0000000..264fc29 --- /dev/null +++ b/TEMPLATE_VERSION @@ -0,0 +1 @@ +v2025.08.5 \ No newline at end of file diff --git a/docs/templates/API.md b/docs/templates/API.md new file mode 100644 index 0000000..431560f --- /dev/null +++ b/docs/templates/API.md @@ -0,0 +1,8 @@ +# Référence API — Template + +- Vue d’ensemble +- Authentification/permissions +- Endpoints par domaine (schémas, invariants) +- Codes d’erreur +- Limites et quotas +- Sécurité et conformité diff --git a/docs/templates/ARCHITECTURE.md b/docs/templates/ARCHITECTURE.md new file mode 100644 index 0000000..42b78b2 --- /dev/null +++ b/docs/templates/ARCHITECTURE.md @@ -0,0 +1,8 @@ +# Architecture — Template + +- Contexte et objectifs +- Découpage en couches (UI, services, données) +- Flux principaux +- Observabilité +- CI/CD +- Contraintes et SLA diff --git a/docs/templates/CONFIGURATION.md b/docs/templates/CONFIGURATION.md new file mode 100644 index 0000000..3506069 --- /dev/null +++ b/docs/templates/CONFIGURATION.md @@ -0,0 +1,6 @@ +# Configuration — Template + +- Variables d’environnement (nom, type, défaut, portée) +- Fichiers de configuration (format, validation) +- Réseau et sécurité (ports, TLS, auth) +- Observabilité (logs, métriques, traces) diff --git a/docs/templates/INDEX.md b/docs/templates/INDEX.md new file mode 100644 index 0000000..be566c0 --- /dev/null +++ b/docs/templates/INDEX.md @@ -0,0 +1,12 @@ +# Index — Templates de documentation (pour projets dérivés) + +Utilisez ces squelettes pour démarrer la documentation de votre projet. + +- API.md — squelette de référence API +- ARCHITECTURE.md — squelette d’architecture +- CONFIGURATION.md — squelette de configuration +- USAGE.md — squelette d’usage +- TESTING.md — squelette de stratégie de tests +- SECURITY_AUDIT.md — squelette d’audit sécurité +- RELEASE_PLAN.md — squelette de plan de release +- OPEN_SOURCE_CHECKLIST.md — squelette de checklist open source diff --git a/docs/templates/OPEN_SOURCE_CHECKLIST.md b/docs/templates/OPEN_SOURCE_CHECKLIST.md new file mode 100644 index 0000000..8406e38 --- /dev/null +++ b/docs/templates/OPEN_SOURCE_CHECKLIST.md @@ -0,0 +1,7 @@ +# Checklist open source — Template + +- Gouvernance: LICENSE, CONTRIBUTING, CODE_OF_CONDUCT +- CI/CD: workflows, tests, security-audit, release-guard +- Documentation: README, INDEX, guides essentiels +- Sécurité: secrets, permissions, audit +- Publication: tag, changelog, release notes diff --git a/docs/templates/README.md b/docs/templates/README.md new file mode 100644 index 0000000..fe4d4bb --- /dev/null +++ b/docs/templates/README.md @@ -0,0 +1,29 @@ +# README — Template de projet + +## Présentation + +Décrivez brièvement l’objectif du projet, son périmètre et ses utilisateurs cibles. + +## Démarrage rapide + +- Prérequis (langages/outils) +- Étapes d’installation +- Commandes de démarrage + +## Documentation + +- Index: `docs/INDEX.md` +- Architecture: `docs/ARCHITECTURE.md` +- Configuration: `docs/CONFIGURATION.md` +- Tests: `docs/TESTING.md` +- Sécurité: `docs/SECURITY_AUDIT.md` +- Déploiement: `docs/DEPLOYMENT.md` + +## Contribution + +- GUIDE: `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` +- Processus de PR et revues + +## Licence + +- Indiquez la licence choisie (MIT/Apache-2.0/GPL) diff --git a/docs/templates/RELEASE_PLAN.md b/docs/templates/RELEASE_PLAN.md new file mode 100644 index 0000000..ab912bf --- /dev/null +++ b/docs/templates/RELEASE_PLAN.md @@ -0,0 +1,7 @@ +# Plan de release — Template + +- Vue d’ensemble, objectifs, date cible +- Préparation (docs/CI/tests/sécurité) +- Communication (annonces, canaux) +- Lancement (checklist, tagging) +- Post‑lancement (support, retours) diff --git a/docs/templates/SECURITY_AUDIT.md b/docs/templates/SECURITY_AUDIT.md new file mode 100644 index 0000000..3876d6a --- /dev/null +++ b/docs/templates/SECURITY_AUDIT.md @@ -0,0 +1,7 @@ +# Audit de sécurité — Template + +- Menaces et surfaces d’attaque +- Contrôles préventifs et détectifs +- Gestion des secrets +- Politique de dépendances +- Vérifications CI (security-audit) diff --git a/docs/templates/TESTING.md b/docs/templates/TESTING.md new file mode 100644 index 0000000..81a4b51 --- /dev/null +++ b/docs/templates/TESTING.md @@ -0,0 +1,6 @@ +# Tests — Template + +- Pyramide: unit, integration, connectivity, external, performance +- Structure des répertoires +- Exécution et rapports +- Intégration CI diff --git a/docs/templates/USAGE.md b/docs/templates/USAGE.md new file mode 100644 index 0000000..8cad2e9 --- /dev/null +++ b/docs/templates/USAGE.md @@ -0,0 +1,7 @@ +# Usage — Template + +- Démarrage quotidien +- Opérations courantes +- Tests (référence vers TESTING.md) +- Sécurité (référence vers SECURITY_AUDIT.md) +- Déploiement (référence vers DEPLOYMENT.md) diff --git a/scripts/checks/version_alignment.sh b/scripts/checks/version_alignment.sh old mode 100644 new mode 100755 diff --git a/scripts/deploy/setup.sh b/scripts/deploy/setup.sh new file mode 100755 index 0000000..8908ea9 --- /dev/null +++ b/scripts/deploy/setup.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +set -euo pipefail + +ENV_DIR="${HOME}/.4nk_template" +ENV_FILE="${ENV_DIR}/.env" +TEMPLATE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +TEMPLATE_IN_REPO="${TEMPLATE_ROOT}/scripts/env/.env.template" + +usage() { + cat < [--dest DIR] [--force] + +Actions: + 1) Provisionne ~/.4nk_template/.env (si absent) + 2) Clone le dépôt cible si le dossier n'existe pas + 3) Copie la structure normative 4NK_template dans le projet cible: + - .gitea/** (workflows, templates issues/PR) + - AGENTS.md + - .cursor/rules/** (si présent) + - scripts/agents/**, scripts/env/ensure_env.sh, scripts/deploy/setup.sh + - docs/templates/** et docs/INDEX.md (table des matières) + 4) Ne remplace pas les fichiers existants sauf si --force + +Exemples: + $0 https://git.example.com/org/projet.git + $0 git@host:org/projet.git --dest ~/work --force +USAGE +} + +GIT_URL="${1:-}" +DEST_PARENT="$(pwd)" +FORCE_COPY=0 +shift || true +while [[ $# -gt 0 ]]; do + case "$1" in + --dest) + DEST_PARENT="${2:-}"; shift 2 ;; + --force) + FORCE_COPY=1; shift ;; + -h|--help) + usage; exit 0 ;; + *) + echo "Option inconnue: $1" >&2; usage; exit 2 ;; + esac +done + +if [[ -z "${GIT_URL}" ]]; then + usage; exit 2 +fi + +mkdir -p "${ENV_DIR}" +chmod 700 "${ENV_DIR}" || true + +if [[ ! -f "${ENV_FILE}" ]]; then + if [[ -f "${TEMPLATE_IN_REPO}" ]]; then + cp "${TEMPLATE_IN_REPO}" "${ENV_FILE}" + else + cat >"${ENV_FILE}" <<'EOF' +# Fichier d'exemple d'environnement pour 4NK_template +# Copiez ce fichier vers ~/.4nk_template/.env puis complétez les valeurs. +# Ne committez jamais de fichier contenant des secrets. + +# OpenAI (agents IA) +OPENAI_API_KEY= +OPENAI_MODEL= +OPENAI_API_BASE=https://api.openai.com/v1 +OPENAI_TEMPERATURE=0.2 + +# Gitea (release via API) +BASE_URL=https://git.4nkweb.com +RELEASE_TOKEN= +EOF + fi + chmod 600 "${ENV_FILE}" || true + echo "Fichier créé: ${ENV_FILE}. Complétez les valeurs requises (ex: OPENAI_API_KEY, OPENAI_MODEL, RELEASE_TOKEN)." >&2 +fi + +# 2) Clonage du dépôt si nécessaire +repo_name="$(basename -s .git "${GIT_URL}")" +target_dir="${DEST_PARENT%/}/${repo_name}" +if [[ ! -d "${target_dir}" ]]; then + echo "Clonage: ${GIT_URL} → ${target_dir}" >&2 + git clone --depth 1 "${GIT_URL}" "${target_dir}" +else + echo "Dossier existant, pas de clone: ${target_dir}" >&2 +fi + +copy_item() { + local src="$1" dst="$2" + if [[ ! -e "$src" ]]; then return 0; fi + if [[ -d "$src" ]]; then + mkdir -p "$dst" + if (( FORCE_COPY )); then + cp -a "$src/." "$dst/" + else + (cd "$src" && find . -type f -print0) | while IFS= read -r -d '' f; do + if [[ ! -e "$dst/$f" ]]; then + mkdir -p "$(dirname "$dst/$f")" + cp -a "$src/$f" "$dst/$f" + fi + done + fi + else + if [[ -e "$dst" && $FORCE_COPY -eq 0 ]]; then return 0; fi + mkdir -p "$(dirname "$dst")" && cp -a "$src" "$dst" + fi +} + +# 3) Copie de la structure normative +copy_item "${TEMPLATE_ROOT}/.gitea" "${target_dir}/.gitea" +copy_item "${TEMPLATE_ROOT}/AGENTS.md" "${target_dir}/AGENTS.md" +copy_item "${TEMPLATE_ROOT}/.cursor" "${target_dir}/.cursor" +copy_item "${TEMPLATE_ROOT}/.cursorignore" "${target_dir}/.cursorignore" +copy_item "${TEMPLATE_ROOT}/.gitignore" "${target_dir}/.gitignore" +copy_item "${TEMPLATE_ROOT}/.markdownlint.json" "${target_dir}/.markdownlint.json" +copy_item "${TEMPLATE_ROOT}/LICENSE" "${target_dir}/LICENSE" +copy_item "${TEMPLATE_ROOT}/CONTRIBUTING.md" "${target_dir}/CONTRIBUTING.md" +copy_item "${TEMPLATE_ROOT}/CODE_OF_CONDUCT.md" "${target_dir}/CODE_OF_CONDUCT.md" +copy_item "${TEMPLATE_ROOT}/SECURITY.md" "${target_dir}/SECURITY.md" +copy_item "${TEMPLATE_ROOT}/TEMPLATE_VERSION" "${target_dir}/TEMPLATE_VERSION" +copy_item "${TEMPLATE_ROOT}/security" "${target_dir}/security" +copy_item "${TEMPLATE_ROOT}/scripts" "${target_dir}/scripts" +copy_item "${TEMPLATE_ROOT}/docs/templates" "${target_dir}/docs/templates" + +# Génération docs/INDEX.md dans le projet cible (si absent ou --force) +INDEX_DST="${target_dir}/docs/INDEX.md" +if [[ ! -f "${INDEX_DST}" || $FORCE_COPY -eq 1 ]]; then + mkdir -p "$(dirname "${INDEX_DST}")" + cat >"${INDEX_DST}" <<'IDX' +# Documentation du projet + +Cette table des matières oriente vers: +- Documentation spécifique au projet: `docs/project/` +- Modèles génériques à adapter: `docs/templates/` + +## Sommaire +- À personnaliser: `docs/project/README.md`, `docs/project/INDEX.md`, `docs/project/ARCHITECTURE.md`, `docs/project/USAGE.md`, etc. + +## Modèles génériques +- Voir: `docs/templates/` +IDX +fi + +echo "Template 4NK appliqué à: ${target_dir}" >&2 +exit 0 diff --git a/scripts/dev/run_container.sh b/scripts/dev/run_container.sh new file mode 100755 index 0000000..2d543cb --- /dev/null +++ b/scripts/dev/run_container.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_NAME="4nk-template-dev:debian" +DOCKERFILE="docker/Dockerfile.debian" + +echo "[build] ${IMAGE_NAME}" +docker build -t "${IMAGE_NAME}" -f "${DOCKERFILE}" . + +echo "[run] launching container and executing agents" +docker run --rm -it \ + -v "${PWD}:/work" -w /work \ + "${IMAGE_NAME}" \ + "scripts/agents/run.sh; ls -la tests/reports/agents || true" + diff --git a/scripts/dev/run_project_ci.sh b/scripts/dev/run_project_ci.sh new file mode 100755 index 0000000..d92d96b --- /dev/null +++ b/scripts/dev/run_project_ci.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build et lance le conteneur unifié (runner+agents) sur ce projet +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$ROOT_DIR" + +# Build image +docker compose -f docker-compose.ci.yml build + +# Exécuter agents par défaut +RUNNER_MODE="${RUNNER_MODE:-agents}" BASE_URL="${BASE_URL:-}" REGISTRATION_TOKEN="${REGISTRATION_TOKEN:-}" \ + docker compose -f docker-compose.ci.yml up --remove-orphans --abort-on-container-exit diff --git a/scripts/env/ensure_env.sh b/scripts/env/ensure_env.sh new file mode 100755 index 0000000..6435819 --- /dev/null +++ b/scripts/env/ensure_env.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +TEMPLATE_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.env.template" +ENV_DIR="${HOME}/.4nk_template" +ENV_FILE="${ENV_DIR}/.env" + +mkdir -p "${ENV_DIR}" +chmod 700 "${ENV_DIR}" || true + +if [[ ! -f "${ENV_FILE}" ]]; then + if [[ -f "${TEMPLATE_FILE}" ]]; then + cp "${TEMPLATE_FILE}" "${ENV_FILE}" + chmod 600 "${ENV_FILE}" || true + echo "Fichier d'environnement créé: ${ENV_FILE}" >&2 + echo "Veuillez renseigner les variables requises (OPENAI_API_KEY, OPENAI_MODEL, etc.)." >&2 + exit 3 + else + echo "Modèle d'environnement introuvable: ${TEMPLATE_FILE}" >&2 + exit 2 + fi +fi + +# Charger pour validation +set -a +. "${ENV_FILE}" +set +a + +MISSING=() +for var in OPENAI_API_KEY OPENAI_MODEL; do + if [[ -z "${!var:-}" ]]; then + MISSING+=("$var") + fi +done + +if (( ${#MISSING[@]} > 0 )); then + echo "Variables manquantes dans ${ENV_FILE}: ${MISSING[*]}" >&2 + exit 4 +fi + +echo "Environnement valide: ${ENV_FILE}" >&2 diff --git a/scripts/local/run_agents_for_project.sh b/scripts/local/run_agents_for_project.sh new file mode 100755 index 0000000..5070846 --- /dev/null +++ b/scripts/local/run_agents_for_project.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Script pour lancer les agents de 4NK_template sur un projet externe +# Usage: ./run_agents_for_project.sh [project_path] [output_dir] + +PROJECT_PATH="${1:-.}" +OUTPUT_DIR="${2:-tests/reports/agents}" +TEMPLATE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +MODULE_LAST_IMAGE_FILE="$(cd "$TEMPLATE_DIR/.." && pwd)/modules/4NK_template/.last_image" + +if [[ ! -d "$PROJECT_PATH" ]]; then + echo "Erreur: Le projet '$PROJECT_PATH' n'existe pas" >&2 + exit 1 +fi + +mkdir -p "$PROJECT_PATH/$OUTPUT_DIR" + +echo "=== Lancement des agents 4NK_template sur: $PROJECT_PATH ===" + +if ! command -v docker >/dev/null 2>&1; then + echo "Docker requis pour exécuter les agents via conteneur." >&2 + exit 2 +fi + +# Si une image du module existe, l'utiliser en priorité +if [[ -f "$MODULE_LAST_IMAGE_FILE" ]]; then + IMAGE_NAME="$(cat "$MODULE_LAST_IMAGE_FILE" | tr -d '\r\n')" + echo "Utilisation de l'image du module: $IMAGE_NAME" + # Préparer montage du fichier d'env si présent + ENV_MOUNT="" + if [[ -f "$HOME/.4nk_template/.env" ]]; then + ENV_MOUNT="-v $HOME/.4nk_template/.env:/root/.4nk_template/.env:ro" + fi + # Lancer le conteneur en utilisant l'ENTRYPOINT qui configure safe.directory + docker run --rm \ + -e RUNNER_MODE=agents \ + -e TARGET_DIR=/work \ + -e OUTPUT_DIR=/work/$OUTPUT_DIR \ + -v "$(realpath "$PROJECT_PATH"):/work" \ + $ENV_MOUNT \ + "$IMAGE_NAME" || true +else + echo "Aucune image de module détectée, fallback docker compose dans 4NK_template" + cd "$TEMPLATE_DIR" + docker compose -f docker-compose.ci.yml build + RUNNER_MODE="agents" TARGET_DIR="/work" OUTPUT_DIR="/work/$OUTPUT_DIR" \ + docker compose -f docker-compose.ci.yml run --rm project-ci || true +fi + +echo "=== Agents terminés → $PROJECT_PATH/$OUTPUT_DIR ===" diff --git a/scripts/release/guard.sh b/scripts/release/guard.sh old mode 100644 new mode 100755 diff --git a/scripts/scripts/auto-ssh-push.sh b/scripts/scripts/auto-ssh-push.sh old mode 100644 new mode 100755 index 653b59c..0064500 --- a/scripts/scripts/auto-ssh-push.sh +++ b/scripts/scripts/auto-ssh-push.sh @@ -26,8 +26,23 @@ fi echo "✅ Authentification SSH réussie" # Fonction pour push automatique +get_current_branch() { + # Détecte la branche courante, compatible anciennes versions de git + local br + br="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)" + if [ -z "$br" ] || [ "$br" = "HEAD" ]; then + br="$(git symbolic-ref --short -q HEAD 2>/dev/null || true)" + fi + if [ -z "$br" ]; then + # dernier recours: parser la sortie de "git branch" + br="$(git branch 2>/dev/null | sed -n 's/^* //p' | head -n1)" + fi + echo "$br" +} + auto_push() { - local branch=${1:-$(git branch --show-current)} + local branch + branch=${1:-$(get_current_branch)} local commit_message=${2:-"Auto-commit $(date '+%Y-%m-%d %H:%M:%S')"} echo "🚀 Push automatique sur la branche: $branch" @@ -35,7 +50,7 @@ auto_push() { # Ajouter tous les changements git add . - # Ne pas commiter si rien à commiter + # Ne pas commiter si rien à commite if [[ -z "$(git diff --cached --name-only)" ]]; then echo "ℹ️ Aucun changement indexé. Skip commit/push." return 0 @@ -54,7 +69,7 @@ auto_push() { # Fonction pour push avec message personnalisé push_with_message() { local message="$1" - local branch=${2:-$(git branch --show-current)} + local branch=${2:-$(get_current_branch)} echo "💬 Push avec message: $message" auto_push "$branch" "$message" @@ -62,7 +77,7 @@ push_with_message() { # Fonction pour push rapide (sans message) quick_push() { - local branch=${1:-$(git branch --show-current)} + local branch=${1:-$(get_current_branch)} auto_push "$branch" } @@ -77,7 +92,7 @@ push_branch() { # Fonction pour push et merge vers main push_and_merge() { - local source_branch=${1:-$(git branch --show-current)} + local source_branch=${1:-$(get_current_branch)} local target_branch=${2:-main} echo "🔄 Push et merge $source_branch -> $target_branch" diff --git a/scripts/scripts/init-ssh-env.sh b/scripts/scripts/init-ssh-env.sh old mode 100644 new mode 100755 diff --git a/scripts/scripts/setup-ssh-ci.sh b/scripts/scripts/setup-ssh-ci.sh old mode 100644 new mode 100755 diff --git a/scripts/security/audit.sh b/scripts/security/audit.sh new file mode 100755 index 0000000..c705469 --- /dev/null +++ b/scripts/security/audit.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "[security-audit] démarrage" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)" +cd "$ROOT_DIR" + +rc=0 + +# 1) Audit Rust (si Cargo.toml présent et cargo disponible) +if command -v cargo >/dev/null 2>&1 && [ -f Cargo.toml ] || find . -maxdepth 2 -name Cargo.toml | grep -q . ; then + echo "[security-audit] cargo audit" + if ! cargo audit --deny warnings; then rc=1; fi || true +else + echo "[security-audit] pas de projet Rust (ok)" +fi + +# 2) Audit npm (si package.json présent) +if [ -f package.json ]; then + echo "[security-audit] npm audit --audit-level=moderate" + if ! npm audit --audit-level=moderate; then rc=1; fi || true +else + echo "[security-audit] pas de package.json (ok)" +fi + +# 3) Recherche de secrets grossiers +echo "[security-audit] scan secrets" +if grep -RIE "(?i)(api[_-]?key|secret|password|private[_-]?key)" --exclude-dir .git --exclude-dir node_modules --exclude-dir target --exclude "*.md" . >/dev/null 2>&1; then + echo "[security-audit] secrets potentiels détectés"; rc=1 +else + echo "[security-audit] aucun secret évident" +fi + +echo "[security-audit] terminé rc=$rc" +exit $rc + + diff --git a/scripts/utils/check_md024.ps1 b/scripts/utils/check_md024.ps1 new file mode 100644 index 0000000..000c6d1 --- /dev/null +++ b/scripts/utils/check_md024.ps1 @@ -0,0 +1,47 @@ +Param( + [string]$Root = "." +) + +$ErrorActionPreference = "Stop" + +$files = Get-ChildItem -Path $Root -Recurse -Filter *.md | Where-Object { $_.FullName -notmatch '\\archive\\' } +$had = $false +foreach ($f in $files) { + try { + $lines = Get-Content -LiteralPath $f.FullName -Encoding UTF8 -ErrorAction Stop + } catch { + Write-Warning ("Impossible de lire: {0} — {1}" -f $f.FullName, $_.Exception.Message) + continue + } + $map = @{} + $firstMap = @{} + $dups = @{} + for ($i = 0; $i -lt $lines.Count; $i++) { + $line = $lines[$i] + if ($line -match '^\s{0,3}#{1,6}\s+(.*)$') { + $t = $Matches[1].Trim() + $norm = ([regex]::Replace($t, '\s+', ' ')).ToLowerInvariant() + if ($map.ContainsKey($norm)) { + if (-not $dups.ContainsKey($norm)) { + $dups[$norm] = New-Object System.Collections.ArrayList + $firstMap[$norm] = $map[$norm] + } + [void]$dups[$norm].Add($i + 1) + } else { + $map[$norm] = $i + 1 + } + } + } + if ($dups.Keys.Count -gt 0) { + $had = $true + Write-Output "=== $($f.FullName) ===" + foreach ($k in $dups.Keys) { + $first = $firstMap[$k] + $others = ($dups[$k] -join ', ') + Write-Output ("Heading: '{0}' first@{1} duplicates@[{2}]" -f $k, $first, $others) + } + } +} +if (-not $had) { + Write-Output "No duplicate headings detected." +}