diff --git a/.gitignore b/.gitignore
index 6531108..a6c7c7d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,10 @@ docs/contributing.md
docs/home.md
docs/vendors.md
docs/examples/**/*.md
+
+# Development setup exclusions β keeping the repository clean and minimal.
+# Except for `.venv/`, these files are typically not included in
+# `.gitignore`, but are listed here to maintain simplicity and clarity.
+.venv/
+.pre-commit-config.yaml
+.markdownlint.json
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8545d98..cb712a2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,3 +7,44 @@ image: "https://raw.githubusercontent.com/bitol-io/artwork/main/horizontal/color
# Contributing to Open Data Contract Standard
Thank you for your interest in contributing to Open Data Contract Standard (ODCS). Please refer to the [TSC contributing guidelines](https://github.com/bitol-io/tsc/blob/main/CONTRIBUTING.md).
+
+## Create a Local Development Environment
+
+To set up a local development environment, use the predefined setup scripts:
+
+For Windows Development Environment run:
+
+```shell
+. \scr\script\dev_setup.ps1
+```
+
+
+
+For Unix Development Environment run:
+
+```bash
+source src/script/dev_setup.sh
+```
+
+
+
+Each of these scripts will:
+
+* Check the virtual environment:
+ * Create and activate it if missing
+ * Activate it if not already active
+* Check `pip` status:
+ * Verify the current version
+ * Compare against the latest version
+ * Upgrade if necessary
+* Check `pre_commit` status:
+ * Install if missing
+* Check `pre-commit` version:
+ * Verify the current version
+ * Compare against the latest version
+ * Upgrade if necessary
+* Check `pre-commit` hooks:
+ * Create `.pre-commit-config.yaml` if missing
+ * Update and install all hooks
+* Check `.markdownlint.json`:
+ * Create the file if it doesnβt exist
diff --git a/src/script/dev_setup.ps1 b/src/script/dev_setup.ps1
new file mode 100644
index 0000000..73857c4
--- /dev/null
+++ b/src/script/dev_setup.ps1
@@ -0,0 +1,267 @@
+#!/usr/bin/env pwsh
+<#
+.SYNOPSIS
+ Developer environment setup (PowerShell 7+)
+.DESCRIPTION
+ Creates/activates venv (where possible), checks/updates pip and pre-commit,
+ and Asserts config files (.pre-commit-config.yaml, .markdownlint.json, .commitlintrc.json).
+#>
+
+# -----------------------------
+# π¨ Color map & helpers
+# -----------------------------
+$Colors = @{
+ 'Info' = 'Cyan'
+ 'Task' = 'Yellow'
+ 'Pass' = 'Green'
+ 'Warn' = 'Magenta'
+ 'Fail' = 'Red'
+}
+
+function Write-Info([string]$m){Write-Host "π‘ [INFO] $m" -ForegroundColor $Colors.Info }
+function Write-Task([string]$m){ Write-Host "β‘ [TASK] $m" -ForegroundColor $Colors.Task }
+function Write-Pass([string]$m){ Write-Host "β
[PASS] $m" -ForegroundColor $Colors.Pass }
+function Write-Warn([string]$m){ Write-Host "β οΈ [WARN] $m" -ForegroundColor $Colors.Warn }
+function Write-Fail([string]$m){ Write-Host "β [FAIL] $m" -ForegroundColor $Colors.Fail }
+
+# -----------------------------
+# π± Virtual environment
+# -----------------------------
+$VenvDir = ".venv"
+
+function Assert-VirtualEnv {
+ Write-Info "Checking virtual environment status ..."
+
+ # PowerShell activation path (Windows & pwsh)
+ $ActivatePs1 = Join-Path $VenvDir "Scripts/Activate.ps1"
+
+ if (Test-Path $ActivatePs1) {
+ if ($env:VIRTUAL_ENV) {
+ Write-Pass "Virtual environment found and active."
+ } else {
+ Write-Info "Virtual environment found but not active."
+ Write-Task "activating ..."
+ try {
+ & $ActivatePs1
+ } catch {
+ Write-Warn "Activation script ran but returned an error: $($_.Exception.Message)"
+ }
+ }
+ } else {
+ Write-Warn "No virtual environment found."
+ Write-Task "creating and activating ..."
+ try {
+ & python -m venv $VenvDir 2>&1 | Out-Null
+ & $ActivatePs1
+ } catch {
+ Write-Fail "Failed to create virtual environment: $($_.Exception.Message)"
+ }
+ }
+}
+
+# -----------------------------
+# π§ Pip version helpers (robust regex)
+# -----------------------------
+function Get-PipVersion {
+ try {
+ $out = & pip --version 2>&1
+ } catch {
+ return $null
+ }
+ $match = [regex]::Match($out, 'pip\s+(\d+(?:\.\d+)+)')
+ if ($match.Success) { return $match.Groups[1].Value }
+ return $null
+}
+
+function Get-LatestPipVersion {
+ try {
+ $dry = & python -m pip install --upgrade pip --dry-run 2>&1
+ } catch {
+ $dry = $null
+ }
+ if ($null -ne $dry) {
+ # look for "pip-1.2.3" or parenthesized versions like "(1.2.3)"
+ $m = [regex]::Match($dry, 'pip-?(\d+(?:\.\d+)*)')
+ if ($m.Success) { return $m.Groups[1].Value }
+ $m2 = [regex]::Match($dry, '\((\d+(?:\.\d+)*)\)')
+ if ($m2.Success) { return $m2.Groups[1].Value }
+ }
+ return $null
+}
+
+function Assert-PipUpToDate {
+ Write-Info "Checking pip version ..."
+ $current = Get-PipVersion
+ $latest = Get-LatestPipVersion
+
+ if (-not $current -or -not $latest) {
+ Write-Warn "Could not determine pip versions (current: $current, latest: $latest). Skipping upgrade check."
+ return
+ }
+ Write-Info "Current: $current | Latest: $latest"
+ if ($current -eq $latest) {
+ Write-Pass "pip is up to date."
+ } else {
+ Write-Warn "pip is outdated."
+ Write-Task "upgrading ..."
+ try { & python -m pip install --upgrade pip } catch { Write-Fail "pip upgrade failed: $($_.Exception.Message)" }
+ }
+}
+
+# -----------------------------
+# π pre-commit helpers (robust regex)
+# -----------------------------
+function Assert-PreCommitInstalled {
+ Write-Info "Checking pre-commit installation ..."
+ if (Get-Command pre-commit -ErrorAction SilentlyContinue) {
+ Write-Pass "pre-commit is installed."
+ } else {
+ Write-Warn "pre-commit is missing."
+ Write-Task "installing ..."
+ try { & pip install pre-commit } catch { Write-Fail "Failed to install pre-commit: $($_.Exception.Message)"; return }
+ }
+}
+
+function Get-PreCommitVersion {
+ try { $out = & pre-commit --version 2>&1 } catch { return $null }
+ $match = [regex]::Match($out, 'pre-commit\s+(\d+(?:\.\d+)*)')
+ if ($match.Success) { return $match.Groups[1].Value }
+ return $null
+}
+
+function Get-LatestPreCommitVersion {
+ try { $dry = & pip install pre-commit --upgrade --dry-run 2>&1 } catch { $dry = $null }
+ if ($null -ne $dry) {
+ $m = [regex]::Match($dry, 'commit-?(\d+(?:\.\d+)*)')
+ if ($m.Success) { return $m.Groups[1].Value }
+ $m2 = [regex]::Match($dry, '\((\d+(?:\.\d+)*)\)')
+ if ($m2.Success) { return $m2.Groups[1].Value }
+ }
+ return $null
+}
+
+function Assert-PreCommitUpToDate {
+ Write-Info "Checking pre-commit version ..."
+ $current = Get-PreCommitVersion
+ $latest = Get-LatestPreCommitVersion
+ if (-not $current -or -not $latest) {
+ Write-Warn "Could not determine pre-commit versions (current: $current, latest: $latest). Skipping upgrade check."
+ return
+ }
+ Write-Info "Current: $current | Latest: $latest"
+ if ($current -eq $latest) {
+ Write-Pass "pre-commit is up to date."
+ } else {
+ Write-Warn "pre-commit is outdated."
+ Write-Task "upgrading ..."
+ try { & pip install --upgrade pre-commit } catch { Write-Fail "Failed to upgrade pre-commit: $($_.Exception.Message)" }
+ }
+}
+
+# -----------------------------
+# π Config file creators (PowerShell-native)
+# -----------------------------
+function New-PreCommitConfig { @"
+default_stages: [pre-commit, manual]
+
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v6.0.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ args: ["--unsafe"]
+ - id: check-added-large-files
+
+ - repo: https://github.com/tcort/markdown-link-check
+ rev: v3.13.7
+ hooks:
+ - id: markdown-link-check
+ args: [-q]
+
+ - repo: https://github.com/igorshubovych/markdownlint-cli
+ rev: v0.45.0
+ hooks:
+ - id: markdownlint
+ args: ["--ignore", "CHANGELOG.md", "--fix"]
+"@ | Out-File -FilePath ".pre-commit-config.yaml" -Encoding utf8 }
+
+function New-MarkdownLintConfig { @"
+{
+ "comment": "Markdown Lint Rules",
+ "default": true,
+ "MD007": {"indent": 4},
+ "MD013": false,
+ "MD024": false,
+ "MD025": {"front_matter_title": ""},
+ "MD029": {"style": "one_or_ordered"},
+ "MD033": false
+}
+"@ | Out-File -FilePath ".markdownlint.json" -Encoding utf8 }
+
+function New-CommitLintConfig { @"
+{
+ "rules": {
+ "body-leading-blank": [1, "always"],
+ "footer-leading-blank": [1, "always"],
+ "header-max-length": [2, "always", 72],
+ "scope-case": [2, "always", "upper-case"],
+ "scope-empty": [2, "never"],
+ "subject-case": [2, "never", ["start-case", "pascal-case", "upper-case"]],
+ "subject-empty": [2, "never"],
+ "subject-full-stop": [2, "never", "."],
+ "type-case": [2, "always", "lower-case"],
+ "type-empty": [2, "never"],
+ "type-enum": [2, "always", ["build","chore","ci","docs","feat","fix","perf","refactor","revert","style","test"]]
+ }
+}
+"@ | Out-File -FilePath ".commitlintrc.json" -Encoding utf8 }
+
+# -----------------------------
+# π§ Assert files and hooks
+# -----------------------------
+function Assert-File([string]$path, [scriptblock]$createBlock) {
+ if (Test-Path $path) {
+ Write-Pass "$path already exists, please ensure it has the correct format."
+ } else {
+ Write-Warn "$path file is missing."
+ Write-Task "creating ..."
+ & $createBlock
+ }
+}
+
+function Assert-PreCommitHooks {
+ Write-Info "Checking pre-commit config and hooks ..."
+ Assert-File ".pre-commit-config.yaml" { New-PreCommitConfig }
+ try {
+ & pre-commit autoupdate
+ & pre-commit install
+ } catch {
+ Write-Warn "pre-commit command failed to run: $($_.Exception.Message)"
+ }
+
+ # If commit-msg/commitlint present, install commit-msg hook and Assert commitlintrc
+ $hasCommitMsg = Select-String -Path ".pre-commit-config.yaml" -Pattern "commit-msg|commitlint" -Quiet
+ if ($hasCommitMsg) {
+ Write-Task "Installing commit-msg hook ..."
+ try {
+ & pre-commit install --hook-type commit-msg
+ } catch {
+ Write-Warn "Could not install commit-msg hook: $($_.Exception.Message)"
+ }
+ Assert-File ".commitlintrc.json" { New-CommitLintConfig }
+ }
+ Assert-File ".markdownlint.json" { New-MarkdownLintConfig }
+}
+
+# -----------------------------
+# π Run tasks
+# -----------------------------
+Assert-VirtualEnv
+Assert-PipUpToDate
+Assert-PreCommitInstalled
+Assert-PreCommitUpToDate
+Assert-PreCommitHooks
+
+Write-Pass "π Setup Completed Successfully!"
diff --git a/src/script/dev_setup.sh b/src/script/dev_setup.sh
new file mode 100644
index 0000000..0581747
--- /dev/null
+++ b/src/script/dev_setup.sh
@@ -0,0 +1,263 @@
+#!/usr/bin/env bash
+set -o pipefail
+
+# π¨ Colors
+NC='\033[0m' # No Color
+CYAN='\033[0;36m'
+YELLOW='\033[0;33m'
+GREEN='\033[0;32m'
+RED='\033[0;31m'
+MAGENTA='\033[0;35m'
+
+# π± Default virtual environment directory
+VENV_DIR=".venv"
+
+# -----------------------------
+# π― Logging Functions
+# -----------------------------
+print_info() {
+ echo -e "π‘ [${CYAN}INFO${NC}] ${CYAN}$1${NC}"
+}
+print_task() {
+ echo -e "β‘ [${YELLOW}TASK${NC}] ${YELLOW}$1${NC}"
+}
+print_pass() {
+ echo -e "β
[${GREEN}PASS${NC}] ${GREEN}$1${NC}"
+}
+print_warning() {
+ echo -e "β οΈ [${MAGENTA}WARN${NC}] ${MAGENTA}$1${NC}"
+}
+print_error() {
+ echo -e "β [${RED}FAIL${NC}] ${RED}$1${NC}"
+}
+
+# -----------------------------
+# π Virtual Environment Check
+# -----------------------------
+virtual_environment_check() {
+ print_info "Checking virtual environment ..."
+
+ if [[ -d "$VENV_DIR" && -f "$VENV_DIR/bin/activate" ]]; then
+ if [[ -n "${VIRTUAL_ENV:-}" ]]; then
+ print_pass "Virtual environment found and active."
+ else
+ print_info "Virtual environment found but not active."
+ print_task "activating ..."
+ source "$VENV_DIR/bin/activate"
+ fi
+ else
+ print_warning "No virtual environment found."
+ print_task "creating and activating ..."
+ python3 -m venv "$VENV_DIR"
+ source "$VENV_DIR/bin/activate"
+ fi
+}
+
+# -----------------------------
+# π§ Pip Version Check
+# -----------------------------
+pip_current_version_check() {
+ CURRENT_VERSION=$(pip --version | grep -oP '(?<=pip )\d+(\.\d+)+')
+ [[ -z "$CURRENT_VERSION" ]] && print_error "Could not extract pip version."
+}
+
+pip_latest_version_check() {
+ DRY_RUN_OUTPUT=$(python3 -m pip install --upgrade pip --dry-run 2> /dev/null)
+ LATEST_VERSION=$(echo "$DRY_RUN_OUTPUT" | grep -oP 'pip-[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n1 | tr -d 'pip-')
+ [[ -z "$LATEST_VERSION" ]] && LATEST_VERSION=$(echo "$DRY_RUN_OUTPUT" | grep -oP '\([0-9]+\.[0-9]+(\.[0-9]+)?\)' | head -n1 | tr -d '()')
+ [[ -z "$LATEST_VERSION" ]] && print_error "Could not determine the latest pip version."
+}
+
+pip_status_check() {
+ print_info "Checking pip version ..."
+ print_info "Current: ${CURRENT_VERSION} | Latest: ${LATEST_VERSION}"
+
+ if [[ "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then
+ print_pass "pip is up to date."
+ else
+ print_warning "pip is outdated."
+ print_task "updating ..."
+ python3 -m pip install --upgrade pip
+ fi
+}
+
+# -----------------------------
+# π Pre-commit Check
+# -----------------------------
+pre_commit_status_check() {
+ print_info "Checking pre-commit installation ..."
+ if command -v pre-commit > /dev/null 2>&1; then
+ print_pass "pre-commit is installed."
+ else
+ print_warning "pre-commit is missing."
+ print_task "Installing pre-commit ..."
+ pip install pre-commit
+ fi
+}
+
+pre_commit_current_version_check() {
+ CURRENT_VERSION=$(pre-commit --version | grep -oP '(?<=pre-commit )\d+(\.\d+)+')
+}
+
+pre_commit_latest_version_check() {
+ DRY_RUN_OUTPUT=$(pip install pre-commit --upgrade --dry-run 2> /dev/null)
+ LATEST_VERSION=$(echo "$DRY_RUN_OUTPUT" | grep -oP 'commit-[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n1 | tr -d 'commit-')
+ [[ -z "$LATEST_VERSION" ]] && LATEST_VERSION=$(echo "$DRY_RUN_OUTPUT" | grep -oP '\([0-9]+\.[0-9]+(\.[0-9]+)?\)' | head -n1 | tr -d '()')
+ [[ -z "$LATEST_VERSION" ]] && print_error "Could not determine the latest pre-commit version."
+}
+
+pre_commit_version_check() {
+ print_info "Checking pre-commit version ..."
+ print_info "Current: ${CURRENT_VERSION} | Latest: ${LATEST_VERSION}"
+
+ if [[ "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then
+ print_pass "pre-commit is up to date."
+ else
+ print_warning "pre-commit is outdated."
+ print_task "updating ..."
+ pip install --upgrade pre-commit
+ fi
+}
+
+# -----------------------------
+# π Pre Commit Config File Creation
+# -----------------------------
+pre_commit_config_create() {
+ cat << EOF > .pre-commit-config.yaml
+default_stages: [pre-commit, manual]
+
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v6.0.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ args: ["--unsafe"]
+ - id: check-added-large-files
+
+ - repo: https://github.com/tcort/markdown-link-check
+ rev: v3.13.7
+ hooks:
+ - id: markdown-link-check
+ args: [-q]
+
+ - repo: https://github.com/igorshubovych/markdownlint-cli
+ rev: v0.45.0
+ hooks:
+ - id: markdownlint
+ args: ["--ignore", "CHANGELOG.md", "--fix"]
+EOF
+}
+
+# -----------------------------
+# π Markdown Lint Config File Creation
+# -----------------------------
+markdownlint_create() {
+ cat << EOF > .markdownlint.json
+{
+ "comment": "Markdown Lint Rules",
+ "default": true,
+ "MD007": {"indent": 4},
+ "MD013": false,
+ "MD024": false,
+ "MD025": {"front_matter_title": ""},
+ "MD029": {"style": "one_or_ordered"},
+ "MD033": false
+}
+EOF
+}
+
+# -----------------------------
+# π Commit Lint Config File Creation
+# -----------------------------
+commitlintrc_create() {
+ cat << EOF > .commitlintrc.json
+{
+ "rules": {
+ "body-leading-blank": [1, "always"],
+ "footer-leading-blank": [1, "always"],
+ "header-max-length": [2, "always", 72],
+ "scope-case": [2, "always", "upper-case"],
+ "scope-empty": [2, "never"],
+ "subject-case": [2, "never", ["start-case", "pascal-case", "upper-case"]],
+ "subject-empty": [2, "never"],
+ "subject-full-stop": [2, "never", "."],
+ "type-case": [2, "always", "lower-case"],
+ "type-empty": [2, "never"],
+ "type-enum": [2, "always", ["build","chore","ci","docs","feat","fix","perf","refactor","revert","style","test"]]
+ }
+}
+EOF
+}
+
+# -----------------------------
+# π§ Config Files Checks
+# -----------------------------
+commitlintrc_file_check() {
+ print_info "Checking .commitlintrc.json ..."
+ if [[ -f ".commitlintrc.json" ]]; then
+ print_pass "Already exists, please ensure it has the correct format."
+ else
+ print_warning ".pre-commit-config.yaml file is missing."
+ print_task "creating ..."
+ commitlintrc_create
+ fi
+}
+
+markdownlint_file_check() {
+ print_info "Checking .markdownlint.json ..."
+ if [[ -f ".markdownlint.json" ]]; then
+ print_pass "Already exists, please ensure it has the correct format."
+ else
+ print_warning ".markdownlint.json file is missing."
+ print_task "creating ..."
+ markdownlint_create
+ fi
+}
+
+# -----------------------------
+# π§ Pre Commit Hooks Overall Check
+# -----------------------------
+pre_commit_hooks_check() {
+ print_info "Checking pre-commit hooks ..."
+ if [[ -f ".pre-commit-config.yaml" ]]; then
+ print_pass ".pre-commit-config.yaml already exists, please ensure it has the correct format."
+ print_task "Updating and installing hooks ..."
+ pre-commit autoupdate
+ pre-commit install
+ [[ $(grep -v '^\s*#' .pre-commit-config.yaml | grep -cE "commit-msg|commitlint") -gt 0 ]] && {
+ print_task "Installing commit-msg hook ..."
+ pre-commit install --hook-type commit-msg
+ commitlintrc_file_check
+ }
+ else
+ print_warning ".pre-commit-config.yaml is missing."
+ print_task "creating ..."
+ pre_commit_config_create
+ pre-commit autoupdate
+ pre-commit install
+ [[ $(grep -v '^\s*#' .pre-commit-config.yaml | grep -cE "commit-msg|commitlint") -gt 0 ]] && {
+ print_task "Installing commit-msg hook ..."
+ pre-commit install --hook-type commit-msg
+ commitlintrc_file_check
+ }
+ fi
+}
+
+# -----------------------------
+# π Execute Steps
+# -----------------------------
+virtual_environment_check
+pip_current_version_check
+pip_latest_version_check
+pip_status_check
+
+pre_commit_status_check
+pre_commit_current_version_check
+pre_commit_latest_version_check
+pre_commit_version_check
+pre_commit_hooks_check
+markdownlint_file_check
+
+print_pass "π Setup Completed Successfully!"