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
28 changes: 5 additions & 23 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,8 @@
# Copilot Instructions

Follow the repository's canonical engineering skills under
`docs/engineering/skills/`.
Repository-wide guidance for AI agents lives in `AGENTS.md`. Read it first: it
lists the engineering skill to read for each kind of change, and its generated
Rules section lists every rule that governs this repository.

For a new HTTP route or an existing route query-parameter contract change,
read `docs/engineering/skills/api-routes.md`.

For API v2 migration contract, route-group metadata, generated migration docs,
or migration guard changes, read
`docs/engineering/skills/migration_contracts.md`.

For tests, read `docs/engineering/skills/testing.md` before adding, moving, or
reviewing test files.

For SQLAlchemy model or Alembic migration work, read
`docs/engineering/skills/alembic-migrations.md`.

For API v2 route, service, or database-access module additions or moves, read
`docs/engineering/skills/v2-code-organization.md`.

For `Makefile` changes, read
`docs/engineering/skills/repository-maintenance.md`.

For pull requests, read `docs/engineering/skills/github-prs.md` before opening,
replacing, or sharing a PR.
This file is a Copilot adapter. Keep it a pointer: put guidance in `AGENTS.md`
or in a skill under `docs/engineering/skills/`, never here.
2 changes: 2 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ jobs:
python-version: "3.12"
- name: Test the rules registry tool
run: python -m unittest discover -s scripts/rules/tests -t .
- name: Install shellcheck (for the ci-scripts guard)
run: pip install "shellcheck-py==0.11.0.1"
- name: Run quality guards
run: python scripts/run_quality_guards.py

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install shellcheck (for the ci-scripts guard)
run: pip install "shellcheck-py==0.11.0.1"
- name: Run quality guards
run: python scripts/run_quality_guards.py

Expand Down
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ names, logging, or observability failure handling, read
When modifying the `Makefile`, read
`docs/engineering/skills/repository-maintenance.md`.

When adding or changing a GitHub Actions workflow step or a script under
`.github/scripts/`, read `docs/engineering/skills/ci-workflows.md`.

When adding, changing, or removing a rule, a guard test, a checker
configuration, or a CI job that enforces one, read
`docs/engineering/skills/rules-registry.md`.
Expand All @@ -50,6 +53,12 @@ python scripts/run_quality_guards.py
python scripts/export_migration_contracts.py
```

## Safety Boundaries

Do not claim a route, database table, compute path, or deployment surface has
migrated unless the relevant contract tests, migration flags, and generated
migration docs identify that state.

## Rules

<!-- BEGIN GENERATED: rules (python -m scripts.rules render) -->
Expand Down Expand Up @@ -121,13 +130,21 @@ To add or change a rule, read `docs/engineering/skills/rules-registry.md`.
- `api.observability.id-is-diagnostic-only`: observability_id is used only to query diagnostic records, never for idempotency, cache ownership, authorization, database identity, routing or calculation behavior. (doc-only, not checked: follow it yourself)
- `api.observability.registered-stages-only`: Span names come from the stage plans in policyengine_api.observability.stages; route and service code never adds span-name string literals. (doc-only, not checked: follow it yourself)

### CI workflows and scripts

- `api.ci.multiline-run-is-script`: A GitHub Actions run: block holds one line of code and no chained commands (&&, ||, ;); anything more lives in a tested script under .github/scripts/, called from a single-line step. (blocking: quality guard `ci-scripts`)
- `api.ci.shell-scripts-pass-shellcheck`: Every shell script under .github/scripts/ passes shellcheck. (blocking: quality guard `ci-scripts`)
- `api.ci.workflow-scripts-tested`: Every shell script a workflow calls is run by at least one test. (blocking: quality guard `ci-scripts`)

### Process and repository hygiene

- `api.agents.adapters-point-to-agents-md`: CLAUDE.md and .github/copilot-instructions.md only point to AGENTS.md; skills and rules are listed in AGENTS.md alone. (blocking: quality guard `rules-registry`)
- `api.process.agents-never-bypass`: Agents never bypass branch rules, even with admin credentials: no gh pr merge --admin and no bypass option; a blocked merge is reported to a human. (doc-only, not checked: follow it yourself)
- `api.process.changelog-fragment`: Every PR adds a changelog.d fragment. (blocking: CI job `check-changelog`)
- `api.process.cloud-run-image-builds`: The Cloud Run container image builds. (blocking: CI job `test_cloud_run_container_builds`)
- `api.process.docker-image-builds`: The API Docker image builds. (blocking: CI job `test_container_builds`)
- `api.process.makefile-phony`: Command-only Makefile targets are declared in .PHONY, and .PHONY stays in sync when targets are renamed or removed. (doc-only, not checked: follow it yourself)
- `api.process.pr-tracking-issue`: Every PR has a tracking issue in this repository, and the first line of its description is Fixes #ISSUE_NUMBER. (doc-only, not checked: follow it yourself)
- `api.process.rule-machinery-code-owned`: Changes to the rule machinery (rules/, scripts/rules/, scripts/guards/, scripts/run_quality_guards.py and CODEOWNERS) need approval from a core developer other than the author. (doc-only, not checked: follow it yourself)
- `api.process.rules-registry`: Every rule is registered in rules/registry.toml, is enforced where it claims to be, and its rendered views are current. (blocking: quality guard `rules-registry`)
- `api.process.stacked-prs-full-ci-at-bottom`: In a stack of pull requests, only the lowest unmerged one runs the full PR workflow. Every pr.yml job is needed by the CI complete gate, which fails on the pull requests above it and otherwise passes only when every job passed, so a stack merges bottom-up and nothing merges untested. (blocking: pytest `tests/unit/test_ci_complete_gate.py`)
Expand Down
45 changes: 5 additions & 40 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,8 @@
# Claude Instructions

These instructions apply repository-wide.
Repository-wide guidance for AI agents lives in `AGENTS.md`. Read it first: it
lists the engineering skill to read for each kind of change, and its generated
Rules section lists every rule that governs this repository.

## Canonical Guidance

Repository-wide AI-facing engineering guidance lives in `AGENTS.md`.
Canonical skills live under `docs/engineering/skills/`.

Use those files as the source of truth. This file is a Claude adapter and should
stay thin; do not duplicate detailed testing, CI, formatting, or architecture
rules here.

## Required Skill Lookup

Before adding an HTTP route or changing an existing route's query-parameter
contract, read `docs/engineering/skills/api-routes.md`.

Before opening, replacing, or sharing a PR, read
`docs/engineering/skills/github-prs.md`.

When changing API v2 migration contracts, route-group migration metadata,
generated migration docs, or migration guard scripts, read
`docs/engineering/skills/migration_contracts.md`.

When adding, moving, or reviewing tests, read
`docs/engineering/skills/testing.md`.

When changing SQLAlchemy models, Alembic configuration, database schemas, or
migration revisions, read
`docs/engineering/skills/alembic-migrations.md`.

When adding or moving API v2 route, service, or database-access modules, read
`docs/engineering/skills/v2-code-organization.md`.

When modifying the `Makefile`, read
`docs/engineering/skills/repository-maintenance.md`.

## Safety Boundaries

Do not claim a route, database table, compute path, or deployment surface has
migrated unless the relevant contract tests, migration flags, and generated
migration docs identify that state.
This file is a Claude adapter. Keep it a pointer: put guidance in `AGENTS.md` or
in a skill under `docs/engineering/skills/`, never here.
1 change: 1 addition & 0 deletions changelog.d/rules-followups.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Made CLAUDE.md and the Copilot instructions thin pointers to AGENTS.md, required workflow logic longer than one line to live in tested and shellchecked scripts, and registered the tracking-issue rule.
2 changes: 2 additions & 0 deletions docs/engineering/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Current skills:
routes and existing query-contract changes.
- `alembic-migrations.md`: mandatory autogenerated Alembic revision workflow,
deployment safeguards, and migration validation.
- `ci-workflows.md`: one line of code per workflow `run:` block, tested
workflow scripts, and shellcheck.
- `github-prs.md`: PR workflow and migration PR handoff expectations.
- `migration_contracts.md`: API v2 migration route contracts, route-group
metadata, generated migration artifacts, and quality guards.
Expand Down
51 changes: 51 additions & 0 deletions docs/engineering/skills/ci-workflows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# CI Workflows

Read this before adding or changing a GitHub Actions workflow step, or a script
under `.github/scripts/`.

## One line of code per `run:` block

A workflow `run:` block may hold only one line of code, with no chained
commands. Anything more goes in a shell script under `.github/scripts/`, called
from a single-line step:

```yaml
- name: Install ruff (version pinned in uv.lock)
run: bash .github/scripts/install_locked_ruff.sh
```

Inline shell in YAML can't be linted, unit-tested or run locally; a script can.

Every non-empty, non-comment line counts, including each line of a folded
(`>`) block and each backslash-continued line. Chaining commands on one line
with `&&`, `||` or `;` also counts as more than one line. Operators inside
quotes or `${{ }}` expressions don't count, and a single pipeline (`a | b`) is
one command.

## Writing a workflow script

- Start with `#!/usr/bin/env bash` and `set -euo pipefail`.
- Take inputs from arguments or environment variables, not from `${{ }}`
expressions inside the script.
- It must pass `shellcheck` (the `ci-scripts` quality guard runs it).

## Testing a workflow script

Every script a workflow calls must be referenced by name in at least one test.
Test it the way the existing scripts are tested: start it with `subprocess`,
put fake versions of the commands it calls (`gcloud`, `pip`, `curl`, ...) first
on `PATH`, and assert on what they were called with. Cover the success path and
at least one failure path.

Examples: `tests/unit/test_install_locked_ruff_script.py` and
`tests/unit/test_cloud_run_deploy_scripts.py`.

## Enforcement and baselines

The `ci-scripts` quality guard (`python scripts/run_quality_guards.py`, or
`python -m scripts.rules ci-scripts`) checks all three rules above. Existing
violations are listed in `rules/baselines/ci-*.txt`. Anything new fails, and so
does a baseline entry that no longer applies, so each list only shrinks. When
you convert an existing multi-line block into a script, or add a missing test,
remove its baseline entry and lower the rule's `baseline.count` in
`rules/registry.toml`.
42 changes: 36 additions & 6 deletions docs/generated/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ Every rule that governs `policyengine-api` is listed here with how it is enforce

| Tier | enforced | ratcheted | advisory | doc-only | Total |
| --- | ---: | ---: | ---: | ---: | ---: |
| mechanical | 36 | 0 | 2 | 11 | 49 |
| mechanical | 37 | 3 | 2 | 12 | 54 |
| metric | 0 | 0 | 0 | 0 | 0 |
| judgment | 0 | 0 | 0 | 0 | 0 |

**Blocking:** 36 of 49 rules.
**Blocking:** 40 of 54 rules.

## Gaps

Expand All @@ -23,6 +23,7 @@ Every rule that governs `policyengine-api` is listed here with how it is enforce
- `api.observability.registered-stages-only`: Span names come from the stage plans in policyengine_api.observability.stages; route and service code never adds span-name string literals.
- `api.process.agents-never-bypass`: Agents never bypass branch rules, even with admin credentials: no gh pr merge --admin and no bypass option; a blocked merge is reported to a human.
- `api.process.makefile-phony`: Command-only Makefile targets are declared in .PHONY, and .PHONY stays in sync when targets are renamed or removed.
- `api.process.pr-tracking-issue`: Every PR has a tracking issue in this repository, and the first line of its description is Fixes #ISSUE_NUMBER.
- `api.process.rule-machinery-code-owned`: Changes to the rule machinery (rules/, scripts/rules/, scripts/guards/, scripts/run_quality_guards.py and CODEOWNERS) need approval from a core developer other than the author.
- `api.routes.shared-query-parameters`: Routes that accept query parameters define them through the shared typed query models, never with a second manual parser.
- `api.v2.connectors-never-commit`: v2 connectors receive a session from the service and never open, commit, roll back or close it.
Expand Down Expand Up @@ -63,7 +64,9 @@ Every rule that governs `policyengine-api` is listed here with how it is enforce

### Ratcheted (existing violations still tolerated)

- None.
- `api.ci.multiline-run-is-script`: 10 remaining in `rules/baselines/ci-multiline-run.txt`
- `api.ci.shell-scripts-pass-shellcheck`: 1 remaining in `rules/baselines/ci-shellcheck.txt`
- `api.ci.workflow-scripts-tested`: 1 remaining in `rules/baselines/ci-untested-scripts.txt`

### Exceptions and review dates

Expand Down Expand Up @@ -320,21 +323,47 @@ flowchart LR
r_api_observability_registered_stages_only -.-> gap_unenforced
```

## CI workflows and scripts

| Rule | Statement | Tier | Status | Blocking | Enforced by | CI | Proof |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `api.ci.multiline-run-is-script` | A GitHub Actions run: block holds one line of code and no chained commands (&&, \|\|, ;); anything more lives in a tested script under .github/scripts/, called from a single-line step. | mechanical | ratcheted | yes | quality guard `ci-scripts` | `pr.yml:quality-guards` | yes |
| `api.ci.shell-scripts-pass-shellcheck` | Every shell script under .github/scripts/ passes shellcheck. | mechanical | ratcheted | yes | quality guard `ci-scripts` | `pr.yml:quality-guards` | yes |
| `api.ci.workflow-scripts-tested` | Every shell script a workflow calls is run by at least one test. | mechanical | ratcheted | yes | quality guard `ci-scripts` | `pr.yml:quality-guards` | yes |

```mermaid
flowchart LR
r_api_ci_multiline_run_is_script("api.ci.multiline-run-is-script (ratcheted)")
e_quality_guard_ci_scripts["quality guard ci-scripts"]
j_CI_pr_yml_quality_guards["CI pr.yml:quality-guards"]
r_api_ci_shell_scripts_pass_shellcheck("api.ci.shell-scripts-pass-shellcheck (ratcheted)")
r_api_ci_workflow_scripts_tested("api.ci.workflow-scripts-tested (ratcheted)")
r_api_ci_multiline_run_is_script --> e_quality_guard_ci_scripts
e_quality_guard_ci_scripts --> j_CI_pr_yml_quality_guards
r_api_ci_shell_scripts_pass_shellcheck --> e_quality_guard_ci_scripts
r_api_ci_workflow_scripts_tested --> e_quality_guard_ci_scripts
```

## Process and repository hygiene

| Rule | Statement | Tier | Status | Blocking | Enforced by | CI | Proof |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `api.agents.adapters-point-to-agents-md` | CLAUDE.md and .github/copilot-instructions.md only point to AGENTS.md; skills and rules are listed in AGENTS.md alone. | mechanical | enforced | yes | quality guard `rules-registry` | `pr.yml:quality-guards` | yes |
| `api.process.agents-never-bypass` | Agents never bypass branch rules, even with admin credentials: no gh pr merge --admin and no bypass option; a blocked merge is reported to a human. | mechanical | doc-only | no | nothing | — | — |
| `api.process.changelog-fragment` | Every PR adds a changelog.d fragment. | mechanical | enforced | yes | CI job `check-changelog` | `pr.yml:check-changelog` | — |
| `api.process.cloud-run-image-builds` | The Cloud Run container image builds. | mechanical | enforced | yes | CI job `test_cloud_run_container_builds` | `pr.yml:test_cloud_run_container_builds` | — |
| `api.process.docker-image-builds` | The API Docker image builds. | mechanical | enforced | yes | CI job `test_container_builds` | `pr.yml:test_container_builds` | — |
| `api.process.makefile-phony` | Command-only Makefile targets are declared in .PHONY, and .PHONY stays in sync when targets are renamed or removed. | mechanical | doc-only | no | nothing | — | — |
| `api.process.pr-tracking-issue` | Every PR has a tracking issue in this repository, and the first line of its description is Fixes #ISSUE_NUMBER. | mechanical | doc-only | no | nothing | — | — |
| `api.process.rule-machinery-code-owned` | Changes to the rule machinery (rules/, scripts/rules/, scripts/guards/, scripts/run_quality_guards.py and CODEOWNERS) need approval from a core developer other than the author. | mechanical | doc-only | no | nothing | — | — |
| `api.process.rules-registry` | Every rule is registered in rules/registry.toml, is enforced where it claims to be, and its rendered views are current. | mechanical | enforced | yes | quality guard `rules-registry` | `pr.yml:quality-guards` | **unproven** |
| `api.process.stacked-prs-full-ci-at-bottom` | In a stack of pull requests, only the lowest unmerged one runs the full PR workflow. Every pr.yml job is needed by the CI complete gate, which fails on the pull requests above it and otherwise passes only when every job passed, so a stack merges bottom-up and nothing merges untested. | mechanical | enforced | yes | pytest `tests/unit/test_ci_complete_gate.py` | `pr.yml:test`, `pr.yml:ci-complete` | yes |

```mermaid
flowchart LR
r_api_agents_adapters_point_to_agents_md("api.agents.adapters-point-to-agents-md (enforced)")
e_quality_guard_rules_registry["quality guard rules-registry"]
j_CI_pr_yml_quality_guards["CI pr.yml:quality-guards"]
r_api_process_agents_never_bypass("api.process.agents-never-bypass (doc-only)")
gap_unenforced>"not enforced"]
r_api_process_changelog_fragment("api.process.changelog-fragment (enforced)")
Expand All @@ -347,14 +376,15 @@ flowchart LR
e_CI_job_test_container_builds["CI job test_container_builds"]
j_CI_pr_yml_test_container_builds["CI pr.yml:test_container_builds"]
r_api_process_makefile_phony("api.process.makefile-phony (doc-only)")
r_api_process_pr_tracking_issue("api.process.pr-tracking-issue (doc-only)")
r_api_process_rule_machinery_code_owned("api.process.rule-machinery-code-owned (doc-only)")
r_api_process_rules_registry("api.process.rules-registry (enforced)")
e_quality_guard_rules_registry["quality guard rules-registry"]
j_CI_pr_yml_quality_guards["CI pr.yml:quality-guards"]
r_api_process_stacked_prs_full_ci_at_bottom("api.process.stacked-prs-full-ci-at-bottom (enforced)")
e_pytest_tests_unit_test_ci_complete_gate_py["pytest tests/unit/test_ci_complete_gate.py"]
j_CI_pr_yml_test["CI pr.yml:test"]
j_CI_pr_yml_ci_complete["CI pr.yml:ci-complete"]
r_api_agents_adapters_point_to_agents_md --> e_quality_guard_rules_registry
e_quality_guard_rules_registry --> j_CI_pr_yml_quality_guards
r_api_process_agents_never_bypass -.-> gap_unenforced
r_api_process_changelog_fragment --> e_CI_job_check_changelog
e_CI_job_check_changelog --> j_CI_pr_yml_check_changelog
Expand All @@ -363,9 +393,9 @@ flowchart LR
r_api_process_docker_image_builds --> e_CI_job_test_container_builds
e_CI_job_test_container_builds --> j_CI_pr_yml_test_container_builds
r_api_process_makefile_phony -.-> gap_unenforced
r_api_process_pr_tracking_issue -.-> gap_unenforced
r_api_process_rule_machinery_code_owned -.-> gap_unenforced
r_api_process_rules_registry --> e_quality_guard_rules_registry
e_quality_guard_rules_registry --> j_CI_pr_yml_quality_guards
r_api_process_stacked_prs_full_ci_at_bottom --> e_pytest_tests_unit_test_ci_complete_gate_py
e_pytest_tests_unit_test_ci_complete_gate_py --> j_CI_pr_yml_test
e_pytest_tests_unit_test_ci_complete_gate_py --> j_CI_pr_yml_ci_complete
Expand Down
Loading
Loading