Put the language in design document names and add a sync mode

This commit is contained in:
Jan Doubravský
2026-08-18 13:26:19 +02:00
parent 950e946004
commit 6737a27fa0
11 changed files with 306 additions and 89 deletions
+1
View File
@@ -0,0 +1 @@
__pycache__/
+26
View File
@@ -7,6 +7,32 @@ ships documents, so it uses the same `v#` scheme as the documents themselves.
Individual guideline documents carry their own `Document Version` header, and the skills carry
`metadata.version`; both are independent of the revisions below.
## v6 — 2026-08-18
### Added
- `--sync` in `check_versions.py` — offers to overwrite the outdated copies with the master, after
listing them and asking for confirmation (`--yes` skips the prompt, `--dry-run` writes nothing).
Each file keeps the line endings it already used, so a version bump stays a one-line diff on
Windows and Linux alike, and the write goes through a backup that is restored if it fails.
A `MODIFIED` copy is never overwritten — it holds a local edit that copying would discard
- `.gitignore` at the repository root for `__pycache__/`
### Changed
- **The design documents are renamed so the language is part of the file name**:
`DESIGN_DOCUMENT.md``DESIGN_DOCUMENT_PYTHON.md` / `DESIGN_DOCUMENT_RUST.md`,
`DESIGN_DOCUMENT_MODULE.md``DESIGN_DOCUMENT_PYTHON_MODULE.md`,
`DESIGN_DOCUMENT_LIB.md``DESIGN_DOCUMENT_RUST_LIB.md`. Python and Rust applications used to
share one file name, which only worked because the folder here disambiguated them — and the folder
does not travel with the copy that lands in a project root. A project can now carry one design
document per language, and `check_versions.py` no longer has to guess the language from
`Cargo.toml`. Bumped: `DESIGN_DOCUMENT_PYTHON.md` v10 → v11, `DESIGN_DOCUMENT_PYTHON_MODULE.md`
v3 → v4, `DESIGN_DOCUMENT_RUST.md` v3 → v4, `DESIGN_DOCUMENT_RUST_LIB.md` v3 → v4,
`AGENTS.md` v7 → v8, `CLAUDE.md` v2 → v3
- `check_versions.py` recognises the pre-rename file names, reports them as `OLD NAME`, and renames
them during `--sync`. The `Cargo.toml` check survives only to disambiguate legacy
`DESIGN_DOCUMENT.md` copies and can be deleted once every project is migrated. A project holding
both the old and the new name is reported as `DUPLICATE` and never touched
## v5 — 2026-08-18
### Added
+10 -7
View File
@@ -1,6 +1,6 @@
# AI Agents - Project Rules
**Document Version:** v7 (independent, incremented on structural changes)
**Document Version:** v8 (independent, incremented on structural changes)
Language-agnostic rules for AI assistants (Claude Code, Cursor, Copilot, etc.).
@@ -8,13 +8,16 @@ Everything language-specific — package manager, formatter, linter, test framew
logging library, line length, project layout — lives in the matching design document:
| Project type | Design document |
|--------------------|------------------------------|
| Python application | `DESIGN_DOCUMENT.md` |
| Python library | `DESIGN_DOCUMENT_MODULE.md` |
| Rust application | `DESIGN_DOCUMENT.md` |
| Rust library | `DESIGN_DOCUMENT_LIB.md` |
|--------------------|------------------------------------|
| Python application | `DESIGN_DOCUMENT_PYTHON.md` |
| Python library | `DESIGN_DOCUMENT_PYTHON_MODULE.md` |
| Rust application | `DESIGN_DOCUMENT_RUST.md` |
| Rust library | `DESIGN_DOCUMENT_RUST_LIB.md` |
| Godot | `DESIGN_DOCUMENT_GODOT.md` |
The language is part of the file name, so a project that mixes languages carries one design document
per language and nothing collides.
Where this file and a design document disagree, **the design document wins**.
## First-time setup
@@ -67,7 +70,7 @@ Where this file and a design document disagree, **the design document wins**.
- **Commit the lock file for applications, do not commit it for libraries**
- **Every core document is committed**, including the shared ones (`CLAUDE.md`, `AGENTS.md`, `DESIGN_DOCUMENT*.md`). A checkout then carries the rules that applied to that code, and a fresh clone works standalone.
- **Never edit a shared document inside a project** — it is a copy. Changes belong in the documentation repository and are copied outward; project-specific deviations go in `PROJECT.md`.
- Synchronising the copies is its own commit (`docs: sync guidelines to AGENTS v7 / DESIGN_DOCUMENT v10`) and is **not** recorded in the project `CHANGELOG.md` — it is not a change to the product
- Synchronising the copies is its own commit (`docs: sync guidelines to AGENTS v8 / DESIGN_DOCUMENT_PYTHON v11`) and is **not** recorded in the project `CHANGELOG.md` — it is not a change to the product
### Commit messages
+2 -2
View File
@@ -1,6 +1,6 @@
# CLAUDE.md
**Document Version:** v2 (independent, incremented on structural changes)
**Document Version:** v3 (independent, incremented on structural changes)
## First-time setup
@@ -11,4 +11,4 @@
- `AGENTS.md`
- `PROJECT.md`
- `CHANGELOG.md`
- `DESIGN_DOCUMENT*.md` (`DESIGN_DOCUMENT.md`, `DESIGN_DOCUMENT_MODULE.md`, `DESIGN_DOCUMENT_LIB.md` or `DESIGN_DOCUMENT_GODOT.md`)
- `DESIGN_DOCUMENT*.md` — one per language used in the project (`DESIGN_DOCUMENT_PYTHON.md`, `DESIGN_DOCUMENT_PYTHON_MODULE.md`, `DESIGN_DOCUMENT_RUST.md`, `DESIGN_DOCUMENT_RUST_LIB.md`, `DESIGN_DOCUMENT_GODOT.md`)
@@ -1,6 +1,6 @@
# Python Development Guidelines
**Document Version:** v10
**Document Version:** v11
> **Note on Versioning:**
> - This document version is independent — reused across projects
@@ -19,7 +19,7 @@
All detailed documentation of features and systems belongs in the `docs/` folder, not in the project root.
The root directory contains only the core documents: `README.md`, `CLAUDE.md`, `AGENTS.md`, `DESIGN_DOCUMENT.md`, `PROJECT.md`, `CHANGELOG.md`.
The root directory contains only the core documents: `README.md`, `CLAUDE.md`, `AGENTS.md`, `DESIGN_DOCUMENT_PYTHON.md`, `PROJECT.md`, `CHANGELOG.md`.
---
@@ -1,6 +1,6 @@
# Python Library Development Guidelines
**Document Version:** v3
**Document Version:** v4
> **Note on Versioning:**
> - This document version is independent — reused across projects
@@ -18,7 +18,7 @@
All detailed documentation of features and systems belongs in the `docs/` folder, not in the project root.
The root directory contains only the core documents: `README.md`, `CLAUDE.md`, `AGENTS.md`, `DESIGN_DOCUMENT_MODULE.md`, `PROJECT.md`, `CHANGELOG.md`.
The root directory contains only the core documents: `README.md`, `CLAUDE.md`, `AGENTS.md`, `DESIGN_DOCUMENT_PYTHON_MODULE.md`, `PROJECT.md`, `CHANGELOG.md`.
---
+1 -1
View File
@@ -26,7 +26,7 @@ template/
└── test_constants.py # Basic test
```
`CLAUDE.md`, `AGENTS.md` and `DESIGN_DOCUMENT.md` are **not** part of the template — they are copied
`CLAUDE.md`, `AGENTS.md` and `DESIGN_DOCUMENT_PYTHON.md` are **not** part of the template — they are copied
into the project root from the documentation repository, which stays their source of truth. The
copies themselves **are committed** with the project and are never edited in place.
+25 -10
View File
@@ -21,8 +21,8 @@ Dokumentace/
│ └── documentation_context/ # /documentation_context — compress docs/ into CONTEXT.md
├── Python/ # Python development guidelines
│ ├── DESIGN_DOCUMENT.md # Guidelines for Python applications
│ ├── DESIGN_DOCUMENT_MODULE.md # Guidelines for Python libraries
│ ├── DESIGN_DOCUMENT_PYTHON.md # Guidelines for Python applications
│ ├── DESIGN_DOCUMENT_PYTHON_MODULE.md # Guidelines for Python libraries
│ ├── TEMPLATE.md # New project template specification
│ ├── .gitignore # Python-specific ignore rules
│ ├── prebuild.py # Pre-build script (PyInstaller)
@@ -30,8 +30,8 @@ Dokumentace/
│ └── tests/ # Tests for the reference module
├── Rust/ # Rust development guidelines
│ ├── DESIGN_DOCUMENT.md # Guidelines for Rust applications
│ ├── DESIGN_DOCUMENT_LIB.md # Guidelines for Rust libraries
│ ├── DESIGN_DOCUMENT_RUST.md # Guidelines for Rust applications
│ ├── DESIGN_DOCUMENT_RUST_LIB.md # Guidelines for Rust libraries
│ └── .gitignore # Rust-specific ignore rules
├── Godot/ # Godot development guidelines
@@ -62,10 +62,10 @@ Where the two disagree, **the design document wins**.
| Project type | Design document |
|--------------|-----------------|
| Python (application) | `Python/DESIGN_DOCUMENT.md` |
| Python (library) | `Python/DESIGN_DOCUMENT_MODULE.md` |
| Rust (application) | `Rust/DESIGN_DOCUMENT.md` |
| Rust (library) | `Rust/DESIGN_DOCUMENT_LIB.md` |
| Python (application) | `Python/DESIGN_DOCUMENT_PYTHON.md` |
| Python (library) | `Python/DESIGN_DOCUMENT_PYTHON_MODULE.md` |
| Rust (application) | `Rust/DESIGN_DOCUMENT_RUST.md` |
| Rust (library) | `Rust/DESIGN_DOCUMENT_RUST_LIB.md` |
| Godot | `Godot/DESIGN_DOCUMENT_GODOT.md` |
### Starting a new project
@@ -84,7 +84,7 @@ are **committed there**. They are duplicated on purpose:
guidelines are always "latest" and the pairing is lost.
- A fresh clone — another machine, CI, Claude Code on the web or a remote agent — has to work
standalone. `CLAUDE.md` and `AGENTS.md` are only picked up from the repository root.
- Drift becomes visible: `git log DESIGN_DOCUMENT.md` shows when a project was last synchronised.
- Drift becomes visible: `git log DESIGN_DOCUMENT_PYTHON.md` shows when a project was last synchronised.
Three rules keep the copies from rotting:
@@ -93,10 +93,25 @@ Three rules keep the copies from rotting:
session read list. That is also where extra files to read (for example a generated `CONTEXT.md`)
are named.
- **Sync commits stand alone**, in the form
`docs: sync guidelines to AGENTS v6 / DESIGN_DOCUMENT v9`. They do not go into the project
`docs: sync guidelines to AGENTS v8 / DESIGN_DOCUMENT_PYTHON v11`. They do not go into the project
`CHANGELOG.md` — they are not a change to the product.
Run `check_versions.py` from this repository to list every sibling project whose copies are behind.
Add `--sync` and it offers to overwrite the outdated copies with the master version, keeping each
file's existing line endings. Only copies that are purely behind or still carry a pre-rename name are
offered: one edited in place is reported as `MODIFIED` and left alone, because copying would silently
discard the local change.
A project still holding `DESIGN_DOCUMENT.md`, `DESIGN_DOCUMENT_MODULE.md` or `DESIGN_DOCUMENT_LIB.md`
from before the rename is reported as `OLD NAME`, and `--sync` renames it on the way. If both the old
and the new name exist the copy is flagged `DUPLICATE` and left for you to resolve.
```bash
python check_versions.py # report only
python check_versions.py --sync # report, then ask before copying
python check_versions.py --sync --dry-run # show what would be copied, change nothing
python check_versions.py --sync --yes # copy without asking, for scripts
```
### Skills
@@ -1,6 +1,6 @@
# Rust Application Development Guidelines
**Document Version:** v3
**Document Version:** v4
> **Note on Versioning:**
> - This document version is independent — reused across projects
@@ -18,7 +18,7 @@
All detailed documentation of features and systems belongs in the `docs/` folder, not in the project root.
The root directory contains only the core documents: `README.md`, `CLAUDE.md`, `AGENTS.md`, `DESIGN_DOCUMENT.md`, `PROJECT.md`, `CHANGELOG.md`.
The root directory contains only the core documents: `README.md`, `CLAUDE.md`, `AGENTS.md`, `DESIGN_DOCUMENT_RUST.md`, `PROJECT.md`, `CHANGELOG.md`.
---
@@ -1,6 +1,6 @@
# Rust Library Development Guidelines
**Document Version:** v3
**Document Version:** v4
> **Note on Versioning:**
> - This document version is independent — reused across projects
@@ -20,7 +20,7 @@ All detailed documentation of features and systems belongs in the `docs/` folder
The API reference is generated by `cargo doc` from doc comments — `docs/` holds the prose documentation that does not fit in doc comments.
The root directory contains only the core documents: `README.md`, `CLAUDE.md`, `AGENTS.md`, `DESIGN_DOCUMENT_LIB.md`, `PROJECT.md`, `CHANGELOG.md`.
The root directory contains only the core documents: `README.md`, `CLAUDE.md`, `AGENTS.md`, `DESIGN_DOCUMENT_RUST_LIB.md`, `PROJECT.md`, `CHANGELOG.md`.
---
+199 -27
View File
@@ -1,4 +1,4 @@
"""Report sibling projects whose copies of the guideline documents are out of date.
"""Report sibling projects whose copies of the guideline documents are out of date, and offer to sync them.
This repository is the source of truth for ``CLAUDE.md``, ``AGENTS.md`` and the
``DESIGN_DOCUMENT*.md`` family. Every project keeps a committed copy in its root, so the copies
@@ -6,9 +6,20 @@ drift as soon as a document here is bumped. This script walks the sibling direct
repository, reads the ``Document Version`` header of every copy it finds and reports the ones that
are behind, missing, or modified locally.
It also finds copies still using a pre-rename file name (``DESIGN_DOCUMENT.md`` and friends, from
before the language became part of the name) and reports them as ``OLD NAME``.
``--sync`` offers to overwrite the outdated copies with the master version, renaming the old names on
the way. Only ``BEHIND`` and ``OLD NAME`` copies are ever touched: a ``MODIFIED`` copy holds local
edits that overwriting would discard, an unversioned copy cannot be compared, and ``DUPLICATE`` means
both the old and the new name exist, which needs a human to decide.
Standard library only, so it runs without a virtual environment:
python check_versions.py
python check_versions.py # report only
python check_versions.py --sync # report, then offer to copy the outdated ones
python check_versions.py --sync --yes # copy without asking (for scripts)
python check_versions.py --sync --dry-run # show what --sync would copy, change nothing
python check_versions.py --root .. # scan a different directory
python check_versions.py --quiet # print only projects needing attention
"""
@@ -16,24 +27,37 @@ Standard library only, so it runs without a virtual environment:
from __future__ import annotations
import argparse
import os
import re
import shutil
import sys
from dataclasses import dataclass
from pathlib import Path
# Guideline documents owned by this repository, mapped to their master copy.
# Guideline documents owned by this repository, mapped to their master copy. The language is part of
# every design document name, so a project may carry several of them without any collision.
MASTERS: dict[str, str] = {
"CLAUDE.md": "Claude/CLAUDE.md",
"AGENTS.md": "Claude/AGENTS.md",
"DESIGN_DOCUMENT.md": "Python/DESIGN_DOCUMENT.md",
"DESIGN_DOCUMENT_MODULE.md": "Python/DESIGN_DOCUMENT_MODULE.md",
"DESIGN_DOCUMENT_LIB.md": "Rust/DESIGN_DOCUMENT_LIB.md",
"DESIGN_DOCUMENT_PYTHON.md": "Python/DESIGN_DOCUMENT_PYTHON.md",
"DESIGN_DOCUMENT_PYTHON_MODULE.md": "Python/DESIGN_DOCUMENT_PYTHON_MODULE.md",
"DESIGN_DOCUMENT_RUST.md": "Rust/DESIGN_DOCUMENT_RUST.md",
"DESIGN_DOCUMENT_RUST_LIB.md": "Rust/DESIGN_DOCUMENT_RUST_LIB.md",
"DESIGN_DOCUMENT_GODOT.md": "Godot/DESIGN_DOCUMENT_GODOT.md",
}
# A Rust application uses the same file name as a Python one; the language decides which master
# applies. Detected from the project manifest.
RUST_OVERRIDES: dict[str, str] = {"DESIGN_DOCUMENT.md": "Rust/DESIGN_DOCUMENT.md"}
# Names used before the language became part of the file name. A project still carrying one of these
# is reported as LEGACY and renamed by --sync. This mapping is migration code: once every project has
# been synchronised it can be deleted, and with it the last place where the old collision matters.
LEGACY_NAMES: dict[str, str] = {
"DESIGN_DOCUMENT.md": "DESIGN_DOCUMENT_PYTHON.md",
"DESIGN_DOCUMENT_MODULE.md": "DESIGN_DOCUMENT_PYTHON_MODULE.md",
"DESIGN_DOCUMENT_LIB.md": "DESIGN_DOCUMENT_RUST_LIB.md",
}
# The old DESIGN_DOCUMENT.md is ambiguous on its own - Python and Rust applications shared the name.
# Only the project manifest can tell them apart, and only for legacy copies.
LEGACY_RUST_NAMES: dict[str, str] = {"DESIGN_DOCUMENT.md": "DESIGN_DOCUMENT_RUST.md"}
VERSION_PATTERN = re.compile(r"^\*\*Document Version:\*\*\s*(v\d+)", re.MULTILINE)
@@ -41,6 +65,8 @@ STATUS_OK = "ok"
STATUS_BEHIND = "behind"
STATUS_MODIFIED = "modified"
STATUS_UNVERSIONED = "unversioned"
STATUS_LEGACY = "legacy"
STATUS_DUPLICATE = "duplicate"
@dataclass(frozen=True)
@@ -52,11 +78,23 @@ class Result:
project_version: str | None
master_version: str | None
status: str
copy_path: Path
master_path: Path
target_path: Path
@property
def needs_attention(self) -> bool:
return self.status != STATUS_OK
@property
def can_sync(self) -> bool:
"""Behind and legacy copies can be replaced; anything else may hold work we would destroy."""
return self.status in {STATUS_BEHIND, STATUS_LEGACY}
@property
def is_rename(self) -> bool:
return self.copy_path != self.target_path
def read_text(path: Path) -> str:
"""Read a document, tolerating the BOM some editors leave behind."""
@@ -83,6 +121,42 @@ def is_rust_project(project: Path) -> bool:
return (project / "Cargo.toml").is_file()
def detect_newline(path: Path) -> str:
"""Return the line ending a file already uses, so rewriting it does not churn every line."""
data = path.read_bytes()
crlf = data.count(b"\r\n")
lf = data.count(b"\n") - crlf
if crlf and crlf >= lf:
return "\r\n"
if lf:
return "\n"
return os.linesep
def sync_document(master: Path, copy: Path, target: Path) -> None:
"""Overwrite a project copy with its master, renaming it first when the file name changed.
The copy keeps the line endings it already used: writing the master's bytes verbatim would flip
every line on a project that uses the other convention, turning a one-line version bump into a
whole-file diff. Renaming before writing lets git see one rename instead of a delete plus an add.
"""
newline = detect_newline(copy)
backup = copy.with_suffix(copy.suffix + ".bak")
shutil.copyfile(copy, backup)
renamed = False
try:
if copy != target:
os.replace(copy, target)
renamed = True
target.write_text(normalise(read_text(master)) + "\n", encoding="utf-8", newline=newline)
except OSError:
if renamed and target.exists():
os.replace(target, copy)
shutil.copyfile(backup, copy)
raise
backup.unlink()
def load_masters(repo: Path) -> dict[str, tuple[Path, str | None]]:
"""Map each document name to its master path and version."""
masters: dict[str, tuple[Path, str | None]] = {}
@@ -93,20 +167,15 @@ def load_masters(repo: Path) -> dict[str, tuple[Path, str | None]]:
return masters
def check_project(project: Path, repo: Path, masters: dict[str, tuple[Path, str | None]]) -> list[Result]:
def check_project(project: Path, masters: dict[str, tuple[Path, str | None]]) -> list[Result]:
"""Compare every guideline document present in one project against its master."""
results: list[Result] = []
rust = is_rust_project(project)
for name, (master_path, master_version) in masters.items():
copy = project / name
if not copy.is_file():
continue
if rust and name in RUST_OVERRIDES:
master_path = repo / RUST_OVERRIDES[name]
master_version = read_version(master_path)
copy_version = read_version(copy)
if copy_version is None or master_version is None:
@@ -114,12 +183,40 @@ def check_project(project: Path, repo: Path, masters: dict[str, tuple[Path, str
elif version_key(copy_version) < version_key(master_version):
status = STATUS_BEHIND
elif normalise(read_text(copy)) != normalise(read_text(master_path)):
# Same version number but different content someone edited the copy in place.
# Same version number but different content - someone edited the copy in place.
status = STATUS_MODIFIED
else:
status = STATUS_OK
results.append(Result(project.name, name, copy_version, master_version, status))
results.append(Result(project.name, name, copy_version, master_version, status, copy, master_path, copy))
results.extend(check_legacy(project, masters))
return results
def check_legacy(project: Path, masters: dict[str, tuple[Path, str | None]]) -> list[Result]:
"""Report copies still using a pre-rename file name, and where each one should move to."""
results: list[Result] = []
rust = is_rust_project(project)
for old_name, new_name in LEGACY_NAMES.items():
copy = project / old_name
if not copy.is_file():
continue
if rust and old_name in LEGACY_RUST_NAMES:
new_name = LEGACY_RUST_NAMES[old_name]
target = project / new_name
master_path, master_version = masters[new_name]
# Both names present: the rename already happened and the old file is a leftover. Removing
# it is not this script's call, so only report it.
status = STATUS_DUPLICATE if target.is_file() else STATUS_LEGACY
label = f"{old_name} -> {new_name}"
results.append(
Result(project.name, label, read_version(copy), master_version, status, copy, master_path, target)
)
return results
@@ -139,9 +236,62 @@ def format_row(result: Result) -> str:
STATUS_BEHIND: "BEHIND ",
STATUS_MODIFIED: "MODIFIED ",
STATUS_UNVERSIONED: "NO VER. ",
STATUS_LEGACY: "OLD NAME ",
STATUS_DUPLICATE: "DUPLICATE",
}[result.status]
versions = f"{result.project_version or '-'} -> {result.master_version or '-'}"
return f" {marker} {result.document:<26} {versions}"
return f" {marker} {result.document:<50} {versions}"
def confirm(prompt: str) -> bool:
"""Ask a yes/no question. Anything other than an explicit yes - including a closed stdin - is no."""
if not sys.stdin or not sys.stdin.isatty():
print(f"{prompt} [y/N] (stdin is not a terminal, assuming no; use --yes to copy)")
return False
try:
return input(f"{prompt} [y/N] ").strip().lower() in {"y", "yes"}
except (EOFError, KeyboardInterrupt):
print()
return False
def run_sync(pending: list[Result], assume_yes: bool, dry_run: bool) -> int:
"""Copy the masters over the outdated project copies. Returns the number of documents left stale."""
renames = sum(1 for result in pending if result.is_rename)
print(f"{len(pending)} document(s) can be updated from this repository:")
labels = {result: f"{result.project}/{result.document}" for result in pending}
width = max(len(label) for label in labels.values())
for result in pending:
note = " (rename)" if result.is_rename else ""
print(f" {labels[result]:<{width}} {result.project_version} -> {result.master_version}{note}")
print()
if dry_run:
print("Dry run - nothing was written.")
return len(pending)
action = f"Overwrite {len(pending)} file(s)"
if renames:
action += f", renaming {renames} of them"
if not assume_yes and not confirm(f"{action}?"):
print("Nothing was written.")
return len(pending)
failed = 0
for result in pending:
try:
sync_document(result.master_path, result.copy_path, result.target_path)
except OSError as error:
print(f" FAILED {result.project}/{result.document}: {error}", file=sys.stderr)
failed += 1
else:
verb = "renamed" if result.is_rename else "updated"
print(f" {verb} {result.project}/{result.target_path.name} -> {result.master_version}")
print()
print(f"{len(pending) - failed} document(s) updated." if not failed else f"{failed} document(s) failed to update.")
print("Review and commit the changes in each project (a sync is its own commit).")
return failed
def main() -> int:
@@ -153,6 +303,9 @@ def main() -> int:
help="directory holding the projects (default: the parent of this repository)",
)
parser.add_argument("--quiet", action="store_true", help="list only projects that need attention")
parser.add_argument("--sync", action="store_true", help="offer to overwrite outdated copies with the master")
parser.add_argument("--yes", action="store_true", help="with --sync, copy without asking for confirmation")
parser.add_argument("--dry-run", action="store_true", help="with --sync, show what would be copied and stop")
args = parser.parse_args()
repo = Path(__file__).resolve().parent
@@ -164,27 +317,28 @@ def main() -> int:
return 2
print(f"Master documents in {repo.name}:")
width = max(len(name) for name in masters)
for name, (_, version) in sorted(masters.items()):
print(f" {name:<26} {version or '(no version header)'}")
print(f" {name:<{width}} {version or '(no version header)'}")
print()
stale = 0
attention: list[Result] = []
unmanaged: list[str] = []
for project in find_projects(root, repo):
results = check_project(project, repo, masters)
results = check_project(project, masters)
if not results:
unmanaged.append(project.name)
continue
attention = [result for result in results if result.needs_attention]
stale += len(attention)
stale = [result for result in results if result.needs_attention]
attention.extend(stale)
if args.quiet and not attention:
if args.quiet and not stale:
continue
print(f"{project.name}")
for result in results if not args.quiet else attention:
for result in results if not args.quiet else stale:
print(format_row(result))
print()
@@ -192,8 +346,26 @@ def main() -> int:
print(f"No guideline documents: {', '.join(unmanaged)}")
print()
if stale:
print(f"{stale} document(s) need attention.")
if not attention:
print("All projects are up to date.")
return 0
syncable = [result for result in attention if result.can_sync]
blocked = len(attention) - len(syncable)
if args.sync and syncable:
remaining = run_sync(syncable, args.yes, args.dry_run) + blocked
else:
if syncable and not args.sync:
print(f"Run with --sync to copy {len(syncable)} outdated document(s) from this repository.")
remaining = len(attention)
if blocked:
print(f"{blocked} document(s) left alone - MODIFIED holds local edits, NO VER. cannot be compared,")
print("and DUPLICATE means both the old and the new file name exist; remove the old one by hand.")
if remaining:
print(f"{remaining} document(s) need attention.")
return 1
print("All projects are up to date.")