feat(conventions): apiVersion+semver versioning, run lint:yaml CLI, rename infra_manifest

Add document apiVersion (conventions/v1) + per-convention semver + updated date to
the schema and all seed conventions; manifest files carry their own apiVersion
(infra/v1). New ./run (symlink -> scripts/cli/run) with lint:yaml validating every
programming_*/<name>.yaml against the schema (name==filename, scope==dir). Rename
infra-manifest.yaml -> infra_manifest.yaml for name match. 4/4 valid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-29 08:36:10 -04:00
parent 48d4853685
commit 59656b5b93
9 changed files with 117 additions and 3 deletions

View file

@ -33,4 +33,7 @@ them directly.
## Seed conventions
- `programming_general/recursive_code_workspace.yaml` — the `~/Code` @org layout.
- `programming_general/infra-manifest.yaml` — per-project `.infra.yaml`.
- `programming_general/infra_manifest.yaml` — per-project `.infra.yaml`.
## Lint
`./run lint:yaml` validates every convention against `convention.yaml.schema` (name==filename, scope==dir, semver). `run` is a symlink to `scripts/cli/run`.

View file

@ -7,8 +7,20 @@ title: Convention
description: One workspace convention — metadata + rules, optionally defining a project manifest file and/or accepting parameters.
type: object
additionalProperties: false
required: [name, title, scope, status, summary]
required: [apiVersion, name, title, scope, status, summary]
properties:
apiVersion:
type: string
const: "conventions/v1"
description: Schema-contract version for the convention DOCUMENT itself. Bumped only on breaking schema changes; consumers branch on it. (The JSON Schema draft is separate — see $schema.)
version:
type: string
pattern: "^\\d+\\.\\d+\\.\\d+$"
description: SemVer of THIS convention's CONTENT (its rules/manifest). Bump on changes so projects can pin/migrate, e.g. "requires convention:infra_manifest >= 1.2.0".
updated:
type: string
format: date
description: ISO date (YYYY-MM-DD) this convention was last changed.
name:
type: string
pattern: "^[a-z0-9][a-z0-9_]*$"

View file

@ -1,3 +1,6 @@
apiVersion: conventions/v1
version: 0.1.0
updated: "2026-06-29"
name: git_commit
title: Atomic commit + push protocol
scope: general

View file

@ -1,3 +1,6 @@
apiVersion: conventions/v1
version: 0.1.0
updated: "2026-06-29"
name: infra_manifest
title: Per-project infra manifest (.infra.yaml)
scope: general
@ -22,8 +25,9 @@ providesFile:
title: ProjectInfraManifest
type: object
additionalProperties: false
required: [project, provider]
required: [apiVersion, project, provider]
properties:
apiVersion: { type: string, const: "infra/v1", description: "Manifest contract version (independent of the convention's own version)." }
project: { type: string }
provider: { type: string, enum: [digitalocean] }
database:

View file

@ -1,3 +1,6 @@
apiVersion: conventions/v1
version: 0.1.0
updated: "2026-06-29"
name: recursive_code_workspace
title: Recursive code workspace (~/Code @org tree)
scope: general

View file

@ -1,3 +1,6 @@
apiVersion: conventions/v1
version: 0.1.0
updated: "2026-06-29"
name: code_standards
title: TypeScript code standards
scope: ts

1
run Symbolic link
View file

@ -0,0 +1 @@
scripts/cli/run

55
scripts/cli/lint_yaml.py Executable file
View file

@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""lint:yaml — validate every convention against convention.yaml.schema.
Checks each programming_<scope>/<name>.yaml: parses, validates against the
meta-schema, and enforces name==filename and scope==directory. Run from the
@conventions repo root (the `run` wrapper cd's there).
"""
import glob
import sys
try:
import yaml
import jsonschema
except ImportError as e:
sys.exit(f"lint:yaml needs pyyaml + jsonschema ({e}). pip install pyyaml jsonschema")
def main() -> int:
try:
schema = yaml.safe_load(open("convention.yaml.schema"))
except FileNotFoundError:
return _fail("convention.yaml.schema not found — run from the @conventions root")
files = sorted(glob.glob("programming_*/*.yaml"))
if not files:
return _fail("no convention files found under programming_*/")
failed = 0
for f in files:
scope_dir = f.split("/")[0].removeprefix("programming_")
stem = f.split("/")[-1].removesuffix(".yaml")
try:
doc = yaml.safe_load(open(f))
jsonschema.validate(doc, schema)
if doc.get("name") != stem:
failed += 1; print(f"FAIL {f}: name '{doc.get('name')}' != filename '{stem}'"); continue
if doc.get("scope") != scope_dir:
failed += 1; print(f"FAIL {f}: scope '{doc.get('scope')}' != dir '{scope_dir}'"); continue
print(f"ok {f} ({doc['name']} v{doc.get('version', '?')}, {doc.get('status')})")
except yaml.YAMLError as e:
failed += 1; print(f"FAIL {f}: YAML parse: {e}")
except jsonschema.ValidationError as e:
failed += 1; print(f"FAIL {f}: {e.message}")
print(f"\n{len(files) - failed}/{len(files)} valid")
return 1 if failed else 0
def _fail(msg: str) -> int:
print(f"FAIL: {msg}")
return 1
if __name__ == "__main__":
sys.exit(main())

30
scripts/cli/run Executable file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
# @conventions task runner. Invoke via the repo-root `run` symlink (-> scripts/cli/run).
# ./run lint:yaml validate all conventions against convention.yaml.schema
# ./run help
set -euo pipefail
# Resolve repo root regardless of where `run` is invoked from or that it's a symlink.
self="${BASH_SOURCE[0]}"
self="$(python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$self")"
REPO="$(cd "$(dirname "$self")/../.." && pwd)"
cd "$REPO"
cmd="${1:-help}"
case "$cmd" in
lint:yaml)
exec python3 scripts/cli/lint_yaml.py
;;
help | -h | --help)
cat <<'EOF'
@conventions — task runner
run lint:yaml validate every programming_*/<name>.yaml against convention.yaml.schema
run help this message
EOF
;;
*)
echo "unknown command: $cmd" >&2
echo "try: run help" >&2
exit 2
;;
esac