Skip to content

Maintenance: Docs

Maintenance: Docs #245

name: "Maintenance: Docs"
# Rechenaufwand-Score: R=2 (K=2, L=2, N=2) | last-calibrated: 2026-08-25
# Trigger policy: repo framework score calibration for workflow cost controls.
# Consolidated docs-maintenance workflow.
# Replaces: 08-maintenance_ai-context-wiki-sync.yml,
# 08-maintenance_code-maturity.yml,
# 08-maintenance_docs-orphan-check.yml,
# 08-maintenance_root-docs-hygiene.yml,
# 08-maintenance_src-include-docs-align.yml
on:
# push trigger retained for docs/ai_context/ai_working changes merged to develop/community.
# pull_request trigger removed — doc hygiene checks run on schedule and do not need
# to block PRs (they are non-blocking maintenance tasks, not PR gate requirements).
push:
branches: [develop, community]
paths:
- 'ai_context/**'
- 'ai_working/**'
- 'INDEX.md'
- 'LOG.md'
- 'AI_WIKI_INTEGRATION_PLAYBOOK.md'
- 'scripts/ai-context-lint.py'
- 'scripts/ai-dev-llm-wiki-sync.py'
- 'ai_context/developer_llm_wiki/**'
- 'ai_context/DEVELOPER_LLM_WIKI_FRAMEWORK.md'
- 'ROADMAP.md'
- 'FUTURE_ENHANCEMENTS.md'
- 'DOCUMENTATION_GOVERNANCE.md'
- 'RELEASE_STRATEGY.md'
- 'BRANCHING_STRATEGY.md'
- 'VERSIONING.md'
- 'CHANGELOG.md'
- 'docs/de/**'
- 'docs/en/**'
- 'docs/**'
- 'scripts/docs-orphan-check.py'
- 'scripts/root-docs-hygiene.py'
- 'scripts/src-include-docs-align.py'
- '.github/scripts/code_maturity_header_writer.py'
- '.github/workflows/maintenance-docs.yml'
pull_request:
branches: [develop, community]
paths:
- 'ai_context/**'
- 'docs/de/**'
- 'docs/en/**'
- 'docs/**'
- 'INDEX.md'
- 'LOG.md'
- 'AI_WIKI_INTEGRATION_PLAYBOOK.md'
- 'scripts/ai-context-lint.py'
- 'scripts/ai-dev-llm-wiki-sync.py'
- 'ai_context/developer_llm_wiki/**'
- 'ai_context/DEVELOPER_LLM_WIKI_FRAMEWORK.md'
- 'ROADMAP.md'
- 'FUTURE_ENHANCEMENTS.md'
- 'DOCUMENTATION_GOVERNANCE.md'
- 'RELEASE_STRATEGY.md'
- 'BRANCHING_STRATEGY.md'
- 'VERSIONING.md'
- 'CHANGELOG.md'
- 'scripts/docs-orphan-check.py'
- 'scripts/root-docs-hygiene.py'
- 'scripts/src-include-docs-align.py'
- '.github/scripts/code_maturity_header_writer.py'
- '.github/workflows/maintenance-docs.yml'
schedule:
- cron: '0 4 * * *' # Daily 04:00 UTC — all daily doc checks (maturity, orphan, hygiene)
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC — src/include alignment
workflow_dispatch:
inputs:
fail_on_findings:
description: 'Fail on any findings (orphan check, hygiene, alignment)'
type: boolean
default: false
update_headers:
description: 'Code maturity: rewrite headers (default: check-only)'
type: boolean
default: false
full_repo_scan:
description: 'Code maturity: include full repository scan target (`.`) for supported code extensions'
type: boolean
default: false
include_all_files:
description: 'Code maturity: update/validate all scanned files regardless of score threshold'
type: boolean
default: false
header_mode:
description: 'Code maturity: header richness mode'
type: choice
default: extended
options:
- lean
- extended
- auto
min_score:
description: 'Code maturity: score threshold when include_all_files=false'
type: string
default: '80'
target_paths:
description: 'Code maturity: comma-separated scan roots (ignored when full_repo_scan=true)'
type: string
default: 'src,include,tests,benchmarks'
apply_ai_updates:
description: 'AI context: apply lint updates'
type: boolean
default: false
llm_wiki_apply_updates:
description: 'Developer LLM Wiki: write/update ai_context/developer_llm_wiki artifacts'
type: boolean
default: false
llm_wiki_full_sync:
description: 'Developer LLM Wiki: perform initial/global source migration scan'
type: boolean
default: false
llm_wiki_fail_on_findings:
description: 'Developer LLM Wiki: fail job when validation findings exist'
type: boolean
default: false
concurrency:
group: maintenance-docs-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
# ──────────────────────────────────────────────────────────────────────────────
jobs:
# 1. AI Context Wiki Sync
ai-context-sync:
name: AI Context lint and sync
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: '3.11'
- name: Run AI context lint
id: lint
run: |
APPLY_FLAG=""
[ "${{ inputs.apply_ai_updates }}" = "true" ] && APPLY_FLAG="--apply-updates"
python3 scripts/ai-context-lint.py \
--format text \
--output /tmp/ai-context-lint.txt \
$APPLY_FLAG
python3 scripts/ai-context-lint.py \
--format json \
--output /tmp/ai-context-lint.json || true
- name: Upload lint artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ai-context-lint-${{ github.run_number }}
path: |
/tmp/ai-context-lint.txt
/tmp/ai-context-lint.json
retention-days: 30
if-no-files-found: ignore
- name: Write job summary
if: always()
run: |
{
echo "## AI Context Sync"
if [ -f /tmp/ai-context-lint.txt ]; then
echo '```'; cat /tmp/ai-context-lint.txt; echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"
# 2. Code Maturity Analysis
developer-llm-wiki:
name: Developer LLM Wiki sync
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: '3.11'
- name: Run developer LLM wiki sync/check
run: |
APPLY_FLAG=""
FULL_FLAG=""
FAIL_FLAG=""
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.llm_wiki_apply_updates }}" = "true" ]; then
APPLY_FLAG="--apply-updates"
fi
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.llm_wiki_full_sync }}" = "true" ]; then
FULL_FLAG="--full-sync"
fi
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.llm_wiki_fail_on_findings }}" = "true" ]; then
FAIL_FLAG="--fail-on-findings"
fi
python3 scripts/ai-dev-llm-wiki-sync.py \
--output-text /tmp/dev-llm-wiki-sync.txt \
--output-json /tmp/dev-llm-wiki-sync.json \
$APPLY_FLAG \
$FULL_FLAG \
$FAIL_FLAG
- name: Upload developer LLM wiki artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: developer-llm-wiki-${{ github.run_number }}
path: |
/tmp/dev-llm-wiki-sync.txt
/tmp/dev-llm-wiki-sync.json
ai_context/developer_llm_wiki/
retention-days: 30
if-no-files-found: ignore
- name: Write job summary
if: always()
run: |
{
echo "## Developer LLM Wiki Sync"
if [ -f /tmp/dev-llm-wiki-sync.txt ]; then
echo '```'; cat /tmp/dev-llm-wiki-sync.txt; echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"
# 3. Code Maturity Analysis
code-maturity:
name: Code Maturity Analysis
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: '3.11'
- name: Determine run mode
id: mode
run: |
HEADER_MODE="extended"
MIN_SCORE="80"
TARGET_PATHS="src,include,tests,benchmarks"
INCLUDE_ALL_FLAG=""
FAIL_ON_VALIDATION_FLAG=""
REPORT_PATH_FLAG="--report-path /tmp/code_maturity_report.md"
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.update_headers }}" = "true" ]; then
{
echo "mode=rewrite"
echo "no_headers_flag="
} >> "$GITHUB_OUTPUT"
else
{
echo "mode=check-only"
echo "no_headers_flag=--no-headers"
} >> "$GITHUB_OUTPUT"
fi
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
HEADER_MODE="${{ inputs.header_mode }}"
MIN_SCORE="${{ inputs.min_score }}"
TARGET_PATHS="${{ inputs.target_paths }}"
if [ "${{ inputs.full_repo_scan }}" = "true" ]; then
TARGET_PATHS="."
fi
if [ "${{ inputs.include_all_files }}" = "true" ] || [ "${{ inputs.full_repo_scan }}" = "true" ]; then
INCLUDE_ALL_FLAG="--include-all-files"
fi
if [ "${{ inputs.fail_on_findings }}" = "true" ]; then
FAIL_ON_VALIDATION_FLAG="--fail-on-validation"
fi
fi
{
echo "header_mode=$HEADER_MODE"
echo "min_score=$MIN_SCORE"
echo "target_paths=$TARGET_PATHS"
echo "include_all_flag=$INCLUDE_ALL_FLAG"
echo "fail_on_validation_flag=$FAIL_ON_VALIDATION_FLAG"
echo "report_path_flag=$REPORT_PATH_FLAG"
} >> "$GITHUB_OUTPUT"
- name: Run code maturity analysis (${{ steps.mode.outputs.mode }})
run: |
python .github/scripts/code_maturity_header_writer.py \
--root . \
--header-mode "${{ steps.mode.outputs.header_mode }}" \
--min-score "${{ steps.mode.outputs.min_score }}" \
--target-paths "${{ steps.mode.outputs.target_paths }}" \
${{ steps.mode.outputs.include_all_flag }} \
${{ steps.mode.outputs.no_headers_flag }} \
${{ steps.mode.outputs.fail_on_validation_flag }} \
${{ steps.mode.outputs.report_path_flag }} \
2>&1 | tee /tmp/maturity_run.log
- name: Upload maturity report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: code-maturity-report-${{ github.run_number }}
path: |
/tmp/code_maturity_report.md
docs/code_maturity_report.md
retention-days: 30
if-no-files-found: ignore
- name: Write job summary
if: always()
run: |
{
echo "## Code Maturity — ${{ steps.mode.outputs.mode }}"
echo "| Mode | ${{ steps.mode.outputs.mode }} |"
echo "| Event | \`${{ github.event_name }}\` |"
echo "| Header mode | \`${{ steps.mode.outputs.header_mode }}\` |"
echo "| Min score | \`${{ steps.mode.outputs.min_score }}\` |"
echo "| Target paths | \`${{ steps.mode.outputs.target_paths }}\` |"
echo "| Include all files | \`${{ steps.mode.outputs.include_all_flag != '' }}\` |"
} >> "$GITHUB_STEP_SUMMARY"
# 4. Docs Orphan Check
docs-orphan-check:
name: docs/de + docs/en Orphan Check
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: '3.11'
- name: Run docs orphan check
run: |
FAIL_FLAG=""
[ "${{ inputs.fail_on_findings }}" = "true" ] && FAIL_FLAG="--fail-on-findings"
python3 scripts/docs-orphan-check.py --format text --output /tmp/docs-orphan-check.txt $FAIL_FLAG
python3 scripts/docs-orphan-check.py --format json --output /tmp/docs-orphan-check.json --quiet || true
- name: Upload orphan-check reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: docs-orphan-check-${{ github.run_number }}
path: |
/tmp/docs-orphan-check.txt
/tmp/docs-orphan-check.json
retention-days: 30
if-no-files-found: ignore
- name: Write job summary
if: always()
run: |
{
echo "## Docs Orphan Check"
if [ -f /tmp/docs-orphan-check.txt ]; then
echo '```'; cat /tmp/docs-orphan-check.txt; echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"
# 5. Root Docs Hygiene
root-docs-hygiene:
name: Repo root markdown hygiene
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: '3.11'
- name: Run root docs hygiene check
run: |
FAIL_FLAG=""
[ "${{ inputs.fail_on_findings }}" = "true" ] && FAIL_FLAG="--fail-on-findings"
python3 scripts/root-docs-hygiene.py --format text --output /tmp/root-docs-hygiene.txt $FAIL_FLAG
python3 scripts/root-docs-hygiene.py --format json --output /tmp/root-docs-hygiene.json || true
- name: Upload hygiene reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: root-docs-hygiene-${{ github.run_number }}
path: |
/tmp/root-docs-hygiene.txt
/tmp/root-docs-hygiene.json
retention-days: 30
if-no-files-found: ignore
- name: Write job summary
if: always()
run: |
{
echo "## Root Docs Hygiene"
if [ -f /tmp/root-docs-hygiene.txt ]; then
echo '```'; cat /tmp/root-docs-hygiene.txt; echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"
# 6. src/include ↔ docs/de Alignment
src-docs-alignment:
name: src/include ↔ docs/de Alignment
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: '3.11'
- name: Run alignment check
run: |
FAIL_FLAG=""
[ "${{ inputs.fail_on_findings }}" = "true" ] && FAIL_FLAG="--fail-on-gaps"
python3 scripts/src-include-docs-align.py --format text --output /tmp/align-report.txt $FAIL_FLAG
python3 scripts/src-include-docs-align.py --format json --output /tmp/align-report.json --quiet || true
- name: Upload alignment reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: src-include-docs-align-${{ github.run_number }}
path: |
/tmp/align-report.txt
/tmp/align-report.json
retention-days: 30
if-no-files-found: ignore
- name: Write job summary
if: always()
run: |
{
echo "## src/include ↔ docs/de Alignment"
if [ -f /tmp/align-report.txt ]; then
echo '```'; cat /tmp/align-report.txt; echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"
# ────────────────────────────────────────────────────────────────────────────
# 5. ai_working + ai_context Hygiene
# Detects stale AI-session working documents in ai_working/ (> STALE_DAYS days,
# not governance-critical) and reports orphaned ai_context/ entries.
# Governance-critical files are explicitly excluded from the stale-file list:
# - ai_working/ENFORCEMENT_WAIVERS.md (read by compliance-governance-gates.yml)
# - ai_working/MERGE_GATE_AUDIT_LOG.jsonl (read by compliance-governance-gates.yml)
# - ai_working/gs3/ (used by maintenance-gs3-gaps.yml; partially .gitignored)
# This job is report-only: it never deletes files and never fails the workflow.
# ────────────────────────────────────────────────────────────────────────────
ai-working-hygiene:
name: ai_working + ai_context Hygiene
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: '3.11'
- name: Scan ai_working/ for stale files and ai_context/ for orphans
id: hygiene
run: |
python3 - <<'PY'
import json
import os
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
STALE_DAYS = 30
REPO_ROOT = Path(
subprocess.check_output(["git", "rev-parse", "--show-toplevel"],
text=True).strip()
)
# Governance-critical files that must not be flagged as stale
GOVERNANCE_KEEP = {
"ENFORCEMENT_WAIVERS.md",
"MERGE_GATE_AUDIT_LOG.jsonl",
"MERGE_GATE_AUDIT_LOG.json",
}
# Subdirectories managed by other workflows — skip them
SKIP_SUBDIRS = {"gs3", "archive", "backups", "delta"}
def git_mtime(path: Path) -> datetime:
"""Return last-commit timestamp for a file (UTC)."""
try:
ts = subprocess.check_output(
["git", "log", "-1", "--format=%ct", "--", str(path)],
text=True,
).strip()
return datetime.fromtimestamp(int(ts), tz=timezone.utc) if ts else datetime.now(tz=timezone.utc)
except Exception:
return datetime.now(tz=timezone.utc)
now = datetime.now(tz=timezone.utc)
stale_files = []
ai_working = REPO_ROOT / "ai_working"
if ai_working.is_dir():
for f in sorted(ai_working.iterdir()):
if f.is_dir() and f.name in SKIP_SUBDIRS:
continue
if not f.is_file():
continue
if f.name in GOVERNANCE_KEEP:
continue
# Only track markdown and plain-text docs (not generated JSON/JSONL)
if f.suffix.lower() not in (".md", ".txt", ".rst"):
continue
age_days = (now - git_mtime(f)).days
if age_days >= STALE_DAYS:
stale_files.append({"file": str(f.relative_to(REPO_ROOT)),
"age_days": age_days})
# --- ai_context orphan check: files not referenced in INDEX.md or PLAYBOOK ---
referenced = set()
for anchor_file in ["INDEX.md", "AI_WIKI_INTEGRATION_PLAYBOOK.md"]:
p = REPO_ROOT / anchor_file
if p.exists():
for line in p.read_text(encoding="utf-8", errors="replace").splitlines():
if "ai_context/" in line:
# extract bare filename
for token in line.split():
if "ai_context/" in token:
fname = token.strip("()[]()`*\"'#").split("/")[-1].split(")")[0]
referenced.add(fname)
ai_context = REPO_ROOT / "ai_context"
orphan_ctx = []
if ai_context.is_dir():
for f in sorted(ai_context.rglob("*.md")):
if f.name not in referenced:
orphan_ctx.append(str(f.relative_to(REPO_ROOT)))
report = {
"stale_ai_working": stale_files,
"orphan_ai_context": orphan_ctx,
"stale_threshold_days": STALE_DAYS,
"scanned_at": now.isoformat(),
}
report_path = Path("/tmp/ai-hygiene-report.json")
report_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
# Write step summary
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
lines = []
lines.append("## ai_working + ai_context Hygiene Report\n")
lines.append(f"Stale threshold: **{STALE_DAYS} days**\n")
lines.append(f"\n### Stale ai_working/ files ({len(stale_files)})\n")
if stale_files:
lines.append("| File | Age (days) |\n|---|---|\n")
for entry in stale_files:
lines.append(f"| `{entry['file']}` | {entry['age_days']} |\n")
lines.append("\n> ℹ️ Review and remove stale working documents when no longer needed.\n")
else:
lines.append("✅ No stale ai_working/ documents.\n")
lines.append(f"\n### Unreferenced ai_context/ files ({len(orphan_ctx)})\n")
if orphan_ctx:
lines.append("These files are not linked from INDEX.md or AI_WIKI_INTEGRATION_PLAYBOOK.md:\n")
for f in orphan_ctx[:20]:
lines.append(f"- `{f}`\n")
if len(orphan_ctx) > 20:
lines.append(f"- ... and {len(orphan_ctx) - 20} more\n")
else:
lines.append("✅ All ai_context/ files are referenced.\n")
output = "".join(lines)
if summary_path:
with open(summary_path, "a", encoding="utf-8") as fh:
fh.write(output)
else:
print(output)
# Write human-readable text report
Path("/tmp/ai-hygiene-report.txt").write_text(output, encoding="utf-8")
# Set output for downstream notification use
github_output = os.environ.get("GITHUB_OUTPUT")
if github_output:
with open(github_output, "a") as fh:
fh.write(f"stale_count={len(stale_files)}\n")
fh.write(f"orphan_ctx_count={len(orphan_ctx)}\n")
PY
- name: Upload hygiene report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ai-hygiene-report-${{ github.run_number }}
path: |
/tmp/ai-hygiene-report.json
/tmp/ai-hygiene-report.txt
retention-days: 30
if-no-files-found: ignore