Standalone CLI utility that implements GitHub CODEOWNERS projection
Generate a GitHub CODEOWNERS file from the role-based oversight metadata in
.accountability/surfaces.toml.
The surfaces file is the advisory rationale: it names each accountability
surface, the paths it covers, and the oversight_role responsible for it.
se-codeowners resolves each role to a GitHub handle or team via
[codeowners.role_handles] and emits the corresponding CODEOWNERS entries.
The generated CODEOWNERS file provides GitHub review routing.
It becomes a merge gate only when repository branch protection or
rulesets require code-owner review.
uv tool install se-codeownersUses the standard-library tomllib. No runtime dependencies.
uvx se-codeowners --help# Render to stdout
uv run se-codeowners generate
# Write the file
uv run se-codeowners generate --output .github/CODEOWNERS
# Fail if any role still maps to a placeholder handle (e.g. `@OWNER`):
uv run se-codeowners generate --strict --output .github/CODEOWNERS
# Verify
uv run se-codeowners checkBoth subcommands accept --surfaces PATH; check also accepts
--codeowners PATH. The defaults are .accountability/surfaces.toml and
.github/CODEOWNERS.
[codeowners]
informs = true # opt in; generation refuses without an entry
requires_code_owner_review = false # opt in; generation refuses without an entry
[codeowners.role_handles]
owner = "@octocat" # a single handle
maintainer = ["@octocat", "@org/team"] # or several owners
data-steward = "@octocat"
[[surface]]
id = "data-contract"
name = "Data contract"
paths = ["data/raw/**"]
oversight_role = "data-steward" # must have an entry in role_handles
oversight_role = "data-steward"
codeowners_order = 100Surfaces without an oversight_role are skipped.
A role used by a surface but missing from role_handles is a hard error.
Projected surfaces must declare codeowners_order.
Lower values emit earlier;
later matching CODEOWNERS patterns take precedence.
surfaces.toml uses repository-root-relative globs. CODEOWNERS uses
gitignore-style patterns with a smaller feature set, so patterns are converted:
| surfaces.toml | CODEOWNERS | meaning |
|---|---|---|
README.md |
/README.md |
one file at the repo root |
docs/** |
/docs/** |
a directory, recursively |
sql/duckdb/** |
/sql/duckdb/** |
a nested directory, recursively |
docs/* |
/docs/* |
direct children only |
src/app/case.py |
/src/app/case.py |
one nested file |
Patterns CODEOWNERS cannot express:
- negation (
!), - single-character (
?), - character ranges (
[...]), or a **segment anywhere but a trailing/**
These are rejected at generation time with a message naming the offending pattern, so the failure surfaces during generation.
GitHub applies the last matching pattern for a path.
Entries are emitted by codeowners_order, then surface id.
Add a hook to the consuming repository so a stale CODEOWNERS fails review:
# .pre-commit-config.yaml in the repo that owns surfaces.toml
- repo: local
hooks:
- id: codeowners-up-to-date
name: CODEOWNERS matches surfaces.toml
entry: uv run se-codeowners check --strict
language: system
files: ^(\.accountability/surfaces\.toml|\.github/CODEOWNERS)$
pass_filenames: falsese-codeowners/
├── src/se_codeowners/
│ ├── __init__.py public API: load_surfaces, render_codeowners, ...
│ ├── __main__.py module entry: python -m se_codeowners (PATH-independent)
│ ├── _narrow.py boundary narrowing: tomllib Any -> typed values
│ ├── model.py frozen dataclasses (Surface, SurfacesDoc)
│ ├── load.py parse + validate surfaces.toml into the model
│ ├── translate.py surface glob -> CODEOWNERS pattern
│ ├── generate.py SurfacesDoc -> CODEOWNERS text
│ └── cli.py generate / check subcommands
└── tests/
Dependency direction is one-way:
_narrowandmodeldepend on nothing in the package;loaddepends on both;translateis standalone;generatedepends onmodelandtranslate;clidepends onloadandgenerate.
No raw Any from tomllib escapes _narrow/load.
Show command reference
Open a machine terminal where you want the project:
git clone https://github.com/structural-explainability/se-codeowners
cd se-codeowners
code .Use VS Code Menu:
View / Command Palette / Developer: Reload Window to refresh.
uv self update
uv python pin 3.14
uv lock --upgrade
uv sync --extra dev --extra docs --upgrade
uvx pre-commit install
uv run se-codeowners --help
uv run se-codeowners generate --help
uv run se-codeowners check --help
# validate manifest file
uvx se-manifest-schema validate-manifest --path SE_MANIFEST.toml --strict
git add -A
uvx pre-commit run --all-files
# repeat if changes were made
uvx pre-commit run --all-files
uv run python -m pyright
uv run python -m pytest
uv run python -m zensical build
# check import layers
uvx --with-editable . --from import-linter lint-imports --config .github/.importlinter
# check complexity; no output is good (all A or B)
uvx radon cc src/se_codeowners -s -a -n C
uv build
uvx twine check dist/*
# save progress
git add -A
git commit -m "update"
git push -u origin main