# One-shot header-update script

**Status:** Draft. Run after OTC formalizes the joint-copyright model.

This script rewrites every source-file copyright header in the
repository to the agreed joint form in a single pass. It is
idempotent — running it twice produces no changes the second time.

The script does not commit; it leaves changes in the working tree
for review before a single sweep PR.

---

## Behavior

- Walks every tracked file under `axiom-os/` matching extensions:
  `.py`, `.toml` (only ones containing a copyright comment),
  `.md` (front-matter copyright lines), `.sh`, `.yaml`, `.yml`.
- Replaces a leading `# Copyright (c) <YEAR> B-Tree Ventures, LLC`
  block (in any of its current variants) with the joint block.
- Preserves the SPDX line.
- For Substrate-derived files (identified by an in-file marker
  comment `# Origin: Substrate` — to be added by hand to the few
  files that qualify), prepends a Substrate copyright line above
  the joint lines.
- Leaves Dendra files alone (they live in a separate repo; not in
  scope).
- Skips third-party vendored code (currently none, but defensive).

## The script

```python
#!/usr/bin/env python3
# Copyright (c) 2026 The University of Texas at Austin
# Copyright (c) 2026 B-Tree Ventures, LLC (DBA B-Tree Labs)
# SPDX-License-Identifier: Apache-2.0
"""One-shot copyright-header rewrite.

Run after UT OTC formalizes the joint-copyright model. Walks every
tracked source file, rewrites the header to the agreed form, and
leaves changes in the working tree for review.

Usage:
    python scripts/rewrite-copyright-headers.py [--dry-run]

Sequencing:
    1. UT OTC formalizes the joint-copyright model.
    2. Update /NOTICE to the proposed form (artifact 02).
    3. Update docs/working/ownership-map.md (artifact 03).
    4. Run this script.
    5. `git diff` review.
    6. Single sweep commit: `chore(license): apply joint UT + B-Tree
       copyright headers per OTC guidance`.
"""

from __future__ import annotations

import argparse
import re
import subprocess
import sys
from pathlib import Path

JOINT_HEADER_PY = """\
# Copyright (c) {year} The University of Texas at Austin
# Copyright (c) {year} B-Tree Ventures, LLC (DBA B-Tree Labs)
# SPDX-License-Identifier: Apache-2.0
"""

SUBSTRATE_PREFIX_PY = "# Copyright (c) 2025 B-Tree Ventures, LLC          # original (Substrate)\n"

# Match an existing single-party B-Tree copyright block at file head.
_RE_OLD_BTREE_HEADER = re.compile(
    r"^# Copyright \(c\) (?P<year>\d{4}) B-Tree Ventures,? LLC.*?\n"
    r"# SPDX-License-Identifier: Apache-2\.0\n",
    re.MULTILINE,
)

# Match the joint header (so we can detect already-rewritten files for idempotency).
_RE_JOINT_HEADER = re.compile(
    r"# Copyright \(c\) \d{4} The University of Texas at Austin\n"
    r"# Copyright \(c\) \d{4} B-Tree Ventures, LLC \(DBA B-Tree Labs\)\n"
    r"# SPDX-License-Identifier: Apache-2\.0\n",
)

SUBSTRATE_MARKER = "# Origin: Substrate"  # add by hand to ported files


def file_creation_year(path: Path) -> str:
    """Return YYYY of the first commit that introduced this file. Falls back to 2026."""
    try:
        out = subprocess.run(
            ["git", "log", "--diff-filter=A", "--format=%ad", "--date=format:%Y", str(path)],
            capture_output=True, text=True, check=True,
        ).stdout.strip()
        first = out.splitlines()[-1] if out else ""
        return first or "2026"
    except subprocess.CalledProcessError:
        return "2026"


def rewrite_python(path: Path, content: str) -> str | None:
    """Return rewritten content, or None if no change needed."""
    # Already joint? — skip
    if _RE_JOINT_HEADER.search(content):
        return None

    # Has the old single-party B-Tree header? — replace
    m = _RE_OLD_BTREE_HEADER.search(content)
    if not m:
        return None

    year = file_creation_year(path) or m.group("year")
    new_header = JOINT_HEADER_PY.format(year=year)

    # Substrate-derived file? — prepend the original Substrate line
    if SUBSTRATE_MARKER in content:
        new_header = SUBSTRATE_PREFIX_PY + new_header

    return content[: m.start()] + new_header + content[m.end():]


def tracked_files(root: Path) -> list[Path]:
    out = subprocess.run(
        ["git", "ls-files"], capture_output=True, text=True, check=True, cwd=root,
    ).stdout.splitlines()
    return [root / p for p in out if p.endswith(".py")]


def main() -> int:
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument("--dry-run", action="store_true", help="Don't write files; print would-change list.")
    args = p.parse_args()

    repo_root = Path(__file__).resolve().parents[1]
    changed: list[Path] = []
    skipped: list[Path] = []

    for path in tracked_files(repo_root):
        try:
            content = path.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError):
            skipped.append(path)
            continue

        new = rewrite_python(path, content)
        if new is None:
            continue

        if args.dry_run:
            changed.append(path)
        else:
            path.write_text(new, encoding="utf-8")
            changed.append(path)

    print(f"Updated {len(changed)} files" + (" (dry run)" if args.dry_run else ""))
    if skipped:
        print(f"Skipped {len(skipped)} (binary or unreadable)", file=sys.stderr)
    return 0


if __name__ == "__main__":
    sys.exit(main())
```

## Caveats and limits

- **Python only**: covers `.py`. Other languages (`.toml`, `.sh`,
  `.yaml`) need their own comment-syntax handlers; add as needed.
- **Substrate marker is manual**: we don't auto-detect Substrate
  lineage. Drop `# Origin: Substrate` into the few ported files
  (estimate <30) before running the script.
- **Year inference uses git log**: the year of first introduction is
  preserved, so 2025-era files don't drift to 2026.
- **The script is idempotent**: re-running on an already-converted
  file is a no-op.
- **Does NOT commit**: by design. Review the `git diff` before
  staging the sweep commit.
- **Does NOT modify Dendra**: Dendra is in a separate repo. If any
  Dendra code is ever imported into this repo, that file gets a
  B-Tree-only header by hand, with a NOTICE entry.

## Sweep PR title and body

```
chore(license): apply joint UT + B-Tree copyright headers per OTC guidance

Per the joint-copyright model formalized by UT OTC on YYYY-MM-DD,
all source-file copyright headers are updated to:

    # Copyright (c) <YEAR> The University of Texas at Austin
    # Copyright (c) <YEAR> B-Tree Ventures, LLC (DBA B-Tree Labs)
    # SPDX-License-Identifier: Apache-2.0

Substrate-ported files retain the original 2025 B-Tree Ventures
copyright above the joint lines.

NOTICE and docs/working/ownership-map.md are updated to match.
LICENSE (Apache-2.0) is unchanged.

No functional changes. CI must pass on the sweep alone.

Co-Authored-By: Benjamin Booth <bbooth@utexas.edu>
```
