Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .crystal-governance.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
schema: crystal-governance-pin/v1
governance_version: v1.2.7
governance_version: v1.4.0
source: Malakof/.github
# Self-pin: Malakof/.github references its own current version.
# Lets tooling (sync, governance-check) treat this repo like any other
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/enforce-conventions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
contents: read
steps:
- name: Checkout caller repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0

Expand All @@ -38,7 +38,7 @@ jobs:
fi

- name: Checkout governance source
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
repository: Malakof/.github
path: .governance
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/format-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
models: read
steps:
- name: Checkout caller repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 1

Expand All @@ -57,7 +57,7 @@ jobs:
fi

- name: Checkout governance at pin
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
repository: Malakof/.github
path: .governance
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/governance-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
issues: read
steps:
- name: Checkout repo
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0

Expand Down Expand Up @@ -52,7 +52,7 @@ jobs:
fi

- name: Checkout governance at pin
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
repository: Malakof/.github
path: .governance
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/on-release-bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout this repo
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Skip if CRYSTAL_GITHUB_PAT secret is missing
id: precheck
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Governance Tests

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.11"

- name: Install dependencies
run: python3 -m pip install "PyYAML>=6.0"

- name: Run governance tests
run: python3 -m unittest discover -s tests -v

- name: Check whitespace
run: git diff --check
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
*.py[cod]
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Single source of truth for Crystal team GitHub conventions:
labels, issue/PR templates, naming, reusable workflows, agent skill.

**Current version**: `v1.1.0` (see `.crystal-governance.yaml`).
**Current version**: `v1.4.0` (see `.crystal-governance.yaml`).

## For humans

Expand Down Expand Up @@ -34,7 +34,7 @@ Add to the target repo:
1. `.crystal-governance.yaml`:
```yaml
schema: crystal-governance-pin/v1
governance_version: v1.1.0
governance_version: v1.4.0
source: Malakof/.github
```

Expand All @@ -44,7 +44,7 @@ Add to the target repo:
on: [push, pull_request]
jobs:
check:
uses: Malakof/.github/.github/workflows/governance-check.yml@v1.1.0
uses: Malakof/.github/.github/workflows/governance-check.yml@v1.4.0
```

3. `.github/workflows/enforce-conventions.yml`:
Expand All @@ -53,7 +53,7 @@ Add to the target repo:
on: [pull_request]
jobs:
enforce:
uses: Malakof/.github/.github/workflows/enforce-conventions.yml@v1.1.0
uses: Malakof/.github/.github/workflows/enforce-conventions.yml@v1.4.0
```

4. Run label sync:
Expand Down
19 changes: 17 additions & 2 deletions governance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ versioned by semver tags, propagated to target repos via the pin file
`.crystal-governance.yaml` and `crystal-company/builders/sync_repo_surface.py`.

**Schema**: `crystal-governance/v1`
**Version**: 1.0.0
**Version**: 1.1.0

---

Expand Down Expand Up @@ -55,6 +55,21 @@ Commit subjects use Conventional Commits with a targeted emoji:
**Allowed types**: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`,
`test`, `build`, `ci`, `chore`, `revert`.

PR title types map deterministically to the functional `type:*` taxonomy:

| PR title type | Required label |
|---|---|
| `feat` | `type:feature` |
| `fix`, `revert` | `type:bug` |
| `docs` | `type:docs` |
| `refactor`, `perf` | `type:refactor` |
| `test` | `type:test` |
| `style`, `build`, `ci`, `chore` | `type:chore` |

The map records the modal meaning of each Conventional Commit type. Use
`fix:` for a performance regression and `chore:` for a non-remedial withdrawal
when the modal `perf:` or `revert:` mapping would misclassify the delivery.

**Scopes**: per repo in [`scopes.yaml`](./scopes.yaml). Optional but
recommended on repos with multiple functional domains.

Expand Down Expand Up @@ -266,7 +281,7 @@ inactivity.

```yaml
schema: crystal-governance-pin/v1
governance_version: v1.0.0
governance_version: v1.4.0
source: Malakof/.github
```

Expand Down
22 changes: 21 additions & 1 deletion governance/labels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,27 @@
# - description max 100 characters (GitHub API limit)

schema: crystal-labels/v1
version: 1.0.0
version: 1.1.0

# Conventional Commit PR types and issue-title types intentionally differ from
# the functional type:* taxonomy. This total mapping is the canonical bridge
# used by deterministic PR and issue validation.
title_type_map:
feat: type:feature
fix: type:bug
docs: type:docs
style: type:chore
refactor: type:refactor
perf: type:refactor
test: type:test
build: type:chore
ci: type:chore
chore: type:chore
revert: type:bug
feature: type:feature
bug: type:bug
spike: type:spike
epic: type:epic

# =============================================================================
# 1. UNIVERSAL — applied to ALL Crystal repos, mandatory
Expand Down
74 changes: 74 additions & 0 deletions scripts/conventions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Shared Crystal title grammar and title-to-label governance policy."""

from __future__ import annotations

import re
from pathlib import Path

import yaml

ALLOWED_CONVENTIONAL_TYPES = (
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"build",
"ci",
"chore",
"revert",
)
ISSUE_TITLE_TYPES = ("feature", "bug", "chore", "docs", "refactor", "spike", "test")
ALL_GOVERNED_TITLE_TYPES = frozenset((*ALLOWED_CONVENTIONAL_TYPES, *ISSUE_TITLE_TYPES, "epic"))

_CONVENTIONAL_TYPE_PATTERN = "|".join(ALLOWED_CONVENTIONAL_TYPES)
CONVENTIONAL_PREFIX_PATTERN = (
rf"(?P<type>{_CONVENTIONAL_TYPE_PATTERN})"
r"(?:\((?P<scope>[a-z0-9._-]+)\))?"
r"(?P<bang>!)?"
)
CONVENTIONAL_TITLE_RE = re.compile(rf"^{CONVENTIONAL_PREFIX_PATTERN}: (?P<subject>.+)$")
CONVENTIONAL_COMMIT_RE = re.compile(
rf"^{CONVENTIONAL_PREFIX_PATTERN}: (?P<emoji>\S+) (?P<subject>.+)$"
)
ISSUE_TITLE_RE = re.compile(
rf"^(?P<type>{'|'.join(ISSUE_TITLE_TYPES)}): (?P<subject>.+)$"
)
EPIC_TITLE_RE = re.compile(r"^\[EPIC\] (?P<subject>.+)$")


def load_title_type_map(labels_file: Path) -> dict[str, str]:
"""Load and validate the total governed title-type mapping."""
data = yaml.safe_load(labels_file.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError(f"{labels_file}: expected a YAML object")

raw_mapping = data.get("title_type_map")
if not isinstance(raw_mapping, dict):
raise ValueError(f"{labels_file}: title_type_map must be an object")
mapping = {str(key): str(value) for key, value in raw_mapping.items()}

keys = set(mapping)
missing = sorted(ALL_GOVERNED_TITLE_TYPES - keys)
unexpected = sorted(keys - ALL_GOVERNED_TITLE_TYPES)
if missing or unexpected:
raise ValueError(
f"{labels_file}: title_type_map domain mismatch; missing={missing}, unexpected={unexpected}"
)

categories = data.get("categories")
type_category = categories.get("type") if isinstance(categories, dict) else None
raw_labels = type_category.get("labels") if isinstance(type_category, dict) else None
canonical_labels = {
item.get("name")
for item in raw_labels or []
if isinstance(item, dict) and isinstance(item.get("name"), str)
}
invalid_values = sorted(set(mapping.values()) - canonical_labels)
if invalid_values:
raise ValueError(
f"{labels_file}: title_type_map references non-canonical labels {invalid_values}"
)
return mapping
64 changes: 33 additions & 31 deletions scripts/format_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
import urllib.request
from pathlib import Path

from conventions import (
CONVENTIONAL_TITLE_RE,
EPIC_TITLE_RE,
ISSUE_TITLE_RE,
load_title_type_map,
)

ENDPOINT = "https://models.github.ai/inference/chat/completions"
DEFAULT_MODEL = "openai/gpt-4o-mini"
COMMENT_MARKER = "<!-- crystal-format-check -->"
Expand All @@ -39,11 +46,13 @@ def gh_json(args: list[str]) -> object:
return json.loads(result.stdout)


def load_governance(governance_root: Path) -> dict[str, str]:
def load_governance(governance_root: Path) -> dict[str, object]:
labels_path = governance_root / "governance" / "labels.yaml"
pieces = {
"labels_yaml": (governance_root / "governance" / "labels.yaml").read_text(encoding="utf-8"),
"labels_yaml": labels_path.read_text(encoding="utf-8"),
"scopes_yaml": (governance_root / "governance" / "scopes.yaml").read_text(encoding="utf-8"),
"skill_md": (governance_root / "skills" / "crystal-github-conventions" / "SKILL.md").read_text(encoding="utf-8"),
"title_type_map": load_title_type_map(labels_path),
}
return pieces

Expand All @@ -59,34 +68,23 @@ def fetch_artifact(repo: str, number: int, kind: str) -> dict:
return payload if isinstance(payload, dict) else {}


def deterministic_precheck(artifact: dict, kind: str) -> dict:
def deterministic_precheck(
artifact: dict, kind: str, title_type_map: dict[str, str]
) -> dict:
"""Run mechanical checks before sending to the LLM. Returns a dict the LLM
can trust as ground truth."""
import re
title = (artifact.get("title") or "").strip()
label_names = {item.get("name") for item in (artifact.get("labels") or []) if isinstance(item, dict)}

cc_re = re.compile(
r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)"
r"(!)?(?:\(([a-z0-9._-]+)\))?: (.+)$"
)
issue_re = re.compile(r"^(feature|bug|chore|docs|refactor|spike|test): (.+)$")
epic_re = re.compile(r"^\[EPIC\] .+$")

is_epic = epic_re.match(title) is not None
cc_match = cc_re.match(title)
issue_match = issue_re.match(title) if kind == "issue" else None

title_format_valid = bool(cc_match) or bool(issue_match) or is_epic
title_type = (
cc_match.group(1)
if cc_match
else (issue_match.group(1) if issue_match else ("epic" if is_epic else None))
)
title_subject = (
cc_match.group(4)
if cc_match
else (issue_match.group(2) if issue_match else (title.removeprefix("[EPIC] ") if is_epic else title))
conventional_match = CONVENTIONAL_TITLE_RE.match(title) if kind == "pr" else None
issue_match = ISSUE_TITLE_RE.match(title) if kind == "issue" else None
epic_match = EPIC_TITLE_RE.match(title) if kind == "issue" else None

title_format_valid = bool(conventional_match or issue_match or epic_match)
match = conventional_match or issue_match
title_type = match.group("type") if match else ("epic" if epic_match else None)
title_subject = match.group("subject") if match else (
epic_match.group("subject") if epic_match else title
)

subject_len_ok = len(title_subject or "") <= (80 if kind == "issue" else 72)
Expand All @@ -97,10 +95,10 @@ def deterministic_precheck(artifact: dict, kind: str) -> dict:
has_one_priority = len(priority_labels) == 1
has_one_type = len(type_labels) == 1

type_label_matches_title = False
if has_one_type and title_type:
type_map = {"feat": "feature"}
type_label_matches_title = type_labels[0] == f"type:{type_map.get(title_type, title_type)}"
expected_type_label = title_type_map.get(title_type) if title_type else None
type_label_matches_title = bool(
has_one_type and expected_type_label and type_labels[0] == expected_type_label
)

return {
"title": title,
Expand All @@ -113,13 +111,17 @@ def deterministic_precheck(artifact: dict, kind: str) -> dict:
"type_labels": type_labels,
"has_one_priority": has_one_priority,
"has_one_type": has_one_type,
"expected_type_label": expected_type_label,
"type_label_matches_title": type_label_matches_title,
"all_labels": sorted(label_names),
}


def build_prompts(governance: dict[str, str], artifact: dict, kind: str, repo: str) -> tuple[str, str]:
precheck = deterministic_precheck(artifact, kind)
def build_prompts(governance: dict[str, object], artifact: dict, kind: str, repo: str) -> tuple[str, str]:
title_type_map = governance["title_type_map"]
if not isinstance(title_type_map, dict):
raise ValueError("governance title_type_map is not an object")
precheck = deterministic_precheck(artifact, kind, title_type_map)
system = (
"You are Crystal Governance Validator, a qualitative reviewer for GitHub artifacts in the Crystal team. "
"You receive (a) the canonical conventions, (b) the artifact, and (c) a deterministic mechanical pre-check that has ALREADY validated title format, label presence, type matching, and subject length. "
Expand Down
Loading