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:
parent
48d4853685
commit
59656b5b93
9 changed files with 117 additions and 3 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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_]*$"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
1
run
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
scripts/cli/run
|
||||
55
scripts/cli/lint_yaml.py
Executable file
55
scripts/cli/lint_yaml.py
Executable 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
30
scripts/cli/run
Executable 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
|
||||
Loading…
Add table
Reference in a new issue