Put the language in design document names and add a sync mode
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
__pycache__/
|
||||||
@@ -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
|
Individual guideline documents carry their own `Document Version` header, and the skills carry
|
||||||
`metadata.version`; both are independent of the revisions below.
|
`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
|
## v5 — 2026-08-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+10
-7
@@ -1,6 +1,6 @@
|
|||||||
# AI Agents - Project Rules
|
# 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.).
|
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:
|
logging library, line length, project layout — lives in the matching design document:
|
||||||
|
|
||||||
| Project type | Design document |
|
| Project type | Design document |
|
||||||
|--------------------|------------------------------|
|
|--------------------|------------------------------------|
|
||||||
| Python application | `DESIGN_DOCUMENT.md` |
|
| Python application | `DESIGN_DOCUMENT_PYTHON.md` |
|
||||||
| Python library | `DESIGN_DOCUMENT_MODULE.md` |
|
| Python library | `DESIGN_DOCUMENT_PYTHON_MODULE.md` |
|
||||||
| Rust application | `DESIGN_DOCUMENT.md` |
|
| Rust application | `DESIGN_DOCUMENT_RUST.md` |
|
||||||
| Rust library | `DESIGN_DOCUMENT_LIB.md` |
|
| Rust library | `DESIGN_DOCUMENT_RUST_LIB.md` |
|
||||||
| Godot | `DESIGN_DOCUMENT_GODOT.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**.
|
Where this file and a design document disagree, **the design document wins**.
|
||||||
|
|
||||||
## First-time setup
|
## 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**
|
- **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.
|
- **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`.
|
- **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
|
### Commit messages
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
**Document Version:** v2 (independent, incremented on structural changes)
|
**Document Version:** v3 (independent, incremented on structural changes)
|
||||||
|
|
||||||
## First-time setup
|
## First-time setup
|
||||||
|
|
||||||
@@ -11,4 +11,4 @@
|
|||||||
- `AGENTS.md`
|
- `AGENTS.md`
|
||||||
- `PROJECT.md`
|
- `PROJECT.md`
|
||||||
- `CHANGELOG.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
|
# Python Development Guidelines
|
||||||
|
|
||||||
**Document Version:** v10
|
**Document Version:** v11
|
||||||
|
|
||||||
> **Note on Versioning:**
|
> **Note on Versioning:**
|
||||||
> - This document version is independent — reused across projects
|
> - 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.
|
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
|
# Python Library Development Guidelines
|
||||||
|
|
||||||
**Document Version:** v3
|
**Document Version:** v4
|
||||||
|
|
||||||
> **Note on Versioning:**
|
> **Note on Versioning:**
|
||||||
> - This document version is independent — reused across projects
|
> - 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.
|
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
@@ -26,7 +26,7 @@ template/
|
|||||||
└── test_constants.py # Basic test
|
└── 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
|
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.
|
copies themselves **are committed** with the project and are never edited in place.
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ Dokumentace/
|
|||||||
│ └── documentation_context/ # /documentation_context — compress docs/ into CONTEXT.md
|
│ └── documentation_context/ # /documentation_context — compress docs/ into CONTEXT.md
|
||||||
│
|
│
|
||||||
├── Python/ # Python development guidelines
|
├── Python/ # Python development guidelines
|
||||||
│ ├── DESIGN_DOCUMENT.md # Guidelines for Python applications
|
│ ├── DESIGN_DOCUMENT_PYTHON.md # Guidelines for Python applications
|
||||||
│ ├── DESIGN_DOCUMENT_MODULE.md # Guidelines for Python libraries
|
│ ├── DESIGN_DOCUMENT_PYTHON_MODULE.md # Guidelines for Python libraries
|
||||||
│ ├── TEMPLATE.md # New project template specification
|
│ ├── TEMPLATE.md # New project template specification
|
||||||
│ ├── .gitignore # Python-specific ignore rules
|
│ ├── .gitignore # Python-specific ignore rules
|
||||||
│ ├── prebuild.py # Pre-build script (PyInstaller)
|
│ ├── prebuild.py # Pre-build script (PyInstaller)
|
||||||
@@ -30,8 +30,8 @@ Dokumentace/
|
|||||||
│ └── tests/ # Tests for the reference module
|
│ └── tests/ # Tests for the reference module
|
||||||
│
|
│
|
||||||
├── Rust/ # Rust development guidelines
|
├── Rust/ # Rust development guidelines
|
||||||
│ ├── DESIGN_DOCUMENT.md # Guidelines for Rust applications
|
│ ├── DESIGN_DOCUMENT_RUST.md # Guidelines for Rust applications
|
||||||
│ ├── DESIGN_DOCUMENT_LIB.md # Guidelines for Rust libraries
|
│ ├── DESIGN_DOCUMENT_RUST_LIB.md # Guidelines for Rust libraries
|
||||||
│ └── .gitignore # Rust-specific ignore rules
|
│ └── .gitignore # Rust-specific ignore rules
|
||||||
│
|
│
|
||||||
├── Godot/ # Godot development guidelines
|
├── Godot/ # Godot development guidelines
|
||||||
@@ -62,10 +62,10 @@ Where the two disagree, **the design document wins**.
|
|||||||
|
|
||||||
| Project type | Design document |
|
| Project type | Design document |
|
||||||
|--------------|-----------------|
|
|--------------|-----------------|
|
||||||
| Python (application) | `Python/DESIGN_DOCUMENT.md` |
|
| Python (application) | `Python/DESIGN_DOCUMENT_PYTHON.md` |
|
||||||
| Python (library) | `Python/DESIGN_DOCUMENT_MODULE.md` |
|
| Python (library) | `Python/DESIGN_DOCUMENT_PYTHON_MODULE.md` |
|
||||||
| Rust (application) | `Rust/DESIGN_DOCUMENT.md` |
|
| Rust (application) | `Rust/DESIGN_DOCUMENT_RUST.md` |
|
||||||
| Rust (library) | `Rust/DESIGN_DOCUMENT_LIB.md` |
|
| Rust (library) | `Rust/DESIGN_DOCUMENT_RUST_LIB.md` |
|
||||||
| Godot | `Godot/DESIGN_DOCUMENT_GODOT.md` |
|
| Godot | `Godot/DESIGN_DOCUMENT_GODOT.md` |
|
||||||
|
|
||||||
### Starting a new project
|
### 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.
|
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
|
- 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.
|
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:
|
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`)
|
session read list. That is also where extra files to read (for example a generated `CONTEXT.md`)
|
||||||
are named.
|
are named.
|
||||||
- **Sync commits stand alone**, in the form
|
- **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.
|
`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.
|
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
|
### Skills
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Rust Application Development Guidelines
|
# Rust Application Development Guidelines
|
||||||
|
|
||||||
**Document Version:** v3
|
**Document Version:** v4
|
||||||
|
|
||||||
> **Note on Versioning:**
|
> **Note on Versioning:**
|
||||||
> - This document version is independent — reused across projects
|
> - 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.
|
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
|
# Rust Library Development Guidelines
|
||||||
|
|
||||||
**Document Version:** v3
|
**Document Version:** v4
|
||||||
|
|
||||||
> **Note on Versioning:**
|
> **Note on Versioning:**
|
||||||
> - This document version is independent — reused across projects
|
> - 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 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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
+200
-28
@@ -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
|
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
|
``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
|
repository, reads the ``Document Version`` header of every copy it finds and reports the ones that
|
||||||
are behind, missing, or modified locally.
|
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:
|
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 --root .. # scan a different directory
|
||||||
python check_versions.py --quiet # print only projects needing attention
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
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] = {
|
MASTERS: dict[str, str] = {
|
||||||
"CLAUDE.md": "Claude/CLAUDE.md",
|
"CLAUDE.md": "Claude/CLAUDE.md",
|
||||||
"AGENTS.md": "Claude/AGENTS.md",
|
"AGENTS.md": "Claude/AGENTS.md",
|
||||||
"DESIGN_DOCUMENT.md": "Python/DESIGN_DOCUMENT.md",
|
"DESIGN_DOCUMENT_PYTHON.md": "Python/DESIGN_DOCUMENT_PYTHON.md",
|
||||||
"DESIGN_DOCUMENT_MODULE.md": "Python/DESIGN_DOCUMENT_MODULE.md",
|
"DESIGN_DOCUMENT_PYTHON_MODULE.md": "Python/DESIGN_DOCUMENT_PYTHON_MODULE.md",
|
||||||
"DESIGN_DOCUMENT_LIB.md": "Rust/DESIGN_DOCUMENT_LIB.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",
|
"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
|
# Names used before the language became part of the file name. A project still carrying one of these
|
||||||
# applies. Detected from the project manifest.
|
# is reported as LEGACY and renamed by --sync. This mapping is migration code: once every project has
|
||||||
RUST_OVERRIDES: dict[str, str] = {"DESIGN_DOCUMENT.md": "Rust/DESIGN_DOCUMENT.md"}
|
# 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)
|
VERSION_PATTERN = re.compile(r"^\*\*Document Version:\*\*\s*(v\d+)", re.MULTILINE)
|
||||||
|
|
||||||
@@ -41,6 +65,8 @@ STATUS_OK = "ok"
|
|||||||
STATUS_BEHIND = "behind"
|
STATUS_BEHIND = "behind"
|
||||||
STATUS_MODIFIED = "modified"
|
STATUS_MODIFIED = "modified"
|
||||||
STATUS_UNVERSIONED = "unversioned"
|
STATUS_UNVERSIONED = "unversioned"
|
||||||
|
STATUS_LEGACY = "legacy"
|
||||||
|
STATUS_DUPLICATE = "duplicate"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -52,11 +78,23 @@ class Result:
|
|||||||
project_version: str | None
|
project_version: str | None
|
||||||
master_version: str | None
|
master_version: str | None
|
||||||
status: str
|
status: str
|
||||||
|
copy_path: Path
|
||||||
|
master_path: Path
|
||||||
|
target_path: Path
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needs_attention(self) -> bool:
|
def needs_attention(self) -> bool:
|
||||||
return self.status != STATUS_OK
|
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:
|
def read_text(path: Path) -> str:
|
||||||
"""Read a document, tolerating the BOM some editors leave behind."""
|
"""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()
|
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]]:
|
def load_masters(repo: Path) -> dict[str, tuple[Path, str | None]]:
|
||||||
"""Map each document name to its master path and version."""
|
"""Map each document name to its master path and version."""
|
||||||
masters: dict[str, tuple[Path, str | None]] = {}
|
masters: dict[str, tuple[Path, str | None]] = {}
|
||||||
@@ -93,20 +167,15 @@ def load_masters(repo: Path) -> dict[str, tuple[Path, str | None]]:
|
|||||||
return masters
|
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."""
|
"""Compare every guideline document present in one project against its master."""
|
||||||
results: list[Result] = []
|
results: list[Result] = []
|
||||||
rust = is_rust_project(project)
|
|
||||||
|
|
||||||
for name, (master_path, master_version) in masters.items():
|
for name, (master_path, master_version) in masters.items():
|
||||||
copy = project / name
|
copy = project / name
|
||||||
if not copy.is_file():
|
if not copy.is_file():
|
||||||
continue
|
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)
|
copy_version = read_version(copy)
|
||||||
|
|
||||||
if copy_version is None or master_version is None:
|
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):
|
elif version_key(copy_version) < version_key(master_version):
|
||||||
status = STATUS_BEHIND
|
status = STATUS_BEHIND
|
||||||
elif normalise(read_text(copy)) != normalise(read_text(master_path)):
|
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
|
status = STATUS_MODIFIED
|
||||||
else:
|
else:
|
||||||
status = STATUS_OK
|
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
|
return results
|
||||||
|
|
||||||
@@ -137,11 +234,64 @@ def format_row(result: Result) -> str:
|
|||||||
marker = {
|
marker = {
|
||||||
STATUS_OK: "ok ",
|
STATUS_OK: "ok ",
|
||||||
STATUS_BEHIND: "BEHIND ",
|
STATUS_BEHIND: "BEHIND ",
|
||||||
STATUS_MODIFIED: "MODIFIED",
|
STATUS_MODIFIED: "MODIFIED ",
|
||||||
STATUS_UNVERSIONED: "NO VER. ",
|
STATUS_UNVERSIONED: "NO VER. ",
|
||||||
|
STATUS_LEGACY: "OLD NAME ",
|
||||||
|
STATUS_DUPLICATE: "DUPLICATE",
|
||||||
}[result.status]
|
}[result.status]
|
||||||
versions = f"{result.project_version or '-'} -> {result.master_version or '-'}"
|
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:
|
def main() -> int:
|
||||||
@@ -153,6 +303,9 @@ def main() -> int:
|
|||||||
help="directory holding the projects (default: the parent of this repository)",
|
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("--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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
repo = Path(__file__).resolve().parent
|
repo = Path(__file__).resolve().parent
|
||||||
@@ -164,27 +317,28 @@ def main() -> int:
|
|||||||
return 2
|
return 2
|
||||||
|
|
||||||
print(f"Master documents in {repo.name}:")
|
print(f"Master documents in {repo.name}:")
|
||||||
|
width = max(len(name) for name in masters)
|
||||||
for name, (_, version) in sorted(masters.items()):
|
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()
|
print()
|
||||||
|
|
||||||
stale = 0
|
attention: list[Result] = []
|
||||||
unmanaged: list[str] = []
|
unmanaged: list[str] = []
|
||||||
|
|
||||||
for project in find_projects(root, repo):
|
for project in find_projects(root, repo):
|
||||||
results = check_project(project, repo, masters)
|
results = check_project(project, masters)
|
||||||
if not results:
|
if not results:
|
||||||
unmanaged.append(project.name)
|
unmanaged.append(project.name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
attention = [result for result in results if result.needs_attention]
|
stale = [result for result in results if result.needs_attention]
|
||||||
stale += len(attention)
|
attention.extend(stale)
|
||||||
|
|
||||||
if args.quiet and not attention:
|
if args.quiet and not stale:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(f"{project.name}")
|
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(format_row(result))
|
||||||
print()
|
print()
|
||||||
|
|
||||||
@@ -192,8 +346,26 @@ def main() -> int:
|
|||||||
print(f"No guideline documents: {', '.join(unmanaged)}")
|
print(f"No guideline documents: {', '.join(unmanaged)}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
if stale:
|
if not attention:
|
||||||
print(f"{stale} document(s) need 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
|
return 1
|
||||||
|
|
||||||
print("All projects are up to date.")
|
print("All projects are up to date.")
|
||||||
|
|||||||
Reference in New Issue
Block a user