From 59656b5b937744ddb3be9642fe279b1d2b12fd99 Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 29 Jun 2026 08:36:10 -0400 Subject: [PATCH] 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_*/.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) --- README.md | 5 +- convention.yaml.schema | 14 ++++- programming_general/git_commit.yaml | 3 + ...nfra-manifest.yaml => infra_manifest.yaml} | 6 +- .../recursive_code_workspace.yaml | 3 + programming_ts/code_standards.yaml | 3 + run | 1 + scripts/cli/lint_yaml.py | 55 +++++++++++++++++++ scripts/cli/run | 30 ++++++++++ 9 files changed, 117 insertions(+), 3 deletions(-) rename programming_general/{infra-manifest.yaml => infra_manifest.yaml} (89%) create mode 120000 run create mode 100755 scripts/cli/lint_yaml.py create mode 100755 scripts/cli/run diff --git a/README.md b/README.md index 6c05c0a..639140c 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/convention.yaml.schema b/convention.yaml.schema index 55a37c1..0a2f7bd 100644 --- a/convention.yaml.schema +++ b/convention.yaml.schema @@ -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_]*$" diff --git a/programming_general/git_commit.yaml b/programming_general/git_commit.yaml index 4fdab0e..a0c2caf 100644 --- a/programming_general/git_commit.yaml +++ b/programming_general/git_commit.yaml @@ -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 diff --git a/programming_general/infra-manifest.yaml b/programming_general/infra_manifest.yaml similarity index 89% rename from programming_general/infra-manifest.yaml rename to programming_general/infra_manifest.yaml index 3cfd594..a6ccbf6 100644 --- a/programming_general/infra-manifest.yaml +++ b/programming_general/infra_manifest.yaml @@ -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: diff --git a/programming_general/recursive_code_workspace.yaml b/programming_general/recursive_code_workspace.yaml index 93c5966..aafcdea 100644 --- a/programming_general/recursive_code_workspace.yaml +++ b/programming_general/recursive_code_workspace.yaml @@ -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 diff --git a/programming_ts/code_standards.yaml b/programming_ts/code_standards.yaml index fa28c8c..a8cc69e 100644 --- a/programming_ts/code_standards.yaml +++ b/programming_ts/code_standards.yaml @@ -1,3 +1,6 @@ +apiVersion: conventions/v1 +version: 0.1.0 +updated: "2026-06-29" name: code_standards title: TypeScript code standards scope: ts diff --git a/run b/run new file mode 120000 index 0000000..e42c3be --- /dev/null +++ b/run @@ -0,0 +1 @@ +scripts/cli/run \ No newline at end of file diff --git a/scripts/cli/lint_yaml.py b/scripts/cli/lint_yaml.py new file mode 100755 index 0000000..8e3c421 --- /dev/null +++ b/scripts/cli/lint_yaml.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""lint:yaml — validate every convention against convention.yaml.schema. + +Checks each programming_/.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()) diff --git a/scripts/cli/run b/scripts/cli/run new file mode 100755 index 0000000..307b2c0 --- /dev/null +++ b/scripts/cli/run @@ -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_*/.yaml against convention.yaml.schema + run help this message +EOF + ;; + *) + echo "unknown command: $cmd" >&2 + echo "try: run help" >&2 + exit 2 + ;; +esac