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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- `ctx check`: architecture rules engine driven by `.ctx/rules.toml` -- declare layers as glob patterns over indexed files, then enforce `forbidden` layer dependencies, `allowed_dependents` whitelists, `limit` metric thresholds (fan-in / fan-out / complexity / file symbols), and `no_new_dependents` frozen paths; supports `--against REF` to scope violations to changed files, `--list` to inspect parsed rules, and `--json`; exits 1 when violations are found (see `ctx check --help` for a full example)
- Global `--json` flag: `search`, `semantic`, `query find/callers/deps/graph/impact/stats/files`, and `explain` emit a single machine-readable JSON document wrapped in a stable envelope (`ctx_version`, `command`, `generated_at`, `data`); see `docs/json-output.md`
- Index schema versioning via SQLite `PRAGMA user_version`; opening an index built with an incompatible schema now fails with a clear "run `ctx index --force`" message (pre-existing indexes are stamped silently)
- Shared complexity metrics (fan-in / fan-out / complexity) available directly from the SQLite index, mirroring the DuckDB formula
Expand Down
65 changes: 65 additions & 0 deletions docs/json-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,71 @@ Disambiguation: when several symbols match the name and no `--file` filter is gi

When the symbol is not found, `symbol` is `null` and the counts are `0`; when several symbols match without filters, `ambiguous` lists the candidates.

### `check`

`ctx check [--rules PATH] [--against REF] --json`

```json
{
"rules_path": ".ctx/rules.toml",
"against": "main",
"summary": { "violations": 2, "rules_violated": 2 },
"violations": [
{
"rule": "forbidden",
"rule_id": "forbidden: domain -> infrastructure",
"reason": "Domain layer must stay persistence-agnostic",
"message": "src/domain/order.ts:2 -> src/infra/db.ts [calls query]",
"file": "src/domain/order.ts",
"line": 2,
"from": { "name": "order", "qualified_name": null, "kind": "function", "file": "src/domain/order.ts", "line_start": 2, "line_end": 2 },
"to": { "name": "query", "qualified_name": null, "kind": "function", "file": "src/infra/db.ts", "line_start": 1, "line_end": 1 }
},
{
"rule": "limit",
"rule_id": "limit: fan_in <= 25 (symbol)",
"reason": "fan_in 30 exceeds max 25",
"message": "src/app/hub.ts:10 (handle): fan_in 30 exceeds max 25",
"file": "src/app/hub.ts",
"line": 10,
"subject": { "name": "handle", "qualified_name": null, "kind": "function", "file": "src/app/hub.ts", "line_start": 10, "line_end": 42 },
"metric": "fan_in",
"scope": "symbol",
"value": 30,
"max": 25
}
]
}
```

One entry per violation. `rule` is one of `forbidden`, `allowed_dependents`, `limit`, or `no_new_dependents`; `rule_id` identifies the specific rule instance (violations with the same `rule_id` belong to the same rule).

Dependency violations (`forbidden`, `allowed_dependents`, `no_new_dependents`) carry `from`/`to` endpoints. Symbol-level endpoints (resolved call/implements/extends/uses edges) are full SymbolRefs; file-level endpoints (resolved imports) are `{"file": ...}` objects. `limit` violations carry a `subject` endpoint plus `metric`, `scope`, `value`, and `max`. Absent optional fields (`line`, `from`, `to`, `subject`, the metric fields) are omitted rather than `null`. `against` is `null` when `--against` was not given.

Exit codes follow the suite convention: 0 = no violations, 1 = at least one violation, 2 = operational error (missing/invalid rules file, unknown or overlapping layers, missing index, bad git ref).

### `check.list`

`ctx check --list --json`

```json
{
"rules_path": ".ctx/rules.toml",
"version": 1,
"layers": [
{ "name": "domain", "patterns": ["src/domain/**"], "files": 12 }
],
"rules": {
"forbidden": [ { "from": "domain", "to": "infrastructure", "reason": "Domain layer must stay persistence-agnostic" } ],
"allowed_dependents": [ { "layer": "infrastructure", "only": ["application"], "reason": null } ],
"limit": [ { "metric": "fan_in", "scope": "symbol", "max": 25, "exclude": ["src/core/**"] } ],
"no_new_dependents": [ { "paths": ["src/legacy/**"], "reason": "Legacy module is frozen; do not add new callers" } ]
}
}
```

`files` is the number of indexed files matching the layer's globs. `--list` always exits 0.

## Legacy shapes

`ctx complexity --output json`, `ctx graph --output json`, and `ctx audit --output json` still emit their old, ad-hoc JSON shapes. They will be migrated to the envelope in a future release. The old ad-hoc shapes of `search --output json` and `semantic --output json` have already been **replaced** by the envelope described above.
53 changes: 53 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,59 @@ pub enum Command {
incremental: bool,
},

/// Check architecture rules from .ctx/rules.toml against the index
///
/// Exit codes: 0 = no violations, 1 = at least one violation,
/// 2 = operational error (missing/invalid rules file, unknown or
/// overlapping layers, missing index, bad git ref).
#[command(after_help = r#"RULES FILE (.ctx/rules.toml):
version = 1

[layers] # layer name -> globs over indexed files
domain = ["src/domain/**"]
application = ["src/app/**"]
infrastructure = ["src/infra/**", "src/db/**"]

[[rules.forbidden]] # `from` must not depend on `to`
from = "domain"
to = "infrastructure"
reason = "Domain layer must stay persistence-agnostic"

[[rules.allowed_dependents]] # only `only` may depend on `layer`
layer = "infrastructure" # (files in no layer are exempt)
only = ["application"]

[[rules.limit]] # metric thresholds
metric = "fan_in" # fan_in | fan_out | complexity | file_symbols
scope = "symbol" # symbol | file
max = 25
exclude = ["src/core/**"]

[[rules.no_new_dependents]] # frozen paths
paths = ["src/legacy/**"]
reason = "Legacy module is frozen; do not add new callers"

EXAMPLES:
ctx check # check all rules
ctx check --against main # only violations touching files changed since main
ctx check --list # show parsed rules and layer sizes
ctx check --json # machine-readable output (see docs/json-output.md)
"#)]
Check {
/// Path to the rules file (default: .ctx/rules.toml)
#[arg(long)]
rules: Option<std::path::PathBuf>,

/// Only report violations where at least one endpoint's file changed
/// since REF (for no_new_dependents: where the new dependent changed)
#[arg(long, value_name = "REF")]
against: Option<String>,

/// Print the parsed rules and layer membership counts, then exit 0
#[arg(long)]
list: bool,
},

/// Interactive shell for exploring codebase
Shell {
/// History file location
Expand Down
Loading
Loading