For AI agents: this page as Markdown · llms.txt · JSON Schema
#Architecture rule examples
Design rules that maintainers of well-known open-source projects enforce in review, written as Rerail rules. No stock linter checks them, and several projects say no tool does.
Each example shows breaking code, the rule in brief, its source, and the full rule with test cases that Rerail's test suite runs through the engine.
To use one:
- Open the example's full rule fold and copy its JSON into
draft.json. - Change the names and paths to your repository's.
- Run
rerail rules test draft.json, thenrerail rules add draft.json(Testing a draft).
Or describe the rule to /rerail-create and let the agent write it.
#Python
Rules that send control-silo reads, HTTP and client events through the one layer each project allows.
#Sentry: cell-silo code reads control-silo models through RPC
Users and integrations live in Sentry's control silo; a cell's database holds only its organizations' data. A
cell-silo query on User or Integration passes locally, where one database holds every table, and fails in
production. Asked to post a status check, an agent writes:
from sentry.integrations.models.integration import Integration
from sentry.models.repository import Repository
from sentry.silo.base import SiloMode
from sentry.tasks.base import instrumented_task
from sentry.taskworker.namespaces import preprod_tasks
@instrumented_task(
name="sentry.preprod.tasks.post_status_check",
namespace=preprod_tasks,
silo_mode=SiloMode.CELL,
)
def post_status_check(repository_id: int) -> None:
repository = Repository.objects.get(id=repository_id)
integration = Integration.objects.get(id=repository.integration_id)
installation = integration.get_installation(repository.organization_id)
installation.post_check()local:cell-silo-reads-control-through-rpcsteerFrom review: getsentry/sentry#98301.
Review quote and full rule: copy into draft.json
The review: “the Integration model is scoped to the control silo.” The fix it asked for: “So that we use the RPC to fetch the integration from the control silo.”
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:cell-silo-reads-control-through-rpc",
"revision": 1,
"mode": "steer",
"description": "Fetch users and integrations through their RPC services in cell-silo endpoints and tasks, not by querying the control-silo models, which a cell's database doesn't hold.",
"scope": { "include": ["src/sentry/**/*.py"], "exclude": ["src/sentry/testutils/**", "src/sentry/**/migrations/**"] },
"clauses": [
{
"id": "control-model-in-cell-silo",
"forbid": {
"ast": {
"language": "python",
"utils": {
"control-model": {
"any": [{ "kind": "identifier" }, { "kind": "attribute" }],
"resolves": {
"import": [
"sentry.users.models",
"sentry.integrations.models.Integration",
"sentry.integrations.models.integration.Integration",
"sentry.integrations.models.OrganizationIntegration",
"sentry.integrations.models.organization_integration.OrganizationIntegration"
],
"prefix": true
}
},
"cell-silo": {
"kind": "decorated_definition",
"has": {
"kind": "decorator",
"any": [
{
"has": {
"any": [{ "kind": "identifier" }, { "kind": "attribute" }],
"resolves": { "import": ["sentry.api.base.cell_silo_endpoint"] }
}
},
{ "regex": "(?s)^@instrumented_task\\(.*\\bsilo_mode\\s*=\\s*SiloMode\\.CELL\\b" }
]
}
}
},
"rule": {
"kind": "attribute",
"has": { "field": "object", "matches": "control-model", "pattern": "$MODEL" },
"regex": "\\.objects$",
"inside": { "matches": "cell-silo", "stopBy": "end" }
}
}
},
"violation": {
"code": "CONTROL_MODEL_IN_CELL_SILO",
"anchor": "witness.span",
"message": "$MODEL lives in the control silo, so this cell-silo code can't query it in production: fetch it through its RPC service (user_service, integration_service)."
}
}
]
},
"cases": {
"caught": [
{ "path": "src/sentry/preprod/vcs/status_checks/size/tasks.py", "content": "from sentry.integrations.models.integration import Integration\nfrom sentry.models.repository import Repository\nfrom sentry.silo.base import SiloMode\nfrom sentry.tasks.base import instrumented_task\nfrom sentry.taskworker.namespaces import preprod_tasks\n\n\n@instrumented_task(\n name=\"sentry.preprod.tasks.post_status_check\",\n namespace=preprod_tasks,\n silo_mode=SiloMode.CELL,\n)\ndef post_status_check(repository_id: int) -> None:\n repository = Repository.objects.get(id=repository_id)\n integration = Integration.objects.get(id=repository.integration_id)\n installation = integration.get_installation(repository.organization_id)\n installation.post_check()\n" },
{ "path": "src/sentry/issues/endpoints/organization_reviewers.py", "content": "from rest_framework.request import Request\nfrom rest_framework.response import Response\n\nfrom sentry.api.api_owners import ApiOwner\nfrom sentry.api.base import cell_silo_endpoint\nfrom sentry.api.bases.organization import OrganizationEndpoint\nfrom sentry.users.models import User as SentryUser\n\n\n@cell_silo_endpoint\nclass OrganizationReviewersEndpoint(OrganizationEndpoint):\n owner = ApiOwner.ISSUES\n\n def get(self, request: Request, organization) -> Response:\n users = SentryUser.objects.filter(id__in=request.GET.getlist(\"id\"))\n return Response([user.email for user in users])\n" },
{ "path": "src/sentry/integrations/api/endpoints/organization_integration_status.py", "content": "from sentry.api import base\nfrom sentry.api.bases.organization import OrganizationEndpoint\n\n\n@base.cell_silo_endpoint\nclass OrganizationIntegrationStatusEndpoint(OrganizationEndpoint):\n def get(self, request, organization, integration_id):\n from sentry.integrations.models import OrganizationIntegration\n\n exists = OrganizationIntegration.objects.filter(\n organization_id=organization.id, integration_id=integration_id\n ).exists()\n return self.respond({\"installed\": exists})\n" }
],
"passed": [
{ "path": "src/sentry/preprod/vcs/status_checks/size/tasks.py", "content": "from sentry.integrations.services.integration import integration_service\nfrom sentry.models.repository import Repository\nfrom sentry.silo.base import SiloMode\nfrom sentry.tasks.base import instrumented_task\nfrom sentry.taskworker.namespaces import preprod_tasks\n\n\n@instrumented_task(name=\"sentry.preprod.tasks.post_status_check\", namespace=preprod_tasks, silo_mode=SiloMode.CELL)\ndef post_status_check(repository_id: int) -> None:\n repository = Repository.objects.get(id=repository_id)\n integration = integration_service.get_integration(integration_id=repository.integration_id)\n integration.get_installation(organization_id=repository.organization_id).post_check()\n" },
{ "path": "src/sentry/issues/endpoints/organization_reviewers.py", "content": "from rest_framework.request import Request\nfrom rest_framework.response import Response\n\nfrom sentry.api.base import cell_silo_endpoint\nfrom sentry.api.bases.organization import OrganizationEndpoint\nfrom sentry.models.organizationmember import OrganizationMember\nfrom sentry.users.services.user.service import user_service\n\n\n@cell_silo_endpoint\nclass OrganizationReviewersEndpoint(OrganizationEndpoint):\n def get(self, request: Request, organization) -> Response:\n member_user_ids = OrganizationMember.objects.filter(\n organization_id=organization.id\n ).values_list(\"user_id\", flat=True)\n users = user_service.get_many_by_id(ids=list(member_user_ids))\n return Response([user.email for user in users])\n" },
{ "path": "src/sentry/users/api/endpoints/user_emails.py", "content": "from sentry.api.base import control_silo_endpoint\nfrom sentry.users.api.bases.user import UserEndpoint\nfrom sentry.users.models.useremail import UserEmail\n\n\n@control_silo_endpoint\nclass UserEmailsEndpoint(UserEndpoint):\n def get(self, request, user):\n emails = UserEmail.objects.filter(user=user)\n return self.respond([email.email for email in emails])\n" },
{ "path": "src/sentry/tasks/user_report.py", "content": "from sentry.models.userreport import UserReport\nfrom sentry.silo.base import SiloMode\nfrom sentry.tasks.base import instrumented_task\nfrom sentry.taskworker.namespaces import issues_tasks\nfrom sentry.users.models.user import User\n\n\n@instrumented_task(name=\"sentry.tasks.user_report.link\", namespace=issues_tasks, silo_mode=SiloMode.CELL)\ndef link_user_report(report_id: int, user: User | None = None) -> None:\n UserReport.objects.filter(id=report_id).update(user_id=user.id if user else None)\n" }
]
}
}#Home Assistant: integrations talk HTTP through a library and the shared session
Home Assistant keeps device protocols in PyPI libraries and hands them its shared web session. An integration's own
ClientSession adds a connection pool per config entry that never closes; an inline requests call puts protocol
code in core. A new integration's setup often starts like this:
"""The Pool Monitor integration."""
from aiohttp import ClientSession
from pool_monitor import PoolMonitorClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Pool Monitor from a config entry."""
client = PoolMonitorClient(entry.data[CONF_HOST], session=ClientSession())
entry.runtime_data = await client.get_device()
return Truelocal:integration-http-through-librarysteerFrom review: home-assistant/core#65090 and the development checklist.
Review quotes and full rule: copy into draft.json
The review: “please use async_get_clientsession both here and in the config flow to reduce sessions created”. The checklist: “All communication with devices and services goes through a Python library published on PyPI. The integration itself contains no protocol code.”
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:integration-http-through-library",
"revision": 1,
"mode": "steer",
"description": "Keep HTTP out of Home Assistant integrations: call the device's PyPI library and pass it Home Assistant's shared web session instead of opening one.",
"scope": {
"include": ["homeassistant/components/**/*.py"],
"exclude": [
"homeassistant/components/rest/**",
"homeassistant/components/rest_command/**",
"homeassistant/components/scrape/**"
]
},
"clauses": [
{
"id": "own-aiohttp-session",
"forbid": {
"ast": {
"language": "python",
"rule": {
"kind": "call",
"has": {
"field": "function",
"pattern": "$SESSION",
"any": [{ "kind": "identifier" }, { "kind": "attribute" }],
"resolves": { "import": ["aiohttp.ClientSession"] }
}
}
}
},
"violation": {
"code": "OWN_AIOHTTP_SESSION",
"anchor": "witness.span",
"message": "Give the library async_get_clientsession(hass) instead of opening a new $SESSION; use async_create_clientsession(hass) if its cookies must stay private."
}
},
{
"id": "own-httpx-client",
"forbid": {
"ast": {
"language": "python",
"rule": {
"kind": "call",
"has": {
"field": "function",
"pattern": "$SESSION",
"any": [{ "kind": "identifier" }, { "kind": "attribute" }],
"resolves": { "import": ["httpx.AsyncClient", "httpx.Client"] }
}
}
}
},
"violation": {
"code": "OWN_HTTPX_CLIENT",
"anchor": "witness.span",
"message": "Give the library get_async_client(hass) instead of opening a new $SESSION; use create_async_httpx_client(hass) if its cookies must stay private."
}
},
{
"id": "raw-http-call",
"forbid": {
"ast": {
"language": "python",
"rule": {
"kind": "call",
"has": {
"field": "function",
"pattern": "$CALL",
"any": [{ "kind": "identifier" }, { "kind": "attribute" }],
"resolves": {
"import": [
"requests.get",
"requests.post",
"requests.put",
"requests.patch",
"requests.delete",
"requests.head",
"requests.request",
"httpx.get",
"httpx.post",
"httpx.put",
"httpx.patch",
"httpx.delete",
"httpx.head",
"httpx.request",
"httpx.stream",
"aiohttp.request",
"urllib.request.urlopen"
]
}
}
}
}
},
"violation": {
"code": "HTTP_IN_INTEGRATION",
"anchor": "witness.span",
"message": "Move this HTTP call ($CALL) into the integration's PyPI library and call the library from here; integrations hold no protocol code."
}
}
]
},
"cases": {
"caught": [
{ "path": "homeassistant/components/pool_monitor/__init__.py", "content": "\"\"\"The Pool Monitor integration.\"\"\"\n\nfrom aiohttp import ClientSession\nfrom pool_monitor import PoolMonitorClient\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import CONF_HOST\nfrom homeassistant.core import HomeAssistant\n\n\nasync def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:\n \"\"\"Set up Pool Monitor from a config entry.\"\"\"\n client = PoolMonitorClient(entry.data[CONF_HOST], session=ClientSession())\n entry.runtime_data = await client.get_device()\n return True\n" },
{ "path": "homeassistant/components/pool_monitor/coordinator.py", "content": "\"\"\"Coordinator for Pool Monitor.\"\"\"\n\nimport requests\n\nfrom homeassistant.helpers.update_coordinator import DataUpdateCoordinator\n\n\nclass PoolMonitorCoordinator(DataUpdateCoordinator[dict]):\n def _fetch(self) -> dict:\n response = requests.get(f\"http://{self.host}/api/v1/status\", timeout=10)\n response.raise_for_status()\n return response.json()\n\n async def _async_update_data(self) -> dict:\n return await self.hass.async_add_executor_job(self._fetch)\n" },
{ "path": "homeassistant/components/pool_monitor/config_flow.py", "content": "\"\"\"Config flow for Pool Monitor.\"\"\"\n\nimport httpx as http\n\nfrom homeassistant.config_entries import ConfigFlow, ConfigFlowResult\n\n\nclass PoolMonitorConfigFlow(ConfigFlow, domain=\"pool_monitor\"):\n async def async_step_user(self, user_input=None) -> ConfigFlowResult:\n if user_input is not None:\n async with http.AsyncClient() as client:\n await client.get(f\"http://{user_input['host']}/api/v1/info\")\n return self.async_create_entry(title=\"Pool\", data=user_input)\n return self.async_show_form(step_id=\"user\")\n" },
{ "path": "homeassistant/components/pool_monitor/sensor.py", "content": "from aiohttp import request as http_request\n\n\nasync def _read_ph(host: str) -> float:\n async with http_request(\"GET\", f\"http://{host}/api/v1/ph\") as response:\n return (await response.json())[\"ph\"]\n" }
],
"passed": [
{ "path": "homeassistant/components/pool_monitor/__init__.py", "content": "\"\"\"The Pool Monitor integration.\"\"\"\n\nfrom pool_monitor import PoolMonitorClient\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import CONF_HOST\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.aiohttp_client import async_get_clientsession\n\n\nasync def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:\n \"\"\"Set up Pool Monitor from a config entry.\"\"\"\n session = async_get_clientsession(hass)\n client = PoolMonitorClient(entry.data[CONF_HOST], session=session)\n entry.runtime_data = await client.get_device()\n return True\n" },
{ "path": "homeassistant/components/pool_monitor/config_flow.py", "content": "\"\"\"Config flow for Pool Monitor.\"\"\"\n\nimport aiohttp\nfrom pool_monitor import PoolMonitorClient\n\nfrom homeassistant.config_entries import ConfigFlow, ConfigFlowResult\nfrom homeassistant.helpers.aiohttp_client import async_create_clientsession\n\n\nclass PoolMonitorConfigFlow(ConfigFlow, domain=\"pool_monitor\"):\n async def async_step_user(self, user_input=None) -> ConfigFlowResult:\n # The device sets a login cookie, so this flow gets its own session, not ClientSession().\n session = async_create_clientsession(self.hass)\n client = PoolMonitorClient(user_input[\"host\"], session=session, timeout=aiohttp.ClientTimeout(total=10))\n try:\n await client.get(\"info\")\n except aiohttp.ClientError:\n return self.async_show_form(step_id=\"user\", errors={\"base\": \"cannot_connect\"})\n return self.async_create_entry(title=\"Pool\", data=user_input)\n" },
{ "path": "homeassistant/components/pool_monitor/coordinator.py", "content": "\"\"\"Coordinator for Pool Monitor.\"\"\"\n\nfrom pool_monitor import PoolMonitorClient, request\n\nfrom homeassistant.helpers.update_coordinator import DataUpdateCoordinator\n\n\nclass PoolMonitorCoordinator(DataUpdateCoordinator[dict]):\n client: PoolMonitorClient\n\n async def _async_update_data(self) -> dict:\n return await self.client.send(request(\"status\"))\n" },
{ "path": "homeassistant/helpers/aiohttp_client.py", "content": "import aiohttp\n\n\ndef _async_create_clientsession(hass, **kwargs) -> aiohttp.ClientSession:\n return aiohttp.ClientSession(**kwargs)\n" }
]
}
}#Zulip: views validate; zerver/actions changes state and tells clients
In Zulip, a do_ function in zerver/actions/ makes a change, writes its RealmAuditLog row and sends the event
that updates open clients. A view that sends the event itself bypasses it, so other callers of the same change (an
endpoint, a management command, an import) send no event and clients go stale:
from django.http import HttpRequest, HttpResponse
from zerver.lib.drafts import access_draft
from zerver.lib.response import json_success
from zerver.models import UserProfile
from zerver.tornado.django_api import send_event_on_commit
def delete_draft(
request: HttpRequest, user_profile: UserProfile, draft_id: int
) -> HttpResponse:
draft = access_draft(user_profile, draft_id)
draft.delete()
event = {"type": "drafts", "op": "remove", "draft_id": draft_id}
send_event_on_commit(user_profile.realm, event, [user_profile.id])
return json_success(request)local:views-change-state-through-actionssteerFrom the architecture doc: Zulip directory structure.
Doc quote and full rule: copy into draft.json
On zerver/actions/: “we have a policy that all code calling send_event_on_commit to trigger pushing data to clients must live here.”
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:views-change-state-through-actions",
"revision": 1,
"mode": "steer",
"description": "Keep client events and audit-log writes out of Zulip views; a do_ function in zerver/actions makes the change, logs it and notifies clients.",
"scope": { "include": ["zerver/views/**/*.py"], "exclude": ["zerver/views/development/**"] },
"clauses": [
{
"id": "event-from-view",
"forbid": {
"ast": {
"language": "python",
"rule": {
"kind": "call",
"has": {
"field": "function",
"pattern": "$SEND",
"any": [{ "kind": "identifier" }, { "kind": "attribute" }],
"resolves": {
"import": [
"zerver.tornado.django_api.send_event_on_commit",
"zerver.tornado.django_api.send_event_rollback_unsafe"
]
}
}
}
}
},
"violation": {
"code": "EVENT_FROM_VIEW",
"anchor": "witness.span",
"message": "Move this change and its $SEND call into a do_ function in zerver/actions/ and call that from the view; views validate input, actions write and notify."
}
},
{
"id": "audit-log-from-view",
"forbid": {
"ast": {
"language": "python",
"rule": {
"kind": "call",
"has": {
"field": "function",
"kind": "attribute",
"regex": "\\.objects\\.(?:create|bulk_create)$",
"has": {
"field": "object",
"kind": "attribute",
"has": {
"field": "object",
"pattern": "$LOG",
"resolves": { "import": ["zerver.models.RealmAuditLog", "zerver.models.realm_audit_logs.RealmAuditLog"] }
}
}
}
}
}
},
"violation": {
"code": "AUDIT_LOG_FROM_VIEW",
"anchor": "witness.span",
"message": "Write this $LOG entry in the do_ function in zerver/actions/ that makes the change, so the change, its audit log and its event commit together."
}
}
]
},
"cases": {
"caught": [
{ "path": "zerver/views/drafts.py", "content": "from django.http import HttpRequest, HttpResponse\n\nfrom zerver.lib.drafts import access_draft\nfrom zerver.lib.response import json_success\nfrom zerver.models import UserProfile\nfrom zerver.tornado.django_api import send_event_on_commit\n\n\ndef delete_draft(\n request: HttpRequest, user_profile: UserProfile, draft_id: int\n) -> HttpResponse:\n draft = access_draft(user_profile, draft_id)\n draft.delete()\n event = {\"type\": \"drafts\", \"op\": \"remove\", \"draft_id\": draft_id}\n send_event_on_commit(user_profile.realm, event, [user_profile.id])\n return json_success(request)\n" },
{ "path": "zerver/views/realm_linkifiers.py", "content": "from django.utils.timezone import now as timezone_now\n\nfrom zerver.lib.response import json_success\nfrom zerver.models import RealmAuditLog, RealmFilter\nfrom zerver.models.realm_audit_logs import AuditLogEventType\nfrom zerver.tornado import django_api\n\n\ndef delete_linkifier(request, user_profile, filter_id: int):\n RealmFilter.objects.filter(realm=user_profile.realm, id=filter_id).delete()\n RealmAuditLog.objects.create(\n realm=user_profile.realm,\n acting_user=user_profile,\n event_type=AuditLogEventType.REALM_LINKIFIER_REMOVED,\n event_time=timezone_now(),\n )\n django_api.send_event_on_commit(user_profile.realm, {\"type\": \"realm_linkifiers\"}, [])\n return json_success(request)\n" },
{ "path": "zerver/views/user_settings.py", "content": "from zerver.tornado.django_api import send_event_rollback_unsafe as send_event\n\n\ndef mark_tour_seen(request, user_profile):\n user_profile.tutorial_status = \"F\"\n user_profile.save(update_fields=[\"tutorial_status\"])\n send_event(user_profile.realm, {\"type\": \"onboarding_steps\"}, [user_profile.id])\n" }
],
"passed": [
{ "path": "zerver/views/drafts.py", "content": "from django.http import HttpRequest, HttpResponse\n\nfrom zerver.actions.drafts import do_delete_draft\nfrom zerver.lib.response import json_success\nfrom zerver.models import UserProfile\n\n\ndef delete_draft(request: HttpRequest, user_profile: UserProfile, draft_id: int) -> HttpResponse:\n do_delete_draft(draft_id, user_profile)\n return json_success(request)\n" },
{ "path": "zerver/views/registration.py", "content": "from zerver.models import RealmAuditLog\nfrom zerver.models.realm_audit_logs import AuditLogEventType\n\n\ndef password_was_changed(importing_user) -> bool:\n return RealmAuditLog.objects.filter(\n modified_user=importing_user, event_type=AuditLogEventType.USER_PASSWORD_CHANGED\n ).exists()\n" },
{ "path": "zerver/views/events_register.py", "content": "from zerver.lib.events import do_events_register\nfrom zerver.lib.response import json_success\n\n\ndef events_register_backend(request, user_profile, event_types=None):\n # Clients receive what send_event_on_commit() pushes through this queue.\n ret = do_events_register(user_profile, request.client, event_types=event_types)\n return json_success(request, data=ret)\n" },
{ "path": "zerver/actions/drafts.py", "content": "from zerver.models import Draft, UserProfile\nfrom zerver.tornado.django_api import send_event_on_commit\n\n\ndef do_delete_draft(draft_id: int, user_profile: UserProfile) -> None:\n Draft.objects.filter(user_profile=user_profile, id=draft_id).delete()\n event = {\"type\": \"drafts\", \"op\": \"remove\", \"draft_id\": draft_id}\n send_event_on_commit(user_profile.realm, event, [user_profile.id])\n" }
]
}
}#TypeScript and Vue
Rules for layer boundaries, package imports and dependency injection in TypeScript and Vue codebases.
#Immich (NestJS): services reach the filesystem through repositories
Immich keeps technology in src/repositories and business logic in src/services, so service tests mock each repository. A service calling node:fs needs a real disk to test, and the storage repository is no longer the one place the server touches files.
A service that resets a file's modified time calls node:fs itself:
import { Injectable } from '@nestjs/common';
import { utimes } from 'node:fs/promises';
import { BaseService } from 'src/services/base.service';
import { UploadFile } from 'src/types';
@Injectable()
export class AssetMediaService extends BaseService {
private async restoreModifiedAt(file: UploadFile, modifiedAt: Date) {
await utimes(file.originalPath, new Date(), modifiedAt);
}
}local:services-go-through-repositoriessteerFrom review: immich-app/immich#7010 and the architecture doc.
Review quotes and full rule: copy into draft.json
A regex can't tell the fs function utimes(...) from a repository method of that name; the rule follows each name to
its import.
The review: “We don't call fs methods directly in services. Can you just add this to the storage repository and use that here instead?” The architecture doc: “we aim to separate technology specific implementations (src/repositories) from core business logic (src/services).”
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:services-go-through-repositories",
"revision": 1,
"mode": "steer",
"description": "Reach the filesystem and child processes from server services through the storage and process repositories, not node:fs or node:child_process; services stay testable with mocks.",
"scope": { "include": ["server/src/services/**/*.ts"], "exclude": ["**/*.spec.ts"] },
"clauses": [
{
"id": "fs-call",
"forbid": {
"ast": {
"language": "typescript",
"utils": {
"from-fs": {
"kind": "identifier",
"resolves": {
"binding": {
"site": {
"inside": {
"kind": "import_statement",
"has": { "field": "source", "regex": "^['\"](?:node:)?fs(?:/promises)?['\"]$" },
"stopBy": "end"
}
}
}
}
}
},
"rule": {
"kind": "call_expression",
"has": {
"field": "function",
"pattern": "$FN",
"any": [
{ "kind": "identifier", "matches": "from-fs" },
{ "kind": "member_expression", "has": { "field": "object", "kind": "identifier", "matches": "from-fs" } }
]
}
}
}
},
"violation": {
"code": "FS_CALL_IN_SERVICE",
"anchor": "witness.span",
"message": "Services don't call fs directly: call this.storageRepository instead of $FN, adding a StorageRepository method if none fits, so service tests can mock it."
}
},
{
"id": "child-process-call",
"forbid": {
"ast": {
"language": "typescript",
"utils": {
"from-cp": {
"kind": "identifier",
"resolves": {
"binding": {
"site": {
"inside": {
"kind": "import_statement",
"has": { "field": "source", "regex": "^['\"](?:node:)?child_process['\"]$" },
"stopBy": "end"
}
}
}
}
}
},
"rule": {
"kind": "call_expression",
"has": {
"field": "function",
"pattern": "$FN",
"any": [
{ "kind": "identifier", "matches": "from-cp" },
{ "kind": "member_expression", "has": { "field": "object", "kind": "identifier", "matches": "from-cp" } }
]
}
}
}
},
"violation": {
"code": "PROCESS_CALL_IN_SERVICE",
"anchor": "witness.span",
"message": "Services don't start processes directly: call this.processRepository (spawn, spawnDuplexStream) instead of $FN, adding a ProcessRepository method if none fits."
}
}
]
},
"cases": {
"caught": [
{ "path": "server/src/services/asset-media.service.ts", "content": "import { Injectable } from '@nestjs/common';\nimport { utimes } from 'node:fs/promises';\nimport { BaseService } from 'src/services/base.service';\nimport { UploadFile } from 'src/types';\n\n@Injectable()\nexport class AssetMediaService extends BaseService {\n private async restoreModifiedAt(file: UploadFile, modifiedAt: Date) {\n await utimes(file.originalPath, new Date(), modifiedAt);\n }\n}\n" },
{ "path": "server/src/services/video-frame.service.ts", "content": "import { Injectable } from '@nestjs/common';\nimport * as fs from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { BaseService } from 'src/services/base.service';\n\n@Injectable()\nexport class VideoFrameService extends BaseService {\n async extract(assetId: string) {\n const tempDir = await fs.mkdtemp(join(tmpdir(), `immich-video-frames-${assetId}-`));\n try {\n return await this.mediaRepository.extractFrames(assetId, tempDir);\n } finally {\n await fs.rm(tempDir, { recursive: true, force: true });\n }\n }\n}\n" },
{ "path": "server/src/services/media.service.ts", "content": "import { Injectable } from '@nestjs/common';\nimport { spawn as run } from 'node:child_process';\nimport { BaseService } from 'src/services/base.service';\n\n@Injectable()\nexport class MediaService extends BaseService {\n probeHardware() {\n return run('vainfo', ['--display', 'drm']);\n }\n}\n" }
],
"passed": [
{ "path": "server/src/services/asset-media.service.ts", "content": "import { Injectable } from '@nestjs/common';\nimport { BaseService } from 'src/services/base.service';\nimport { UploadFile } from 'src/types';\n\n@Injectable()\nexport class AssetMediaService extends BaseService {\n private async restoreModifiedAt(file: UploadFile, modifiedAt: Date) {\n await this.storageRepository.utimes(file.originalPath, new Date(), modifiedAt);\n }\n}\n" },
{ "path": "server/src/services/library.service.ts", "content": "import { Injectable } from '@nestjs/common';\nimport { R_OK } from 'node:constants';\nimport { Stats } from 'node:fs';\nimport path from 'node:path';\nimport { BaseService } from 'src/services/base.service';\n\n@Injectable()\nexport class LibraryService extends BaseService {\n async isReadable(file: string, stat: Stats) {\n return stat.isFile() && path.isAbsolute(file) && (await this.storageRepository.checkFileExists(file, R_OK));\n }\n}\n" },
{ "path": "server/src/services/media.service.ts", "content": "import { Injectable } from '@nestjs/common';\nimport { BaseService } from 'src/services/base.service';\n\n@Injectable()\nexport class MediaService extends BaseService {\n async remove(paths: string[]) {\n const readFile = (file: string) => this.storageRepository.readFile(file);\n await Promise.all(paths.map((file) => readFile(file)));\n return this.processRepository.spawn('vainfo', ['--display', 'drm']);\n }\n}\n" },
{ "path": "server/src/repositories/storage.repository.ts", "content": "import { Injectable } from '@nestjs/common';\nimport fs from 'node:fs/promises';\n\n@Injectable()\nexport class StorageRepository {\n utimes(filepath: string, atime: Date, mtime: Date) {\n return fs.utimes(filepath, atime, mtime);\n }\n}\n" }
]
}
}#n8n (Vue): frontend modules import only the shared packages
An n8n frontend module (packages/) may import only the shared L0–L2 packages, not @/… (the
editor shell that loads it) or sibling modules. A shell import couples the module to shell internals, yet type checks
and builds pass once the dependency is declared. Asked to show a workflow's name in the inbox, an agent imports the shell's store:
<script setup lang="ts">
import { computed } from 'vue';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useInboxStore } from '../inbox.store';
const inboxStore = useInboxStore();
const workflowsStore = useWorkflowsStore();
const title = computed(
() => workflowsStore.workflowsById[inboxStore.activeWorkflowId]?.name,
);
</script>
<template>
<h2>{{ title }}</h2>
</template>local:frontend-modules-stay-in-boundssteerFrom the architecture doc: n8n-io/n8n frontend-module-guide.md.
Doc quotes and full rule: copy into draft.json
The guide: “Never import another @n8n/frontend-module-*. Never import @/…, because @/… is the shell.” and “Until that rule lands, the boundary is the responsibility of the reviewer.”
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:frontend-modules-stay-in-bounds",
"revision": 1,
"mode": "steer",
"description": "Keep n8n frontend modules on the shared packages: no imports of the editor shell or of another module, and no store reads at the top of a descriptor.",
"scope": {
"include": ["packages/modules/*/frontend/src/**/*.ts", "packages/modules/*/frontend/src/**/*.vue"],
"exclude": ["**/*.test.ts", "**/__tests__/**"]
},
"clauses": [
{
"id": "shell-import-vue",
"forbid": {
"ast": {
"language": "vue",
"embedded": "script-block",
"rule": {
"kind": "string",
"pattern": "$MODULE",
"regex": "^['\"](?:@/|@n8n/frontend-module-)",
"not": { "regex": "^['\"]@n8n/frontend-module-sdk(?:/|['\"])" },
"any": [
{ "inside": { "field": "source", "any": [{ "kind": "import_statement" }, { "kind": "export_statement" }] } },
{
"inside": {
"kind": "arguments",
"inside": { "kind": "call_expression", "has": { "field": "function", "kind": "import" } }
}
}
]
}
}
},
"violation": {
"code": "MODULE_IMPORTS_SHELL",
"anchor": "witness.span",
"message": "A frontend module can't import $MODULE: modules use L0–L2 packages only. Import a subpath such as @n8n/stores/settings.store, or reach the shell through a capability."
}
},
{
"id": "shell-import-ts",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"kind": "string",
"pattern": "$MODULE",
"regex": "^['\"](?:@/|@n8n/frontend-module-)",
"not": { "regex": "^['\"]@n8n/frontend-module-sdk(?:/|['\"])" },
"any": [
{ "inside": { "field": "source", "any": [{ "kind": "import_statement" }, { "kind": "export_statement" }] } },
{
"inside": {
"kind": "arguments",
"inside": { "kind": "call_expression", "has": { "field": "function", "kind": "import" } }
}
}
]
}
}
},
"violation": {
"code": "MODULE_IMPORTS_SHELL",
"anchor": "witness.span",
"message": "A frontend module can't import $MODULE: modules use L0–L2 packages only. Import a subpath such as @n8n/stores/settings.store, or reach the shell through a capability."
}
},
{
"id": "store-at-descriptor-top",
"forbid": {
"ast": {
"language": "typescript",
"file": { "path": { "regex": "\\.module\\.ts$" } },
"rule": {
"kind": "call_expression",
"has": { "field": "function", "kind": "identifier", "regex": "^use[A-Z]", "pattern": "$CALL" },
"not": {
"inside": {
"any": [
{ "kind": "arrow_function" },
{ "kind": "function_expression" },
{ "kind": "function_declaration" },
{ "kind": "method_definition" },
{ "kind": "generator_function_declaration" }
],
"stopBy": "end"
}
}
}
}
},
"violation": {
"code": "STORE_AT_DESCRIPTOR_TOP",
"anchor": "witness.span",
"message": "$CALL() at the top of a descriptor runs when main.ts imports it, before the shell installs Pinia: call it inside a route guard, a handler or setup."
}
}
]
},
"cases": {
"caught": [
{ "path": "packages/modules/inbox/frontend/src/components/InboxHeader.vue", "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue';\nimport { useWorkflowsStore } from '@/app/stores/workflows.store';\nimport { useInboxStore } from '../inbox.store';\n\nconst inboxStore = useInboxStore();\nconst workflowsStore = useWorkflowsStore();\nconst title = computed(\n\t() => workflowsStore.workflowsById[inboxStore.activeWorkflowId]?.name,\n);\n</script>\n\n<template>\n\t<h2>{{ title }}</h2>\n</template>\n" },
{ "path": "packages/modules/apps/frontend/src/composables/useAppInstances.ts", "content": "import { computed } from 'vue';\nimport { useInstanceRegistryStore } from '@n8n/frontend-module-instance-registry';\n\nexport function useAppInstances() {\n\tconst registry = useInstanceRegistryStore();\n\treturn computed(() => registry.instances.filter((instance) => instance.hasApps));\n}\n" },
{ "path": "packages/modules/otel/frontend/src/otel.routes.ts", "content": "import type { RouteRecordRaw } from 'vue-router';\n\nexport const otelRoutes: RouteRecordRaw[] = [\n\t{\n\t\tname: 'otel-settings',\n\t\tpath: 'otel',\n\t\tcomponent: async () => await import('@/features/settings/views/SettingsOtelView.vue'),\n\t},\n];\n" },
{ "path": "packages/modules/otel/frontend/src/otel.module.ts", "content": "import { defineFrontendModule } from '@n8n/frontend-module-sdk';\nimport { useSettingsStore } from '@n8n/stores/settings.store';\nimport { OTEL_SETTINGS_VIEW } from './otel.constants';\n\nconst settingsStore = useSettingsStore();\nconst OtelSettingsView = async () => await import('./views/OtelSettingsView.vue');\n\nexport const OtelModule = defineFrontendModule({\n\tid: 'otel',\n\tname: 'OpenTelemetry',\n\troutes: [\n\t\t{\n\t\t\tname: OTEL_SETTINGS_VIEW,\n\t\t\tpath: 'otel',\n\t\t\tcomponent: OtelSettingsView,\n\t\t\tbeforeEnter: () => settingsStore.isModuleActive('otel'),\n\t\t},\n\t],\n});\n" }
],
"passed": [
{ "path": "packages/modules/inbox/frontend/src/components/InboxHeader.vue", "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue';\nimport { useI18n } from '@n8n/i18n';\nimport { useInboxStore } from '../inbox.store';\n\nconst i18n = useI18n();\nconst inboxStore = useInboxStore();\nconst title = computed(() => inboxStore.activeWorkflowName ?? i18n.baseText('generic.unknown'));\n</script>\n\n<template>\n\t<h2>{{ title }}</h2>\n\t<p>See @/app/stores in the editor for the shell's stores.</p>\n</template>\n" },
{ "path": "packages/modules/otel/frontend/src/otel.module.ts", "content": "import { defineFrontendModule } from '@n8n/frontend-module-sdk';\nimport { useSettingsStore } from '@n8n/stores/settings.store';\nimport { OTEL_SETTINGS_VIEW } from './otel.constants';\n\nconst OtelSettingsView = async () => await import('./views/OtelSettingsView.vue');\n\nexport const OtelModule = defineFrontendModule({\n\tid: 'otel',\n\tname: 'OpenTelemetry',\n\troutes: [\n\t\t{\n\t\t\tname: OTEL_SETTINGS_VIEW,\n\t\t\tpath: 'otel',\n\t\t\tcomponent: OtelSettingsView,\n\t\t\tbeforeEnter: () => useSettingsStore().isModuleActive('otel'),\n\t\t},\n\t],\n});\n" },
{ "path": "packages/modules/otel/frontend/src/otel.store.ts", "content": "import { defineStore } from 'pinia';\nimport { ref } from 'vue';\nimport { capabilityRegistry, capabilities } from '@n8n/frontend-module-sdk';\nimport { useRootStore } from '@n8n/stores/useRootStore';\nimport { getOtelSettings } from './otel.api';\n\nexport const useOtelStore = defineStore('otel', () => {\n\tconst rootStore = useRootStore();\n\tconst endpoint = ref('');\n\tconst load = async () => {\n\t\tendpoint.value = (await getOtelSettings(rootStore.restApiContext)).endpoint;\n\t};\n\tconst openSettings = () => capabilityRegistry.use(capabilities.openSettings)('otel');\n\treturn { endpoint, load, openSettings };\n});\n" },
{ "path": "packages/frontend/editor-ui/src/app/modules.manifest.ts", "content": "import { InstanceRegistryModule } from '@n8n/frontend-module-instance-registry';\nimport { useUIStore } from '@/app/stores/ui.store';\n\nexport const modules = [InstanceRegistryModule];\nexport const ui = () => useUIStore();\n" }
]
}
}#VS Code: services come in through the constructor
VS Code builds its service graph from constructor parameters. @IEditorService tells the instantiation service what a class needs, so it creates dependencies first and reports cycles. Holding IInstantiationService and later calling invokeFunction( hides the dependency and creates the service when that line first runs.
A chat widget that opens an editor resolves the editor service when it runs:
import { URI } from '../../../../../base/common/uri.js';
import {
IInstantiationService,
} from '../../../../../platform/instantiation/common/instantiation.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
export class ChatAttachmentWidget {
constructor(
private readonly resource: URI,
@IInstantiationService
private readonly instantiationService: IInstantiationService,
) { }
open(): void {
const editorService = this.instantiationService.invokeFunction(
accessor => accessor.get(IEditorService),
);
editorService.openEditor({ resource: this.resource });
}
}local:inject-services-in-constructorssteerFrom the architecture docs: microsoft/vscode coding guidelines and the Source Code Organization wiki.
Doc quotes and full rule: copy into draft.json
A regex for accessor.get( would also flag every action's run(accessor), where it is the documented way in; the rule
looks only inside callbacks passed to invokeFunction.
The coding guidelines: “Declare service dependencies in constructors via DI — never access services through IInstantiationService elsewhere.” The wiki: “Services get to its clients via constructor injection.”
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:inject-services-in-constructors",
"revision": 1,
"mode": "steer",
"description": "Declare the services a VS Code class needs as constructor parameters, not by resolving them later through the instantiation service's invokeFunction.",
"scope": {
"include": [
"src/vs/workbench/contrib/**/*.ts",
"src/vs/workbench/services/**/*.ts",
"src/vs/sessions/contrib/**/*.ts",
"src/vs/sessions/services/**/*.ts",
"src/vs/editor/contrib/**/*.ts"
],
"exclude": ["**/test/**"]
},
"clauses": [
{
"id": "service-from-invoke-function",
"forbid": {
"ast": {
"language": "typescript",
"utils": {
"invoke-callback": {
"any": [{ "kind": "arrow_function" }, { "kind": "function_expression" }],
"inside": {
"kind": "arguments",
"inside": {
"kind": "call_expression",
"has": { "field": "function", "kind": "member_expression", "has": { "field": "property", "regex": "^invokeFunction$" } }
}
}
}
},
"rule": {
"kind": "call_expression",
"all": [
{
"has": {
"field": "function",
"kind": "member_expression",
"all": [
{ "has": { "field": "property", "regex": "^get$" } },
{
"has": {
"field": "object",
"kind": "identifier",
"resolves": {
"binding": {
"site": {
"inside": {
"matches": "invoke-callback",
"stopBy": {
"any": [
{ "kind": "arrow_function" },
{ "kind": "function_expression" },
{ "kind": "function_declaration" },
{ "kind": "method_definition" },
{ "kind": "generator_function_declaration" }
]
}
}
}
}
}
}
}
]
}
},
{ "has": { "field": "arguments", "has": { "kind": "identifier", "pattern": "$SVC" } } }
],
"inside": { "kind": "class_body", "stopBy": "end" }
}
}
},
"violation": {
"code": "SERVICE_FROM_INVOKE_FUNCTION",
"anchor": "witness.span",
"message": "Inject $SVC as a constructor parameter (@$SVC) instead of resolving it through invokeFunction; if that makes a cycle, pass it in from the code that wires this class."
}
}
]
},
"cases": {
"caught": [
{ "path": "src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidget.ts", "content": "import { URI } from '../../../../../base/common/uri.js';\nimport {\n\tIInstantiationService,\n} from '../../../../../platform/instantiation/common/instantiation.js';\nimport { IEditorService } from '../../../../services/editor/common/editorService.js';\n\nexport class ChatAttachmentWidget {\n\tconstructor(\n\t\tprivate readonly resource: URI,\n\t\t@IInstantiationService\n\t\tprivate readonly instantiationService: IInstantiationService,\n\t) { }\n\n\topen(): void {\n\t\tconst editorService = this.instantiationService.invokeFunction(\n\t\t\taccessor => accessor.get(IEditorService),\n\t\t);\n\t\teditorService.openEditor({ resource: this.resource });\n\t}\n}\n" },
{ "path": "src/vs/sessions/services/configuration/browser/configurationService.ts", "content": "import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';\nimport { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';\nimport { IEditorService } from '../../../../workbench/services/editor/common/editorService.js';\n\nexport class SessionsConfigurationService {\n\tconstructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { }\n\n\tprivate reportWriteError(error: Error, settingsResource: URI): void {\n\t\tthis.instantiationService.invokeFunction((accessor: ServicesAccessor) => {\n\t\t\tconst notificationService = accessor.get(INotificationService);\n\t\t\tconst editorService = accessor.get(IEditorService);\n\t\t\tnotificationService.prompt(Severity.Error, error.message, [{\n\t\t\t\tlabel: 'Open Settings',\n\t\t\t\trun: () => editorService.openEditor({ resource: settingsResource }),\n\t\t\t}]);\n\t\t});\n\t}\n}\n" },
{ "path": "src/vs/editor/contrib/hover/browser/contentHoverController.ts", "content": "export class ContentHoverController {\n\tconstructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { }\n\n\tprivate get _keybindingService(): IKeybindingService {\n\t\treturn this._instantiationService.invokeFunction(a => a.get(IKeybindingService));\n\t}\n}\n" }
],
"passed": [
{ "path": "src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidget.ts", "content": "import { URI } from '../../../../../base/common/uri.js';\nimport { IEditorService } from '../../../../services/editor/common/editorService.js';\n\nexport class ChatAttachmentWidget {\n\tconstructor(\n\t\tprivate readonly resource: URI,\n\t\t@IEditorService private readonly editorService: IEditorService,\n\t) { }\n\n\topen(): void {\n\t\tthis.editorService.openEditor({ resource: this.resource });\n\t}\n}\n" },
{ "path": "src/vs/workbench/contrib/chat/browser/actions/chatOpenAttachmentAction.ts", "content": "import { Action2 } from '../../../../../platform/actions/common/actions.js';\nimport { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';\nimport { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';\nimport { IEditorService } from '../../../../services/editor/common/editorService.js';\n\nexport class OpenAttachmentAction extends Action2 {\n\tasync run(accessor: ServicesAccessor, resource: URI): Promise<void> {\n\t\tconst editorService = accessor.get(IEditorService);\n\t\tawait editorService.openEditor({ resource });\n\t}\n}\n\nCommandsRegistry.registerCommand('chat.openAttachment', (accessor, resource: URI) => accessor.get(IEditorService).openEditor({ resource }));\n" },
{ "path": "src/vs/sessions/services/configuration/browser/configurationService.ts", "content": "import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';\nimport { ConfigurationWriteErrorReporter } from './configurationWriteErrorReporter.js';\n\nexport class SessionsConfigurationService {\n\tprivate errorReporter: ConfigurationWriteErrorReporter | undefined;\n\n\tconstructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { }\n\n\tprivate reportWriteError(error: Error, settingsResource: URI): void {\n\t\tthis.errorReporter ??= this.instantiationService.createInstance(ConfigurationWriteErrorReporter);\n\t\tthis.errorReporter.report(error, settingsResource);\n\t}\n\n\tprivate lookup(id: string): string | undefined {\n\t\treturn this.instantiationService.invokeFunction(() => this.cache.get(id));\n\t}\n}\n" },
{ "path": "src/vs/workbench/contrib/chat/test/browser/chatAttachmentWidget.test.ts", "content": "suite('ChatAttachmentWidget', () => {\n\ttest('opens the resource', () => {\n\t\tconst editorService = instantiationService.invokeFunction(accessor => accessor.get(IEditorService));\n\t\tassert.ok(editorService);\n\t});\n});\n" }
]
}
}#Rust
Rules that keep IO, file access and environment-variable names in the one place each project allows.
#rust-analyzer: the ide and hir crates do no IO
rust-analyzer's ide, hir and base-db crates compute answers only from inputs the server has loaded, so analysis
can't fail on the outside world and tests stay in-process. Only the rust-analyzer crate, which talks to the editor,
touches files, processes and the network. Asked for an SVG of the crate graph, an agent runs dot where the graph is
built:
use std::{fs, process::Command};
use ide_db::RootDatabase;
pub(crate) fn view_crate_graph(db: &RootDatabase) -> Result<String, String> {
let path = std::env::temp_dir().join("crate-graph.dot");
fs::write(&path, render_dot(db)).map_err(|e| e.to_string())?;
let output = Command::new("dot")
.arg("-Tsvg")
.arg(&path)
.output()
.map_err(|e| e.to_string())?;
String::from_utf8(output.stdout).map_err(|e| e.to_string())
}local:no-io-in-ide-and-hirsteerFrom review: rust-lang/rust-analyzer#8801, its follow-up #8807 and the architecture doc.
Review quotes and full rule: copy into draft.json
The review: “It seems that the layering is slightly off here, we generally don’t do IO, including IPC, in the ide layer and below.” The follow-up moved the dot call into the rust-analyzer crate. The architecture doc: “Only parts touching LSP are allowed to do IO.”
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:no-io-in-ide-and-hir",
"revision": 1,
"mode": "steer",
"description": "Keep file, process and network access out of rust-analyzer's ide, hir and base-db crates; only the rust-analyzer crate, which talks to the editor, does IO.",
"scope": {
"include": [
"crates/ide/src/**/*.rs",
"crates/ide-*/src/**/*.rs",
"crates/hir/src/**/*.rs",
"crates/hir-*/src/**/*.rs",
"crates/base-db/src/**/*.rs"
],
"exclude": ["**/tests.rs", "**/tests/**", "**/test_data/**"]
},
"clauses": [
{
"id": "io-call",
"forbid": {
"ast": {
"language": "rust",
"rule": {
"kind": "call_expression",
"has": {
"field": "function",
"any": [{ "kind": "scoped_identifier" }, { "kind": "identifier" }],
"pattern": "$CALL",
"resolves": {
"import": [
"std::fs",
"std::process::Command",
"std::net",
"std::env::current_dir",
"std::env::set_current_dir",
"std::env::temp_dir",
"toolchain::command"
],
"prefix": true
}
},
"not": {
"inside": {
"kind": "mod_item",
"follows": { "kind": "attribute_item", "regex": "^#\\[cfg\\(test\\)\\]$" },
"stopBy": "end"
}
}
}
}
},
"violation": {
"code": "IO_IN_IDE_LAYER",
"anchor": "witness.span",
"message": "ide, hir and base-db do no IO: move $CALL into the rust-analyzer crate, which owns files, processes and the network, and pass its result in or return data for it to use."
}
}
]
},
"cases": {
"caught": [
{ "path": "crates/ide/src/view_crate_graph.rs", "content": "use std::{fs, process::Command};\n\nuse ide_db::RootDatabase;\n\npub(crate) fn view_crate_graph(db: &RootDatabase) -> Result<String, String> {\n let path = std::env::temp_dir().join(\"crate-graph.dot\");\n fs::write(&path, render_dot(db)).map_err(|e| e.to_string())?;\n let output = Command::new(\"dot\")\n .arg(\"-Tsvg\")\n .arg(&path)\n .output()\n .map_err(|e| e.to_string())?;\n String::from_utf8(output.stdout).map_err(|e| e.to_string())\n}\n" },
{ "path": "crates/hir-expand/src/builtin/fn_macro.rs", "content": "fn include_str_expand(\n db: &dyn SourceDatabase,\n arg_id: MacroCallId,\n tt: &tt::TopSubtree,\n call_site: Span,\n) -> ExpandResult<tt::TopSubtree> {\n let (path, input_span) = match parse_string(tt, false) {\n Ok(it) => it,\n Err(e) => return ExpandResult::new(tt::TopSubtree::empty(call_site.into()), e),\n };\n let text = match relative_file(db, arg_id, path.as_str(), true, input_span) {\n Ok(file_id) => db.file_text(file_id.file_id(db)).text(db).to_string(),\n // Excluded files aren't loaded: read them from disk instead.\n Err(_) => std::fs::read_to_string(path.as_str()).unwrap_or_default(),\n };\n ExpandResult::ok(quote!(call_site =>#text))\n}\n" },
{ "path": "crates/ide/src/expand_macro.rs", "content": "use std::{io::Write, process::Stdio};\n\nuse ide_db::{FxHashMap, RootDatabase, base_db::Crate};\nuse toolchain::{Tool, command as tool_command};\n\nfn format_expansion(db: &RootDatabase, krate: Crate, expansion: &str) -> Option<String> {\n let mut rustfmt = tool_command(Tool::Rustfmt.path(), \".\", &FxHashMap::default());\n rustfmt.arg(\"--edition\").arg(krate.data(db).edition.to_string());\n let mut child = rustfmt.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn().ok()?;\n child.stdin.take()?.write_all(expansion.as_bytes()).ok()?;\n String::from_utf8(child.wait_with_output().ok()?.stdout).ok()\n}\n" }
],
"passed": [
{ "path": "crates/ide/src/view_crate_graph.rs", "content": "use dot::{Id, LabelText};\nuse ide_db::{RootDatabase, base_db::all_crates};\n\n// Returns the crate graph in .dot format; the client renders it.\npub(crate) fn view_crate_graph(db: &RootDatabase, full: bool) -> String {\n let graph = DotCrateGraph::new(db, all_crates(db), full);\n let mut dot = Vec::new();\n dot::render(&graph, &mut dot).unwrap();\n String::from_utf8(dot).unwrap()\n}\n" },
{ "path": "crates/ide/src/hover/render.rs", "content": "use std::env;\n\nuse hir::HirDisplay;\n\npub(super) fn closure_ty(sema: &Semantics<'_, RootDatabase>, closure: hir::Closure) -> Option<String> {\n let mut markup = closure.display_with_impl(sema.db, DisplayTarget::default()).to_string();\n if env::var_os(\"RA_DEV\").is_some() {\n format_to!(markup, \"\\n{:?}\", closure);\n }\n Some(markup)\n}\n" },
{ "path": "crates/rust-analyzer/src/handlers/request.rs", "content": "pub(crate) fn handle_view_crate_graph(\n snap: GlobalStateSnapshot,\n params: ViewCrateGraphParams,\n) -> anyhow::Result<String> {\n let dot = snap.analysis.view_crate_graph(params.full)?;\n let output = toolchain::command(\"dot\", snap.config.root_path(), &FxHashMap::default())\n .arg(\"-Tsvg\")\n .stdin(std::process::Stdio::piped())\n .output()?;\n let _ = dot;\n Ok(String::from_utf8(output.stdout)?)\n}\n" },
{ "path": "crates/ide/src/file_structure.rs", "content": "pub(crate) fn file_structure(file: &SourceFile) -> Vec<StructureNode> {\n structure(file.syntax())\n}\n\n#[cfg(test)]\nmod tests {\n use std::fs;\n\n #[test]\n fn structure_of_big_file() {\n let text = fs::read_to_string(\"test_data/big.rs\").unwrap();\n let file = SourceFile::parse(&text, Edition::CURRENT).tree();\n assert!(!super::file_structure(&file).is_empty());\n }\n}\n" },
{ "path": "crates/ide/src/highlight_related.rs", "content": "mod process {\n pub(super) fn exit_points(body: &ast::BlockExpr) -> Vec<TextRange> {\n body.syntax().descendants().filter_map(ast::ReturnExpr::cast).map(|it| it.syntax().text_range()).collect()\n }\n}\n\npub(crate) fn highlight_exit_points(body: &ast::BlockExpr) -> Vec<TextRange> {\n process::exit_points(body)\n}\n" }
]
}
}#Zed: project files go through Fs, not std::fs
Zed edits local, SSH and collaboration projects, and tests against an in-memory FakeFs, because file access goes
through the Fs trait and the Worktree. A std::fs call reads the local disk on whatever thread it runs on, does
nothing useful for a remote project, and escapes FakeFs in tests. Asked to paste an image into a Markdown file, an
agent writes it straight to disk:
use std::fs;
use gpui::{Context, Image, Window};
impl Editor {
fn paste_image(
&mut self,
image: &Image,
dir: &Path,
window: &mut Window,
cx: &mut Context<Self>,
) -> anyhow::Result<()> {
let file_name = format!("image.{}", image.format.extension());
fs::write(dir.join(&file_name), image.bytes())?;
self.insert(&format!(""), window, cx);
Ok(())
}
}local:project-files-through-fssteerFrom review: zed-industries/zed#58588.
Review quote and full rule: copy into draft.json
The review: “Could we go through Worktree::create_entry instead of std::fs::write? That would avoid blocking here and make this work on remote projects.”
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:project-files-through-fs",
"revision": 1,
"mode": "steer",
"description": "Read and write files in Zed's editor, project, workspace and UI crates through the Fs trait or the Worktree, not std::fs or smol::fs; direct calls skip remote projects and FakeFs.",
"scope": {
"include": [
"crates/editor/src/**/*.rs",
"crates/project/src/**/*.rs",
"crates/workspace/src/**/*.rs",
"crates/sidebar/src/**/*.rs",
"crates/terminal_view/src/**/*.rs",
"crates/*_ui/src/**/*.rs"
],
"exclude": ["**/*_tests.rs", "**/tests/**", "**/src/test.rs"]
},
"clauses": [
{
"id": "direct-fs-call",
"forbid": {
"ast": {
"language": "rust",
"rule": {
"kind": "call_expression",
"has": {
"field": "function",
"any": [{ "kind": "scoped_identifier" }, { "kind": "identifier" }],
"pattern": "$CALL",
"resolves": { "import": ["std::fs", "smol::fs"], "prefix": true }
},
"not": {
"any": [
{
"inside": {
"kind": "mod_item",
"follows": { "kind": "attribute_item", "regex": "^#\\[cfg\\(test\\)\\]$" },
"stopBy": "end"
}
},
{
"has": {
"field": "arguments",
"has": {
"any": [{ "kind": "scoped_identifier" }, { "kind": "identifier" }],
"resolves": { "import": ["paths"], "prefix": true },
"stopBy": "end"
}
}
}
]
}
}
}
},
"violation": {
"code": "DIRECT_FS_CALL",
"anchor": "witness.span",
"message": "Replace $CALL with the project's Fs or a Worktree method, so this works on remote projects and on FakeFs in tests. Keep it only for files no project owns, like Zed's own data."
}
}
]
},
"cases": {
"caught": [
{ "path": "crates/editor/src/clipboard.rs", "content": "use std::fs;\n\nuse gpui::{Context, Image, Window};\n\nimpl Editor {\n fn paste_image(\n &mut self,\n image: &Image,\n dir: &Path,\n window: &mut Window,\n cx: &mut Context<Self>,\n ) -> anyhow::Result<()> {\n let file_name = format!(\"image.{}\", image.format.extension());\n fs::write(dir.join(&file_name), image.bytes())?;\n self.insert(&format!(\"\"), window, cx);\n Ok(())\n }\n}\n" },
{ "path": "crates/project/src/task_inventory.rs", "content": "use smol::fs as async_fs;\n\npub(crate) async fn load_worktree_tasks(abs_path: PathBuf) -> anyhow::Result<TaskTemplates> {\n let contents = async_fs::read_to_string(&abs_path).await.context(\"reading tasks.json\")?;\n parse_json_with_comments(&contents)\n}\n" },
{ "path": "crates/agent_ui/src/thread_worktree_archive.rs", "content": "async fn remove_root(root: PathBuf, cx: &mut AsyncApp) -> anyhow::Result<()> {\n cx.background_spawn(async move {\n match std::fs::remove_dir_all(&root) {\n Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),\n result => result.map_err(anyhow::Error::from),\n }\n })\n .await\n}\n" }
],
"passed": [
{ "path": "crates/editor/src/clipboard.rs", "content": "use gpui::{Context, Image, Window};\n\nimpl Editor {\n fn paste_image(&mut self, image: &Image, worktree: Entity<Worktree>, window: &mut Window, cx: &mut Context<Self>) {\n let path = RelPath::unix(&format!(\"image.{}\", image.format.extension())).unwrap().into_arc();\n let create = worktree.update(cx, |worktree, cx| {\n worktree.create_entry(path.clone(), false, Some(image.bytes().to_vec()), cx)\n });\n self.insert(&format!(\"\", path.as_unix_str()), window, cx);\n create.detach_and_log_err(cx);\n }\n}\n" },
{ "path": "crates/agent_ui/src/thread_worktree_archive.rs", "content": "use fs::{Fs, RemoveOptions};\n\nasync fn remove_root(fs: Arc<dyn Fs>, root: PathBuf) -> anyhow::Result<()> {\n fs.remove_dir(&root, RemoveOptions { recursive: true, ignore_if_not_exists: true }).await\n}\n" },
{ "path": "crates/agent_ui/src/mention_set.rs", "content": "pub(crate) fn load_image(project: &Entity<Project>, path: Arc<Path>, cx: &mut App) -> Task<anyhow::Result<Vec<u8>>> {\n let fs = project.read(cx).fs().clone();\n cx.background_spawn(async move { fs.load_bytes(&path).await })\n}\n\n#[cfg(test)]\nmod tests {\n use std::fs;\n\n #[gpui::test]\n async fn loads_png(cx: &mut TestAppContext) {\n let dir = tempfile::tempdir().unwrap();\n fs::write(dir.path().join(\"a.png\"), PNG_BYTES).unwrap();\n }\n}\n" },
{ "path": "crates/snippets_ui/src/snippets_ui.rs", "content": "use std::{borrow::Borrow, fs};\n\nuse paths::snippets_dir;\n\nfn open_snippets_folder(workspace: &mut Workspace, cx: &mut Context<Workspace>) {\n fs::create_dir_all(snippets_dir()).notify_err(workspace, cx);\n cx.open_with_system(snippets_dir().borrow());\n}\n" },
{ "path": "crates/fs/src/fs.rs", "content": "#[async_trait::async_trait]\nimpl Fs for RealFs {\n async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {\n let path = path.to_owned();\n self.executor.spawn(async move { std::fs::remove_dir_all(&path) }).await?;\n Ok(())\n }\n}\n" }
]
}
}#uv: every environment variable is declared once, in EnvVars
uv declares every environment variable it reads as a constant on EnvVars in the uv-static crate, and generates its
environment-variable reference from that list. A string-literal name works but is missing from the reference, and a
typo fails silently. Adding a fallback region for S3 request signing, an agent spells the second variable inline:
use std::env;
use std::sync::LazyLock;
use uv_static::EnvVars;
/// The AWS region used to sign S3 requests, if one is configured.
static S3_REGION: LazyLock<Option<String>> = LazyLock::new(|| {
env::var(EnvVars::AWS_REGION)
.or_else(|_| env::var("AWS_DEFAULT_REGION"))
.ok()
});local:env-vars-through-envvarssteerFrom review: astral-sh/uv#15925 and the EnvVars doc comment.
Review quotes and full rule: copy into draft.json
The review: “Should be in EnvVars”, on std::; the merge reads EnvVars::. The doc comment says EnvVars “Declares all environment variable used throughout uv and its crates.”
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:env-vars-through-envvars",
"revision": 1,
"mode": "steer",
"description": "Name every environment variable uv reads or sets through a constant in uv-static's EnvVars, not a string literal; EnvVars is the one list the environment reference is generated from.",
"scope": {
"include": ["crates/**/*.rs"],
"exclude": ["crates/uv-static/**", "crates/uv-bench/**", "crates/uv-test/**", "**/build.rs"]
},
"clauses": [
{
"id": "literal-env-call",
"forbid": {
"ast": {
"language": "rust",
"rule": {
"kind": "call_expression",
"has": {
"field": "function",
"any": [{ "kind": "scoped_identifier" }, { "kind": "identifier" }],
"resolves": {
"import": ["std::env::var", "std::env::var_os", "std::env::set_var", "std::env::remove_var"]
}
},
"all": [
{
"has": {
"field": "arguments",
"has": { "kind": "string_literal", "nthChild": 1, "pattern": "$NAME" }
}
}
]
}
}
},
"violation": {
"code": "LITERAL_ENV_VAR",
"anchor": "witness.span",
"message": "Name $NAME through its uv_static::EnvVars constant, adding one with a doc comment if it's missing, so the variable is declared once and listed in the environment reference."
}
},
{
"id": "literal-clap-env",
"forbid": {
"ast": {
"language": "rust",
"rule": { "kind": "attribute_item", "regex": "^#\\[(?:arg|clap)\\((?s:.*)\\benv\\s*=\\s*\"" }
}
},
"violation": {
"code": "LITERAL_CLAP_ENV",
"anchor": "witness.span",
"message": "Write this argument's env as an EnvVars constant (env = EnvVars::UV_...), adding one in uv-static if it's missing, so the variable is declared and documented once."
}
}
]
},
"cases": {
"caught": [
{ "path": "crates/uv-auth/src/providers.rs", "content": "use std::env;\nuse std::sync::LazyLock;\n\nuse uv_static::EnvVars;\n\n/// The AWS region used to sign S3 requests, if one is configured.\nstatic S3_REGION: LazyLock<Option<String>> = LazyLock::new(|| {\n env::var(EnvVars::AWS_REGION)\n .or_else(|_| env::var(\"AWS_DEFAULT_REGION\"))\n .ok()\n});\n" },
{ "path": "crates/uv/src/printer.rs", "content": "use std::env::var_os as env_var;\n\npub fn progress_enabled(printer: Printer) -> bool {\n if env_var(\"UV_NO_PROGRESS\").is_some() {\n return false;\n }\n !matches!(printer, Printer::Quiet | Printer::Silent)\n}\n" },
{ "path": "crates/uv-cli/src/lib.rs", "content": "#[derive(Args)]\npub struct PipCompileArgs {\n /// The platform for which requirements should be resolved.\n #[arg(long, env = \"UV_PYTHON_PLATFORM\")]\n pub python_platform: Option<TargetTriple>,\n}\n" }
],
"passed": [
{ "path": "crates/uv-auth/src/providers.rs", "content": "use std::env;\nuse std::sync::LazyLock;\n\nuse uv_static::EnvVars;\n\n/// The AWS region used to sign S3 requests, if one is configured.\nstatic S3_REGION: LazyLock<Option<String>> = LazyLock::new(|| {\n env::var(EnvVars::AWS_REGION)\n .or_else(|_| env::var(EnvVars::AWS_DEFAULT_REGION))\n .ok()\n});\n" },
{ "path": "crates/uv-cache-info/src/cache_info.rs", "content": "/// Read the environment variables a `cache-keys` entry names, e.g. `{ env = \"MACOSX_DEPLOYMENT_TARGET\" }`.\nfn env_cache_key(key: &CacheKey) -> Option<String> {\n match key {\n CacheKey::Env { env } => std::env::var(env).ok(),\n _ => None,\n }\n}\n" },
{ "path": "crates/uv-cli/src/lib.rs", "content": "#[derive(Args)]\npub struct PipCompileArgs {\n /// The platform for which requirements should be resolved.\n #[arg(long, env = EnvVars::UV_PYTHON_PLATFORM)]\n pub python_platform: Option<TargetTriple>,\n}\n" },
{ "path": "crates/uv-cache/src/lib.rs", "content": "pub fn init(cache_dir: Option<&Path>) -> Result<Cache, Error> {\n if cache_dir.is_none() && std::env::var_os(EnvVars::XDG_CACHE_HOME).is_none() {\n warn_user_once!(\"Set `UV_CACHE_DIR` or `XDG_CACHE_HOME` to choose where uv caches downloads\");\n }\n Cache::from_settings(false, cache_dir.map(Path::to_path_buf))\n}\n" },
{ "path": "crates/uv-static/src/env_vars.rs", "content": "impl EnvVars {\n /// The default AWS region to use when signing S3 requests, if `AWS_REGION` is not set.\n #[attr_added_in(\"0.8.21\")]\n pub const AWS_DEFAULT_REGION: &'static str = \"AWS_DEFAULT_REGION\";\n}\n" }
]
}
}#PHP and SQL
Rules that keep apps on public APIs, settings in config files, and slow index builds out of startup.
#Nextcloud: apps use the public OCP API, never the server's private OC classes
Nextcloud apps run on whatever server version the admin installs. OC\ classes are the server's private implementation and can change between releases, so an app using one can break on upgrade. The app store accepts only apps that use the public OCP\ API.
Needing to check an uploaded file name, an agent finds the server's own helper:
<?php
namespace OCA\Forms\Controller;
use OC\Files\Filesystem;
use OCP\AppFramework\OCSController;
class ApiController extends OCSController {
public function uploadFiles(int $formId, string $name): void {
if (Filesystem::isFileBlacklisted($name)) {
throw new \InvalidArgumentException('Invalid file name');
}
}
}local:apps-use-public-apisteerFrom review: nextcloud/forms#2040 and the app store rules.
Review quotes and full rule: copy into draft.json
The review: “OC is private and should not be used in apps. Only use OCP API.” The app store rules: “Apps can only use the public Nextcloud API.”
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:apps-use-public-api",
"revision": 1,
"mode": "steer",
"description": "Use only Nextcloud's public OCP API in the app, never the server's private OC classes; private classes change without notice between releases.",
"scope": { "include": ["lib/**/*.php", "templates/**/*.php", "appinfo/**/*.php"], "exclude": ["tests/**", "vendor/**"] },
"clauses": [
{
"id": "private-server-class",
"forbid": {
"ast": {
"language": "php",
"rule": {
"any": [{ "kind": "qualified_name" }, { "kind": "name", "not": { "inside": { "kind": "qualified_name" } } }],
"pattern": "$NAME",
"resolves": {
"import": ["OC", "OC_App", "OC_Defaults", "OC_Helper", "OC_Hook", "OC_Template", "OC_User", "OC_Util"],
"prefix": true
}
}
}
},
"violation": {
"code": "PRIVATE_SERVER_API",
"anchor": "witness.span",
"message": "$NAME is private server code (the OC namespace) that can change in any release: use the OCP equivalent, injected in classes, e.g. IFilenameValidator for Filesystem, OCP\\Util for OC_Util."
}
}
]
},
"cases": {
"caught": [
{ "path": "lib/Controller/ApiController.php", "content": "<?php\n\nnamespace OCA\\Forms\\Controller;\n\nuse OC\\Files\\Filesystem;\nuse OCP\\AppFramework\\OCSController;\n\nclass ApiController extends OCSController {\n\tpublic function uploadFiles(int $formId, string $name): void {\n\t\tif (Filesystem::isFileBlacklisted($name)) {\n\t\t\tthrow new \\InvalidArgumentException('Invalid file name');\n\t\t}\n\t}\n}\n" },
{ "path": "lib/Service/ConfigService.php", "content": "<?php\n\nnamespace OCA\\Forms\\Service;\n\nuse OC\\{Files\\View, User\\Session as UserSession};\n\nclass ConfigService {\n\tpublic function __construct(private UserSession $session) {\n\t}\n\n\tpublic function getVersion(): string {\n\t\treturn \\OC_Util::getVersionString() . \\OC::$server->getSystemConfig()->getValue('instanceid');\n\t}\n}\n" },
{ "path": "templates/main.php", "content": "<?php\n\\OCP\\Util::addScript('forms', 'forms-main');\n?>\n<div id=\"content\" data-version=\"<?php p(OC_Util::getVersionString()); ?>\"></div>\n" }
],
"passed": [
{ "path": "lib/Controller/ApiController.php", "content": "<?php\n\nnamespace OCA\\Forms\\Controller;\n\nuse OCP\\AppFramework\\OCSController;\nuse OCP\\Files\\IFilenameValidator;\nuse OCP\\IRequest;\n\nclass ApiController extends OCSController {\n\tpublic function __construct(string $appName, IRequest $request, private IFilenameValidator $filenameValidator) {\n\t\tparent::__construct($appName, $request);\n\t}\n\n\tpublic function uploadFiles(int $formId, string $name): void {\n\t\tif (!$this->filenameValidator->isFilenameValid($name)) {\n\t\t\tthrow new \\InvalidArgumentException('Invalid file name');\n\t\t}\n\t}\n}\n" },
{ "path": "lib/Service/CirclesService.php", "content": "<?php\n\nnamespace OCA\\Forms\\Service;\n\nuse OCA\\Circles\\CirclesManager;\nuse OCA\\Forms\\Db\\FormMapper;\nuse OCP\\IConfig;\n\n// CirclesManager is the API Circles publishes for other apps, unlike OC\\Files\\Filesystem.\nclass CirclesService {\n\tpublic function __construct(private CirclesManager $circles, private FormMapper $forms, private IConfig $config) {\n\t}\n}\n" },
{ "path": "templates/main.php", "content": "<?php\n\\OCP\\Util::addScript('forms', 'forms-main');\n?>\n<div id=\"content\" data-version=\"<?php p(implode('.', \\OCP\\Util::getVersion())); ?>\"></div>\n" },
{ "path": "tests/Unit/Controller/ApiControllerTest.php", "content": "<?php\n\nnamespace OCA\\Forms\\Tests\\Unit\\Controller;\n\nuse OC\\Files\\Filesystem;\n" }
]
}
}#Laravel: env() only in config files; read settings through config()
In production Laravel runs php artisan config:cache and stops loading .env. An env() call outside config/ then sees only the server's real environment variables, usually null, though it works on a developer machine. Put the value in a config/ file and read it with config().
Adding a Patreon client, an agent reads the token where it uses it:
<?php
namespace App\Services\Patreon;
use Illuminate\Support\Facades\Http;
class PatreonApi
{
public function campaigns(): array
{
return Http::withToken(env('PATREON_TOKEN'))
->get('https://www.patreon.com/api/oauth2/v2/campaigns')
->json('data');
}
}local:env-only-in-configsteerFrom review: spatie/spatie.be#35 and the Laravel docs.
Review quotes and full rule: copy into draft.json
The review: “The env function may only be used in config files.” The Laravel docs, on configuration caching: “you should be sure that you are only calling the env function from within your configuration files”.
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:env-only-in-config",
"revision": 1,
"mode": "steer",
"description": "Call env only in config files and read settings everywhere else through config; once config is cached, env returns null for values from .env.",
"scope": {
"include": ["app/**/*.php", "routes/**/*.php", "database/**/*.php", "resources/views/**/*.php"],
"exclude": ["tests/**"]
},
"clauses": [
{
"id": "env-call",
"forbid": {
"ast": {
"language": "php",
"rule": {
"kind": "function_call_expression",
"has": {
"field": "function",
"pattern": "$CALL",
"any": [{ "kind": "name", "regex": "^env$" }, { "resolves": { "import": ["env"] } }]
}
}
}
},
"violation": {
"code": "ENV_OUTSIDE_CONFIG",
"anchor": "witness.span",
"message": "Read this setting with config('file.key') instead of $CALL(): once config is cached, .env isn't loaded and $CALL() returns null. Add the key to a file in config/, which may call env()."
}
},
{
"id": "env-class",
"forbid": {
"ast": {
"language": "php",
"rule": {
"kind": "scoped_call_expression",
"all": [
{
"has": { "field": "scope", "pattern": "$CLASS", "resolves": { "import": ["Illuminate\\Support\\Env"] } }
},
{ "has": { "field": "name", "pattern": "$METHOD" } }
]
}
}
},
"violation": {
"code": "ENV_CLASS_OUTSIDE_CONFIG",
"anchor": "witness.span",
"message": "$CLASS::$METHOD() reads .env, which isn't loaded once config is cached: read this setting with config('file.key'), adding the key to a file in config/ that calls env()."
}
},
{
"id": "env-in-view",
"forbid": {
"ast": {
"language": "blade",
"embedded": "compiled-view",
"rule": {
"kind": "function_call_expression",
"has": {
"field": "function",
"pattern": "$CALL",
"any": [{ "kind": "name", "regex": "^env$" }, { "resolves": { "import": ["env"] } }]
}
}
}
},
"violation": {
"code": "ENV_IN_VIEW",
"anchor": "witness.span",
"message": "Use config('file.key') in this view instead of $CALL(): once config is cached, .env isn't loaded and $CALL() returns null. Add the key to a file in config/, which may call env()."
}
}
]
},
"cases": {
"caught": [
{ "path": "app/Services/Patreon/PatreonApi.php", "content": "<?php\n\nnamespace App\\Services\\Patreon;\n\nuse Illuminate\\Support\\Facades\\Http;\n\nclass PatreonApi\n{\n public function campaigns(): array\n {\n return Http::withToken(env('PATREON_TOKEN'))\n ->get('https://www.patreon.com/api/oauth2/v2/campaigns')\n ->json('data');\n }\n}\n" },
{ "path": "app/Providers/GitHubServiceProvider.php", "content": "<?php\n\nnamespace App\\Providers;\n\nuse Github\\Client;\nuse Illuminate\\Support\\Env as DotEnv;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass GitHubServiceProvider extends ServiceProvider\n{\n public function register(): void\n {\n $this->app->singleton(Client::class, function () {\n $client = new Client();\n $client->authenticate(DotEnv::get('GITHUB_TOKEN'), null, Client::AUTH_ACCESS_TOKEN);\n\n return $client;\n });\n }\n}\n" },
{ "path": "routes/web.php", "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\n\nif (\\env('SHOW_HORIZON', false)) {\n Route::view('/horizon-status', 'horizon.status');\n}\n" },
{ "path": "resources/views/front/layouts/app.blade.php", "content": "<head>\n <title>{{ $title }}</title>\n @if (env('FATHOM_SITE_ID'))\n <script src=\"https://cdn.usefathom.com/script.js\" data-site=\"{{ env('FATHOM_SITE_ID') }}\" defer></script>\n @endif\n</head>\n" }
],
"passed": [
{ "path": "app/Services/Patreon/PatreonApi.php", "content": "<?php\n\nnamespace App\\Services\\Patreon;\n\nuse Illuminate\\Support\\Facades\\Http;\n\nclass PatreonApi\n{\n public function campaigns(): array\n {\n return Http::withToken(config('services.patreon.token'))\n ->get('https://www.patreon.com/api/oauth2/v2/campaigns')\n ->json('data');\n }\n}\n" },
{ "path": "app/Console/Commands/ImportPatreonPledgers.php", "content": "<?php\n\nnamespace App\\Console\\Commands;\n\nuse Illuminate\\Console\\Command;\n\nclass ImportPatreonPledgers extends Command\n{\n protected $signature = 'import:patreon-pledgers {--env=}';\n\n public function handle(): void\n {\n // Production only: never call env('PATREON_TOKEN') here.\n if (! app()->environment('production') && $this->option('env') !== 'production') {\n return;\n }\n\n $this->info('Importing pledgers from Patreon...');\n }\n}\n" },
{ "path": "app/Support/Deploy.php", "content": "<?php\n\nnamespace App\\Support;\n\nuse App\\Enums\\Env;\n\nclass Deploy\n{\n public function target(string $name): Env\n {\n return Env::from($name);\n }\n}\n" },
{ "path": "resources/views/front/layouts/app.blade.php", "content": "<head>\n <title>{{ $title }}</title>\n @if (config('services.fathom.site_id'))\n <script src=\"https://cdn.usefathom.com/script.js\" data-site=\"{{ config('services.fathom.site_id') }}\" defer></script>\n @endif\n</head>\n" },
{ "path": "config/services.php", "content": "<?php\n\nreturn [\n 'patreon' => [\n 'token' => env('PATREON_TOKEN'),\n ],\n];\n" }
]
}
}#Synapse (PostgreSQL): index an existing table in a background update, not a schema delta
Synapse applies schema deltas at startup and serves nothing until they finish. CREATE INDEX on a table that already holds a large server's rows keeps the homeserver down while the index builds; DROP INDEX takes a heavy table lock. A table the same delta creates is empty and safe to index.
Asked to speed up a query, an agent writes the obvious delta:
--
-- This file is licensed under the Affero General Public License (AGPL) version 3.
--
-- Copyright (C) 2026 New Vector, Ltd
--
-- So we can count unread notifications per thread.
CREATE INDEX IF NOT EXISTS event_push_actions_thread_id
ON event_push_actions(user_id, room_id, thread_id);local:index-existing-tables-in-backgroundsteerFrom the architecture doc: docs/development/database_schema.md and review: element-hq/synapse#18440.
Review quotes and full rule: copy into draft.json
The doc: “including adding columns, indices or NOT NULL constraints to non-empty tables — such a migration should be done as a background update”. The review: “for new tables, it's fine to create the index right now”.
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:index-existing-tables-in-background",
"revision": 1,
"mode": "steer",
"description": "Create indexes on existing tables, and drop indexes, through background updates instead of schema delta files; a delta blocks Synapse's startup until it finishes.",
"scope": { "include": ["synapse/storage/schema/*/delta/**/*.sql"], "exclude": [] },
"clauses": [
{
"id": "create-index-on-existing-table",
"forbid": {
"ast": {
"language": "sql",
"rule": {
"kind": "create_index",
"has": { "kind": "object_reference", "has": { "field": "name", "pattern": "$TABLE" } },
"not": {
"inside": {
"kind": "program",
"stopBy": "end",
"has": {
"kind": "create_table",
"stopBy": "end",
"has": { "kind": "object_reference", "has": { "field": "name", "pattern": "$TABLE" } }
}
}
}
}
}
},
"violation": {
"code": "INDEX_IN_DELTA",
"anchor": "witness.span",
"message": "Create this index on $TABLE in a background update: insert a background_updates row here and register it with register_background_index_update. A delta blocks startup while it builds."
}
},
{
"id": "drop-index",
"forbid": {
"ast": { "language": "sql", "rule": { "kind": "drop_index", "has": { "field": "name", "pattern": "$INDEX" } } }
},
"violation": {
"code": "DROP_INDEX_IN_DELTA",
"anchor": "witness.span",
"message": "Drop $INDEX in a background update, not a delta: on a large table it takes a heavy lock while startup waits. Pass it as replaces_index to register_background_index_update to swap indexes."
}
}
]
},
"cases": {
"caught": [
{ "path": "synapse/storage/schema/main/delta/93/02_event_push_actions_thread_idx.sql", "content": "--\n-- This file is licensed under the Affero General Public License (AGPL) version 3.\n--\n-- Copyright (C) 2026 New Vector, Ltd\n--\n\n-- So we can count unread notifications per thread.\nCREATE INDEX IF NOT EXISTS event_push_actions_thread_id\n ON event_push_actions(user_id, room_id, thread_id);\n" },
{ "path": "synapse/storage/schema/main/delta/93/03_receipts_idx.sql", "content": "--\n-- This file is licensed under the Affero General Public License (AGPL) version 3.\n--\n-- Copyright (C) 2026 New Vector, Ltd\n--\n\nDROP INDEX IF EXISTS receipts_linearized_user;\nCREATE INDEX receipts_linearized_user ON receipts_linearized (user_id, room_id);\n" },
{ "path": "synapse/storage/schema/main/delta/93/04_sticky_events.sql", "content": "--\n-- This file is licensed under the Affero General Public License (AGPL) version 3.\n--\n-- Copyright (C) 2026 New Vector, Ltd\n--\n\nCREATE TABLE sticky_events (\n room_id TEXT NOT NULL,\n event_id TEXT NOT NULL,\n expires_at BIGINT NOT NULL\n);\n\nCREATE INDEX sticky_events_expires_at ON sticky_events (expires_at);\nCREATE INDEX events_sticky_room ON events (room_id, stream_ordering);\n" }
],
"passed": [
{ "path": "synapse/storage/schema/main/delta/93/02_event_push_actions_thread_idx.sql", "content": "--\n-- This file is licensed under the Affero General Public License (AGPL) version 3.\n--\n-- Copyright (C) 2026 New Vector, Ltd\n--\n\nINSERT INTO background_updates (ordering, update_name, progress_json) VALUES\n (9302, 'event_push_actions_thread_id_index', '{}');\n" },
{ "path": "synapse/storage/schema/main/delta/93/05_delayed_events.sql", "content": "--\n-- This file is licensed under the Affero General Public License (AGPL) version 3.\n--\n-- Copyright (C) 2026 New Vector, Ltd\n--\n\nCREATE TABLE delayed_events (\n delay_id TEXT NOT NULL,\n user_localpart TEXT NOT NULL,\n send_ts BIGINT NOT NULL,\n PRIMARY KEY (user_localpart, delay_id)\n);\n\nCREATE INDEX delayed_events_send_ts ON delayed_events (send_ts);\n" },
{ "path": "synapse/storage/schema/main/delta/93/06_rooms_creator.sql", "content": "--\n-- This file is licensed under the Affero General Public License (AGPL) version 3.\n--\n-- Copyright (C) 2026 New Vector, Ltd\n--\n\n-- Nullable with no default: PostgreSQL adds it without rewriting the table.\nALTER TABLE rooms ADD COLUMN creator TEXT;\n" },
{ "path": "synapse/storage/schema/main/full_schemas/72/full.sql", "content": "CREATE INDEX x ON y (z);\n" }
]
}
}#React
Two rules for an app built from <PanelFrame> panels: one follows imports across files, one checks the enclosing function.
#Panels never nest
Every panel renders inside <PanelFrame>, which draws the title bar and close button and opens the analytics scope.
Nested frames draw two title bars and count the view twice. Asked to show the customer in the order panel, an agent
reuses CustomerPanel, which brings its own frame:
import { PanelFrame } from '@/ui/panel';
import { CustomerPanel } from '@/panels/CustomerPanel';
export function OrdersPanel({ order }: Props) {
return (
<PanelFrame title="Order">
<CustomerPanel id={order.customerId} />
</PanelFrame>
);
}local:no-nested-panelssteerHow it works and full rule: copy into draft.json
The rule never sees inside CustomerPanel. It treats it as a panel because the name is imported from @/panels/, so
the folder convention carries the nesting across files.
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:no-nested-panels",
"revision": 1,
"mode": "steer",
"description": "Don't nest panels: no PanelFrame inside another, and no panel component rendered from inside a panel.",
"scope": { "include": ["src/**/*.tsx"], "exclude": ["**/*.test.tsx", "**/*.stories.tsx"] },
"clauses": [
{
"id": "frame-in-frame",
"forbid": {
"ast": {
"language": "typescript",
"utils": {
"panel-frame": {
"any": [
{ "kind": "jsx_element", "has": { "field": "open_tag", "has": { "field": "name", "regex": "^PanelFrame$" } } },
{ "kind": "jsx_self_closing_element", "has": { "field": "name", "regex": "^PanelFrame$" } }
]
}
},
"rule": {
"matches": "panel-frame",
"inside": { "matches": "panel-frame", "stopBy": "end" }
}
}
},
"violation": {
"code": "NESTED_PANEL_FRAME",
"anchor": "witness.span",
"message": "Panels don't nest: render this content without its own PanelFrame, or open it as a separate panel next to this one."
}
},
{
"id": "panel-in-panel",
"forbid": {
"ast": {
"language": "typescript",
"file": { "path": { "regex": "^src/panels/" } },
"rule": {
"any": [{ "kind": "jsx_opening_element" }, { "kind": "jsx_self_closing_element" }],
"has": {
"field": "name",
"kind": "identifier",
"pattern": "$PANEL",
"resolves": {
"binding": {
"site": {
"inside": {
"kind": "import_statement",
"has": { "field": "source", "regex": "^['\"]@/panels/" },
"stopBy": "end"
}
}
}
}
}
}
}
},
"violation": {
"code": "PANEL_IN_PANEL",
"anchor": "witness.span",
"message": "$PANEL is a panel, and panels don't nest: open it next to this one from the page, or move the part you need out of src/panels into a shared component."
}
}
]
},
"cases": {
"caught": [
{ "path": "src/pages/Orders.tsx", "content": "import { PanelFrame } from '@/ui/panel';\n\nexport function Orders() {\n return (\n <PanelFrame title=\"Orders\">\n <OrderList />\n <PanelFrame title=\"Details\">\n <OrderDetails />\n </PanelFrame>\n </PanelFrame>\n );\n}\n" },
{ "path": "src/pages/Orders.tsx", "content": "import { PanelFrame } from '@/ui/panel';\n\nexport function Orders() {\n return (\n <PanelFrame title=\"Orders\">\n {open && <PanelFrame title=\"Empty\" />}\n </PanelFrame>\n );\n}\n" },
{ "path": "src/panels/OrdersPanel.tsx", "content": "import { PanelFrame } from '@/ui/panel';\nimport { CustomerPanel } from '@/panels/CustomerPanel';\n\nexport function OrdersPanel({ order }: Props) {\n return (\n <PanelFrame title=\"Order\">\n <CustomerPanel id={order.customerId} />\n </PanelFrame>\n );\n}\n" }
],
"passed": [
{ "path": "src/pages/Orders.tsx", "content": "import { PanelFrame } from '@/ui/panel';\n\nexport function Orders() {\n return (\n <>\n <PanelFrame title=\"Orders\"><OrderList /></PanelFrame>\n <PanelFrame title=\"Details\"><OrderDetails /></PanelFrame>\n </>\n );\n}\n" },
{ "path": "src/pages/Orders.tsx", "content": "import { OrdersPanel } from '@/panels/OrdersPanel';\nimport { CustomerPanel } from '@/panels/CustomerPanel';\n\nexport function Orders() {\n return (\n <>\n <OrdersPanel />\n <CustomerPanel />\n </>\n );\n}\n" },
{ "path": "src/panels/OrdersPanel.tsx", "content": "import { PanelFrame } from '@/ui/panel';\nimport { CustomerSummary } from '@/components/CustomerSummary';\n\nfunction OrderLines({ lines }: { lines: Line[] }) {\n return <ul>{lines.map(line => <li key={line.id}>{line.name}</li>)}</ul>;\n}\n\nexport function OrdersPanel({ order }: Props) {\n return (\n <PanelFrame title=\"Order\">\n <CustomerSummary id={order.customerId} />\n <OrderLines lines={order.lines} />\n </PanelFrame>\n );\n}\n" }
]
}
}#A hook that only works inside its wrapper
usePanel() reads the context <PanelFrame> provides, so it works only inside a frame. Called in the component that
renders the frame, it runs before the frame exists and gets no panel. Nothing fails until someone opens the panel:
import { PanelFrame, usePanel } from '@/ui/panel';
export function OrdersPanel() {
const panel = usePanel();
return (
<PanelFrame title="Orders">
<button onClick={panel.close}>Close</button>
</PanelFrame>
);
}local:use-panel-below-panel-framesteerHow it works and full rule: copy into draft.json
"Nearest enclosing function" is "stopBy": { "matches": "function" }. The search for the frame stops at the first
function around the call, so a child component declared in the same file is not blamed for its parent.
Whether every page renders src/panels/ components inside a frame needs the render tree, which no rule sees. Keep
usePanel() throwing when it finds no frame.
{
"kind": "code",
"rule": {
"format": "rerail.policy/v1",
"id": "local:use-panel-below-panel-frame",
"revision": 1,
"mode": "steer",
"description": "Call usePanel only in components rendered inside a PanelFrame, which live under src/panels, never in the component that renders the frame.",
"scope": { "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["**/*.test.*", "**/*.stories.tsx", "src/ui/panel/**"] },
"clauses": [
{
"id": "beside-its-frame",
"forbid": {
"ast": {
"language": "typescript",
"utils": {
"function": {
"any": [
{ "kind": "function_declaration" },
{ "kind": "function_expression" },
{ "kind": "arrow_function" },
{ "kind": "method_definition" }
]
}
},
"rule": {
"pattern": "usePanel($$$ARGS)",
"inside": {
"matches": "function",
"has": {
"any": [{ "kind": "jsx_opening_element" }, { "kind": "jsx_self_closing_element" }],
"has": { "field": "name", "regex": "^PanelFrame$" },
"stopBy": "end"
},
"stopBy": { "matches": "function" }
}
}
}
},
"violation": {
"code": "USE_PANEL_BESIDE_FRAME",
"anchor": "witness.span",
"message": "usePanel() here runs above the PanelFrame this component renders, so it gets no panel: call it in a child component rendered inside the frame."
}
},
{
"id": "outside-panels",
"forbid": {
"ast": {
"language": "typescript",
"file": { "path": { "notRegex": "^src/panels/" } },
"rule": { "pattern": "usePanel($$$ARGS)" }
}
},
"violation": {
"code": "USE_PANEL_OUTSIDE_PANELS",
"anchor": "witness.span",
"message": "Only components under src/panels render inside a PanelFrame: move this component there, or pass it what it needs from usePanel() as props."
}
}
]
},
"cases": {
"caught": [
{ "path": "src/panels/OrdersPanel.tsx", "content": "import { PanelFrame, usePanel } from '@/ui/panel';\n\nexport function OrdersPanel() {\n const panel = usePanel();\n return (\n <PanelFrame title=\"Orders\">\n <button onClick={panel.close}>Close</button>\n </PanelFrame>\n );\n}\n" },
{ "path": "src/panels/OrdersPanel.tsx", "content": "import { PanelFrame, usePanel } from '@/ui/panel';\n\nexport const OrdersPanel = () => {\n const { close } = usePanel();\n return <PanelFrame title=\"Orders\" onEscape={close} />;\n};\n" },
{ "path": "src/components/CloseButton.tsx", "content": "import { usePanel } from '@/ui/panel';\n\nexport function CloseButton() {\n const panel = usePanel();\n return <button onClick={panel.close}>Close</button>;\n}\n" }
],
"passed": [
{ "path": "src/panels/usePanelTitle.ts", "content": "import { usePanel } from '@/ui/panel';\n\nexport function usePanelTitle(): string {\n return usePanel().title;\n}\n" },
{ "path": "src/panels/OrdersPanel.tsx", "content": "import { PanelFrame, usePanel } from '@/ui/panel';\n\nfunction CloseButton() {\n const panel = usePanel();\n return <button onClick={panel.close}>Close</button>;\n}\n\nexport function OrdersPanel() {\n return (\n <PanelFrame title=\"Orders\">\n <CloseButton />\n </PanelFrame>\n );\n}\n" },
{ "path": "src/components/CloseButton.tsx", "content": "export function CloseButton({ onClose }: { onClose: () => void }) {\n return <button onClick={onClose}>Close</button>;\n}\n" }
]
}
}#What a rule can and can't check
Each rule on this page reads one file at a time: its syntax tree, path and text.
What a rule can ask within one file
| Question | How | Example |
|---|---|---|
| Is this node inside that one, at any depth? | inside / has with "stopBy": "end" |
Sentry: a control-silo query inside a @cell_silo_endpoint class; a PanelFrame inside a PanelFrame |
| Is it in the nearest enclosing function, not just some outer one? | inside with "stopBy" set to a rule |
usePanel() in the component that renders the frame |
| Where was this name imported from, under any alias? | resolves: Python, PHP and Rust through the file's imports; TypeScript through the import that binds the name |
Sentry's User as SentryUser, Immich's spawn as run, Nextcloud's use OC\Files\Filesystem, rust-analyzer's use toolchain:: |
| Does a name here match one elsewhere in the file? | the same capture, such as $TABLE, in two places |
Synapse: an index on a table the migration does not create |
| Which file is this? | scope, and a clause's own file.path condition |
n8n: .vue and .ts files under a frontend module |
A rule cannot see into other files. With no type checker, call graph or render tree, it cannot tell which component
renders inside which, whether a called helper does IO, or a value's type. Hence each example's misses line.
A design rule becomes checkable when the code records the decision where one file shows it:
- In the same file: the call sits lexically inside the class, function, block or element it must not be in.
- In the names: the decision follows a module, package or crate (
OC\versusOCP\,std::fsversus Zed'sfs), which name resolution reads under any alias. - In the layout: a folder, file name or import path says which layer the file belongs to (
zerver/views/,crates/ide/,src/panels/).
When none holds, keep the check where it is today (review, a runtime assertion) and write the rule for the part a file shows. The DSL reference documents every construct these examples use.