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
4 changes: 3 additions & 1 deletion cli/check.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Fensu finds your [configuration](/concepts/configuration) by searching upward fr
the current directory, sorts files into the product, test, and tooling
[scopes](/concepts/architecture-model#scopes), and applies each scope's rules. An
explicit multi-target configuration runs every target and produces one report.
Selected [repository rules](/concepts/repository-rules) then run exactly once over
that complete target registry.

## Arguments and options

Expand All @@ -34,7 +36,7 @@ explicit multi-target configuration runs every target and produces one report.
| `--no-color` | Disable ANSI color in the output. |
| `--cache` / `--no-cache` | Enable or disable persistent result caching for this run. |
| `--cache-stats` | Write cache operation counts to stderr. |
| `--target TARGET` | Evaluate only one named analyzer target. |
| `--target TARGET` | Evaluate only one named analyzer target; repository rules are skipped. |

Passing paths replaces the configured roots for that run. The rest of the
configuration, including [evaluation targeting](/concepts/configuration#evaluation-targeting),
Expand Down
10 changes: 7 additions & 3 deletions concepts/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ select = ["FPRS"]

Each target requires `analyzer` and `roots`. `root` defaults to `.` and relocates
that target's paths relative to the configuration directory. Explicit `targets`
cannot be mixed with legacy flat keys. `fensu check` evaluates every configured
target unless `--target NAME` selects one.
cannot be mixed with legacy flat keys; the top-level `repository_rules` table is
the one repository-owned policy exception. `fensu check` evaluates every configured
target unless `--target NAME` selects one. See
[Repository rules](/concepts/repository-rules) for aggregate cross-target policy.

The registered analyzers are `python`, `rust`, `typescript`, and `svelte`. Web-only keys
include `test_layout`, `ui_kit`, `shadcn`, `openapi`, and `generated`; the normal
Expand All @@ -110,7 +112,8 @@ silently dropped key is how a typo quietly disables a rule.
- An unknown top-level key is rejected: `Unknown config key(s): ...`.
- A flat config requires `roots`; an explicit config requires one or more named
targets, each with `analyzer` and `roots`.
- Explicit `targets` cannot be mixed with legacy top-level policy keys.
- Explicit `targets` cannot be mixed with legacy top-level policy keys. They may be
accompanied by top-level `repository_rules`.
- `roots` entries must not be nested or overlapping.
- Every configured root path must exist on disk.
- A path may not belong to two scopes, and two scopes may not claim the same
Expand Down Expand Up @@ -139,6 +142,7 @@ silently dropped key is how a typo quietly disables a rule.
| Key | Type | Default | Purpose |
| --- | --- | --- | --- |
| `targets` | table of named targets | none | Explicit analyzer projects; mutually exclusive with all flat keys. |
| `repository_rules` | table | none | Aggregate custom-rule policy for explicit named targets. |
| `roots` | list of strings | required in a flat config or target | Product code scopes. No nesting. |
| `tests` | list of strings | `["tests"]` | Test scopes. |
| `test_scopes` | list of strings | `["unit", "integration", "e2e"]` | Directory names allowed directly under a test root. |
Expand Down
18 changes: 17 additions & 1 deletion concepts/custom-rules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ under `XDB` and `XDJ`, `XDB` one namespace, `XDB001` one rule.

### Typed check subjects

Prefer one of the two typed callback shapes. A file rule runs once for each
Prefer a typed callback shape. A file rule runs once for each
selected source file:

```python
Expand All @@ -159,6 +159,22 @@ Project rules have no elected source anchor. They inspect `ctx.project.tree` and
`ctx.graph`, and report against an explicit confined path with
`ctx.path_fault(path=...)` or `ctx.fault_at(location=...)`.

A repository rule runs once only during an aggregate all-target check and queries
explicit named target handles:

```python
from fensu import Repository


def check(*, repository: Repository, ctx: RuleContext) -> list[Fault]: ...
```

Its analyzers declare the target capabilities it may read. Repository policy lives
once in top-level `[repository_rules]`; `fensu check --target NAME` skips these
rules rather than providing a partial target registry. See
[Repository rules](/concepts/repository-rules) for configuration, target-local
facts, diagnostic translation, and cache behavior.

The legacy `(*, module: ast.Module, ctx: RuleContext)` callback remains supported
for Python rules that need unrestricted Python AST access. New capabilities arrive
through typed subjects and context rather than additional callback parameters.
Expand Down
217 changes: 217 additions & 0 deletions concepts/repository-rules.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
---
title: Repository Rules
description: "Author aggregate custom rules that compare explicit named Python, Rust, TypeScript, and Svelte targets."
---

Repository rules enforce contracts that span more than one configured analyzer
target. They are ordinary Python-authored custom rules, but receive one immutable
`Repository` subject and explicit named target handles instead of a current file or
one target-local project.

Use them for policies such as a backend contract agreeing with a frontend client,
shared schema ownership, or repository-wide dependency direction. A matching name
in two targets is not evidence of a relationship: compare only facts and resolved
edges that Fensu exposes.

## Execution model

A repository rule runs exactly once during an aggregate all-target check:

```bash
fensu check
```

It does not run during a target-scoped check:

```bash
fensu check --target web
```

Skipping is deliberate. A repository rule never receives a partial target
universe. Target-local file and project rules continue to run normally in the same
aggregate check, and all findings are merged into one deterministic report.

Fensu collects target facts only when a repository rule is selected. Native Rust,
TypeScript, and Svelte discovery, parsing, and built-in checks stay native; web
analysis remains Node-free. Fensu starts the bounded Python host only when an active
repository rule requires it.

## Configure repository policy

Repository rules require at least two explicit named targets. Configure their
policy once in the top-level `[repository_rules]` table, not inside any target:

```toml
[targets.backend]
analyzer = "python"
root = "."
roots = ["src/server"]
tests = ["tests"]
tooling = []
select = ["FF"]

[targets.web]
analyzer = "typescript"
root = "frontend"
roots = ["src"]
tests = ["tests"]
tooling = []
select = ["FPTS"]

[repository_rules]
rule_paths = ["rules/repository.py"]
select = ["XREP001"]
```

The table supports the normal custom-rule `rule_paths`, `rule_modules`, `select`,
`warn`, `ignore`, `rule_options`, `rule_ignores`, `rule_exceptions`, `cache`, and
`skills` keys. Paths in repository exceptions and diagnostics are relative to the
repository configuration, not to an individual target. Repository exceptions are
file-level; they do not accept `symbols`.

An active rule is applicable only when the configured registry contains every
analyzer declared by that rule. Inspect its resolved subject, analyzer capabilities,
options, and policy normally:

```bash
fensu rule XREP001
```

Generated project skills include the same repository-rule metadata and explain the
aggregate-only execution contract.

## Author a repository rule

Declare the `Repository` subject and all analyzer capabilities the rule reads:

```python
from fensu import (
AnalyzerId,
Family,
Fault,
ProjectPath,
Repository,
RuleContext,
rule,
)


@rule(
code="XREP001",
family=Family.CUSTOM,
slug="contract-version",
message="Backend and frontend contract versions differ",
analyzers=(AnalyzerId.PYTHON, AnalyzerId.TYPESCRIPT),
cacheable=True,
)
def contract_version(
*, repository: Repository, ctx: RuleContext
) -> list[Fault]:
del repository
backend = ctx.targets.named("backend")
web = ctx.targets.named("web")
if backend is None or web is None:
return []

contract = backend.python.file(ProjectPath("src/server/contracts.py"))
client = web.web.file(ProjectPath("src/client.ts"))
if contract is None or client is None:
return []

if "VERSION = 1" in contract.source and "version = 2" in client.source:
return [ctx.path_fault(path=web.repository_path(client.file))]
return []
```

The callback shape is:

```python
def check(*, repository: Repository, ctx: RuleContext) -> list[Fault]: ...
```

The `Repository` value is the stable identity of the aggregate subject. Read target
state only through `ctx.targets`; repository contexts do not expose a current file,
source, `ctx.project`, `ctx.graph`, or unqualified analyzer facts.

## Target handles

`ctx.targets.all()` returns every configured target in stable name order.
`ctx.targets.named(name)` returns one exact target or `None`.

Each `RuleTargetFacts` handle exposes:

| Member | Purpose |
| --- | --- |
| `identity` | A `Target` with stable `name`, `analyzer`, and repository-relative `root`. |
| `tree` | The target-local deterministic `ProjectTree`. |
| `graph` | The target-local `ArchitectureGraph`. |
| `python` | `PythonWorkspaceFacts` for a Python target. |
| `rust` | `RustWorkspaceFacts` for a Rust target. |
| `web` | `WebWorkspaceFacts` for a TypeScript or Svelte target. |
| `repository_path(value)` | Translate a target-local `File` or `ProjectPath` for a repository diagnostic. |
| `repository_location(value)` | Translate a target-local `SourceLocation` for `ctx.fault_at()`. |

Target tree, graph, and analyzer facts use target-local paths. Always translate a
path or location before reporting it. Accessing facts for the wrong analyzer, or an
analyzer omitted from the rule's `analyzers`, fails closed rather than returning an
ambiguous value.

Python targets expose `PythonWorkspaceFacts.files` and focused
`PythonWorkspaceFacts.file(...)` lookups. Each `PythonFileFacts` contains the
immutable file identity, source, and Fensu-owned fact, text, syntax, and relation
zones. Rust and web targets use the same workspace models described in the
[`RuleContext` reference](/reference/rule-context#ctxrust).

## Diagnostics

Repository findings are repository-relative. Use one of the translated values:

```python
return [ctx.path_fault(path=web.repository_path(client.file))]
```

```python
return [
ctx.fault_at(location=web.repository_location(import_fact.location))
]
```

`ctx.path_fault()` without a path reports against the repository subject `"."`.
As with other custom rules, warnings do not affect the exit code, blocking findings
produce exit code `1`, and configuration or host errors produce exit code `2`.

## Cache behavior

Repository rules use a distinct cache namespace and requester. Cache identity
always includes the complete deterministic target registry: target names,
analyzers, and roots, together with rule implementation and repository policy.
Adding, removing, renaming, relocating, or changing the analyzer of a target
invalidates the result.

Within a stable registry, invalidation follows the queries the rule actually made:

- focused `python.file(...)`, `rust.file(...)`, `web.file(...)`, tree, and graph
lookups survive unrelated edits;
- broad `files`, `tree.paths`, `graph.nodes`, and similar inventory queries
invalidate when their complete answer changes.

Cached and uncached diagnostics and exit codes are identical. `--cache-stats`
includes repository-rule hits, misses, invalidations, writes, and non-cacheable
results in the aggregate counters.

## Related

<CardGroup cols={2}>
<Card title="Custom rules" icon="code" href="/concepts/custom-rules">
Rule metadata, options, loading, testing, and cacheability.
</Card>
<Card title="RuleContext" icon="brackets-curly" href="/reference/rule-context">
Target-local tree, graph, and analyzer fact contracts.
</Card>
<Card title="fensu check" icon="check" href="/cli/check">
Aggregate checks, target selection, diagnostics, and exit codes.
</Card>
<Card title="Caching" icon="database" href="/concepts/caching">
Persistent cache correctness and invalidation.
</Card>
</CardGroup>
1 change: 1 addition & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
]
},
"concepts/custom-rules",
"concepts/repository-rules",
{
"group": "Additional Analyzers",
"pages": [
Expand Down
35 changes: 35 additions & 0 deletions reference/rule-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ never from internal package paths.
```python
def check(*, file: File, ctx: RuleContext) -> list[Fault]: ...
def check(*, project: Project, ctx: RuleContext) -> list[Fault]: ...
def check(*, repository: Repository, ctx: RuleContext) -> list[Fault]: ...
```

The context groups its surface into current-file facts, project/tree facts, an
Expand Down Expand Up @@ -173,6 +174,24 @@ Broad `files` and focused `file()` calls record their exact answers for
subject-aware cache replay. Tree and graph calls made during the same invocation
record their own query-specific dependencies.

## `ctx.targets`

Repository rules use `ctx.targets` instead of an unqualified target context.
`all()` returns every configured `RuleTargetFacts` handle in target-name order;
`named(name)` returns one exact handle or `None`.

Each handle exposes its stable `Target` identity, target-local `tree` and `graph`,
and the typed `python`, `rust`, or `web` workspace facts owned by its analyzer.
`repository_path(file_or_path)` and `repository_location(location)` translate
target-local evidence before constructing a repository-relative diagnostic.
Wrong-analyzer access and access to an analyzer omitted from the active rule's
`analyzers` fail closed.

Repository contexts have no current file, source, `ctx.project`, or unqualified
analyzer zones. Their queries use a distinct repository requester for cache replay.
See [Repository rules](/concepts/repository-rules) for the complete execution and
cache model.

## `ctx.text`, `ctx.syntax`, `ctx.relations`

Source text, opaque syntax identities, and syntax relationships for the current
Expand Down Expand Up @@ -219,6 +238,11 @@ path, and `fault_for()` requires an absolute path confined to the analyzed proje
`fault_at()` accepts a `SourceLocation` or `SourceRange`; a bare `SyntaxHandle`
cannot be resolved without a current file analysis.

During a repository-rule invocation, `path_fault()` accepts a repository-relative
path and may omit it to report against the repository subject `"."`.
`fault_at()` accepts a repository-relative `SourceLocation`; translate a
target-local location with `target.repository_location(...)` first.

## Position and roles

Location facts derived from configured roots and scopes, not hardcoded package
Expand Down Expand Up @@ -343,6 +367,10 @@ are frozen dataclasses; inspect exact fields with your editor's type information
| --- | --- |
| `File` | Immutable identity of one discovered source file. |
| `Project` | Immutable identity of one analyzed project. |
| `Repository` | Immutable identity of one configured multi-target repository. |
| `Target` | Stable name, analyzer, and repository-relative root for one target. |
| `RuleTargetFacts` | One named target's identity, tree, graph, typed facts, and path translators. |
| `RepositoryTargetFacts` | Deterministic `all()` and `named()` target lookup. |
| `ProjectPath` | Normalized, confined, project-relative POSIX path. |
| `ProjectTree` | Deterministic discovered paths, files, and positions. |
| `FilePosition` | Complete analyzer-neutral layout position for one file. |
Expand All @@ -364,6 +392,13 @@ are frozen dataclasses; inspect exact fields with your editor's type information
| `RustItemFact` | Function, struct, enum, trait, implementation or module declaration. |
| `RustUseFact` | Authored `use` path and explicit resolution evidence. |

**Python repository facts**

| Model | Describes |
| --- | --- |
| `PythonWorkspaceFacts` | Versioned broad and focused Python file queries for one named target. |
| `PythonFileFacts` | One Python file's identity, source, and Fensu-owned fact, text, syntax, and relation zones. |

**Web facts**

| Model | Describes |
Expand Down
Loading