Skip to content

Commit 0f2bba9

Browse files
committed
feat(policy): starter pack + CI exit codes; advertise on the site
Three new starter policies make Lane H actually adoptable instead of just possible. Together with the existing no-huge-files.rfl they cover the four most common review patterns teams want a CI gate on: - concentrated-ownership.rfl bus_factor == 1 on >= 5 commits - change-hotspot.rfl temporal_hotspots.risk_score > 200 - tightly-coupled-pairs.rfl coupling_strength_milli > 500 and co_commits >= 3 (info-severity) Each runs end-to-end against raysense's own self-baseline today; agents and CI both get back deterministic, code-reviewable rules. CI exit codes turn `raysense policy check` from a reporting tool into a gate. The shared helper `memory::policy_exit_code` returns 0 for clean runs, 1 when any policy itself fails to evaluate (parse / type / schema error -- "I cannot tell whether the rule passed"), 2 when every policy parsed but at least one reported an error-severity finding. Eval errors outrank findings because a misconfigured policy is more dangerous than a known violation. Both the CLI and the MCP tool surface the code; MCP also keeps a `pass: bool` for ergonomics. The mutex in src/memory.rs#tests is the cost of having a process- singleton rayforce runtime: ray_sym_load clobbers the global sym table on every read, ray_env_set rebinds 18 names per policy run, and Cargo runs tests across multiple threads. RAYFORCE_TEST_LOCK serializes the affected tests; the few that only build a RayMemory in memory and never read back from disk also take the guard for safety since ray_sym_intern shares state with the loader path. Without this the new tests flaked at 4 of 151 fail under contention. With it: 151/151 stable across repeated runs (~11s wall clock). Site update: a new "Query + Policy" section on sense.rayforcedb.com positions the Rayfall + policy-packs surface as the headline differentiator -- "architectural rules as code, not config." The agent-integration block bumps from four to five skills (the new raysense-query); the capabilities band shifts to alt-bg to keep the visual rhythm. llms.txt mirrors the same content for crawlers.
1 parent 2105a70 commit 0f2bba9

8 files changed

Lines changed: 238 additions & 17 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
;; Policy: high churn x high complexity hotspots.
2+
;;
3+
;; raysense's temporal_hotspots table already multiplies commits by
4+
;; max_complexity into a single risk_score. Files past 200 are the
5+
;; spots most likely to break under further change.
6+
;;
7+
;; Demonstrates a single-column threshold on a derived metric.
8+
9+
(select {severity: "warning"
10+
code: "change-hotspot"
11+
path: path
12+
message: "high commit-frequency x complexity; refactor before adding more"
13+
from: temporal_hotspots
14+
where: (> risk_score 200)})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
;; Policy: bus-factor risk on long-lived files.
2+
;;
3+
;; Files with bus_factor == 1 (one author owns >=50% of commits) plus
4+
;; non-trivial history (>=5 commits) are a maintenance liability:
5+
;; if that author leaves, knowledge about the file leaves with them.
6+
;;
7+
;; Demonstrates the `and` predicate over a single baseline table.
8+
9+
(select {severity: "warning"
10+
code: "bus-factor-1"
11+
path: path
12+
message: "single owner controls a file with non-trivial history; spread reviews / pair on changes"
13+
from: file_ownership
14+
where: (and (== bus_factor 1) (>= total_commits 5))})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
;; Policy: emerging architectural coupling.
2+
;;
3+
;; Files that change together more than half the time (per-mille > 500)
4+
;; AND have at least 3 co-commits are signaling hidden coupling: either
5+
;; they should live together (extract module) or one is leaking concerns
6+
;; into the other. The right-hand partner column is preserved in the
7+
;; result table so reviewers can see both files - raysense surfaces
8+
;; extra columns alongside the required severity/code/path/message.
9+
10+
(select {severity: "info"
11+
code: "tight-coupling"
12+
path: left
13+
message: "co-changes with another file more than 50% of the time"
14+
partner: right
15+
co_commits: co_commits
16+
from: change_coupling
17+
where: (and (> coupling_strength_milli 500)
18+
(>= co_commits 3))})

site/index.html

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ <h2>One binary. A few flags.</h2>
233233
<div class="section-eyebrow">AGENT INTEGRATION</div>
234234
<h2>MCP server for any AI coding agent.</h2>
235235
<p class="section-lead">
236-
Raysense ships as a Claude Code plugin and as a stdio MCP server compatible with any client that speaks the Model Context Protocol. Install the plugin and the agent gets four phase-scoped skills it can pick up at the right moments in its edit cycle. Project state lives in <code>&lt;repo&gt;/.raysense/</code>, never in a global registry, so two sessions on two repositories stay strictly independent.
236+
Raysense ships as a Claude Code plugin and as a stdio MCP server compatible with any client that speaks the Model Context Protocol. Install the plugin and the agent gets five skills covering the edit loop: scan and baseline at session start, blast radius before edits, regression diff after, audits on demand, and a Rayfall query skill for any question the typed tools do not already answer. Project state lives in <code>&lt;repo&gt;/.raysense/</code>, never in a global registry, so two sessions on two repositories stay strictly independent.
237237
</p>
238238

239239
<div class="code-block code-block-wide">
@@ -263,11 +263,57 @@ <h3>Verify</h3>
263263
<h3>Audit</h3>
264264
<p>On request. Architecture, DSM, evolution, test gaps.</p>
265265
</div>
266+
<div class="phase-card">
267+
<div class="phase-step">5</div>
268+
<h3>Query</h3>
269+
<p>Anytime. Custom Rayfall slices over the saved baseline.</p>
270+
</div>
271+
</div>
272+
</div>
273+
</section>
274+
275+
<section id="query" class="section">
276+
<div class="container">
277+
<div class="section-eyebrow">QUERY + POLICY</div>
278+
<h2>Architectural rules as code, not config.</h2>
279+
<p class="section-lead">
280+
Every saved baseline is a queryable columnar database. Agents and humans run Rayfall expressions over the call graph, the import graph, ownership history, and change coupling - select queries for filters and aggregates, <code>.graph.*</code> algorithms for centrality and reachability (PageRank, Louvain, topsort, shortest-path, betweenness), Datalog rules with transitive closure for declarative reachability. Drop the same expression into a <code>.rfl</code> file under <code>.raysense/policies/</code> and it becomes a CI gate. No vendored YAML schema, no plugin SDK to learn - rules ship as code-reviewable files alongside the codebase they govern.
281+
</p>
282+
283+
<div class="code-block code-block-wide">
284+
<div class="code-block-head">.raysense/policies/no-huge-files.rfl</div>
285+
<pre><code><span class="hl-cmt">;; Files over 2000 lines block the merge.</span>
286+
<span class="hl-cmt">;; Result table columns: severity, code, path, message.</span>
287+
(select {severity: "error"
288+
code: "huge-file"
289+
path: path
290+
message: "file exceeds 2000 lines, split before merging"
291+
from: files
292+
where: (&gt; lines 2000)})</code></pre>
293+
</div>
294+
295+
<div class="feature-grid">
296+
<div class="feature-card">
297+
<h3>Ad-hoc Rayfall queries</h3>
298+
<p>Agents call <code>raysense_baseline_query</code> with a Rayfall expression. The named baseline table is bound as <code>t</code> and the result returns as JSON. Three modes: select for filter/project/aggregate, <code>.graph.*</code> for centrality and shortest-path, Datalog for transitive reachability that mirrors blast-radius in two lines.</p>
299+
</div>
300+
<div class="feature-card">
301+
<h3>Pinned policies</h3>
302+
<p><code>raysense policy check</code> walks <code>.raysense/policies/*.rfl</code>, evaluates each, returns findings in the same envelope as built-in rules. Exit code 0 for pass, 1 for any policy that failed to evaluate, 2 for any error-severity finding. Wire it into a pre-commit hook or a CI gate without touching raysense's release cadence.</p>
303+
</div>
304+
<div class="feature-card">
305+
<h3>One vocabulary, two surfaces</h3>
306+
<p>An <code>.rfl</code> policy and an interactive query reference the same baseline tables - <code>files</code>, <code>module_edges</code>, <code>change_coupling</code>, <code>file_ownership</code>, <code>call_edges</code> - because there is only one substrate. Promote a one-off query into a committed rule by renaming the file.</p>
307+
</div>
308+
<div class="feature-card">
309+
<h3>Composable across history</h3>
310+
<p>The baseline already carries change-coupling, file-ages, ownership, and rule-violation tables alongside the structural ones. Cross-time queries like "files tightly coupled in the last 60 days that sit on cycles and changed without test edits" stay one Rayfall expression, not three tools.</p>
311+
</div>
266312
</div>
267313
</div>
268314
</section>
269315

270-
<section class="section">
316+
<section class="section section-alt">
271317
<div class="container">
272318
<div class="section-eyebrow">CAPABILITIES</div>
273319
<h2>Beyond the score.</h2>

site/llms.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,25 @@ raysense --mcp # stdio MCP server for agents
3838

3939
## Agent integration
4040

41-
Raysense ships as a Claude Code plugin. The plugin gives the agent four phase-scoped skills:
41+
Raysense ships as a Claude Code plugin. The plugin gives the agent five skills covering the edit loop:
4242

4343
1. Bootstrap. At session start: scan, save baseline, materialise scan results as splayed-table memory.
4444
2. Impact. Before non-trivial edits: blast radius, coupling, cycle exposure.
4545
3. Verify. After edits: rescan, rule check, baseline diff.
4646
4. Audit. On request: architecture, DSM, evolution signals, test gaps.
47+
5. Query. Anytime: ad-hoc Rayfall expressions over the saved baseline (filter, project, aggregate, graph algorithms, Datalog rules with transitive closure).
4748

4849
Project state lives in `<repo>/.raysense/`, never in a global registry. Two concurrent sessions on two different repos are strictly independent.
4950

51+
## Query and policy
52+
53+
Every saved baseline is a queryable columnar database. Two surfaces share one substrate:
54+
55+
- Ad-hoc query: `raysense_baseline_query` (MCP) or `raysense baseline query <table> <expr>` (CLI). Three modes - select queries for filter/project/aggregate, `.graph.*` algorithms (PageRank, Louvain, topsort, shortest-path, betweenness, closeness, k-shortest, MST, BFS/DFS expand) for centrality and reachability, Datalog rules with transitive closure for declarative reachability.
56+
- Pinned policy: drop a `.rfl` file in `<repo>/.raysense/policies/` and `raysense policy check` (or the `raysense_policy_check` MCP tool) walks the directory, evaluates each policy, and reports findings using the same RuleFinding envelope as built-in rules. Exit code 0 for pass, 1 if any policy failed to evaluate, 2 if any error-severity finding. Architectural rules ship as code-reviewable files alongside the codebase they govern - no vendored YAML schema, no plugin SDK to learn.
57+
58+
A policy is just a Rayfall expression that returns a table with columns severity, code, path, message. Empty result table = policy passed.
59+
5060
## Capabilities beyond the score
5161

5262
- Live treemap dashboard. Every file, every metric, every cycle, refreshed on save.

src/cli.rs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,12 @@ fn run_advanced(command: Command) -> Result<()> {
397397
baseline,
398398
policies,
399399
json,
400-
} => run_policy_check(baseline, policies, json)?,
400+
} => {
401+
let exit = run_policy_check(baseline, policies, json)?;
402+
if exit != 0 {
403+
process::exit(exit);
404+
}
405+
}
401406
},
402407
Command::Trend { command } => match command {
403408
TrendCommand::Record { path, config } => record_trend(&path, config.as_deref())?,
@@ -2886,11 +2891,16 @@ fn default_policies_dir() -> PathBuf {
28862891
PathBuf::from(".raysense/policies")
28872892
}
28882893

2894+
/// Returns 0 on success, 1 if any policy fails to evaluate (parse / type
2895+
/// error, schema mismatch, missing columns), or 2 if any policy reports an
2896+
/// error-severity finding. Eval errors take precedence over findings:
2897+
/// "I cannot tell whether the rule passed" is worse than "the rule
2898+
/// definitively failed."
28892899
fn run_policy_check(
28902900
baseline: Option<PathBuf>,
28912901
policies: Option<PathBuf>,
28922902
json: bool,
2893-
) -> Result<()> {
2903+
) -> Result<i32> {
28942904
let baseline = baseline.unwrap_or_else(default_baseline_dir);
28952905
let tables_dir = baseline.join("tables");
28962906
let policies_dir = policies.unwrap_or_else(default_policies_dir);
@@ -2903,6 +2913,8 @@ fn run_policy_check(
29032913
)
29042914
})?;
29052915

2916+
let exit = crate::memory::policy_exit_code(&results);
2917+
29062918
if json {
29072919
let payload: Vec<serde_json::Value> = results
29082920
.iter()
@@ -2919,16 +2931,22 @@ fn run_policy_check(
29192931
}),
29202932
})
29212933
.collect();
2922-
println!("{}", serde_json::to_string_pretty(&payload)?);
2923-
return Ok(());
2934+
println!(
2935+
"{}",
2936+
serde_json::to_string_pretty(&serde_json::json!({
2937+
"exit": exit,
2938+
"policies": payload,
2939+
}))?
2940+
);
2941+
return Ok(exit);
29242942
}
29252943

29262944
if results.is_empty() {
29272945
println!(
29282946
"no policies found at {} (looking for *.rfl files)",
29292947
policies_dir.display(),
29302948
);
2931-
return Ok(());
2949+
return Ok(exit);
29322950
}
29332951
let mut total = 0usize;
29342952
let mut errors = 0usize;
@@ -2951,12 +2969,13 @@ fn run_policy_check(
29512969
}
29522970
}
29532971
println!(
2954-
"{} policy file(s) evaluated, {} finding(s), {} eval error(s)",
2972+
"{} policy file(s) evaluated, {} finding(s), {} eval error(s); exit {}",
29552973
results.len(),
29562974
total,
29572975
errors,
2976+
exit,
29582977
);
2959-
Ok(())
2978+
Ok(exit)
29602979
}
29612980

29622981
fn parse_columns(columns: Option<&str>) -> Result<Option<Vec<String>>> {

src/mcp.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -885,16 +885,12 @@ fn policy_check_tool(args: &Value) -> Result<Value> {
885885
}),
886886
})
887887
.collect();
888-
let pass = results.iter().all(|r| match &r.findings {
889-
Ok(findings) => !findings
890-
.iter()
891-
.any(|f| matches!(f.severity, crate::RuleSeverity::Error)),
892-
Err(_) => false,
893-
});
888+
let exit = crate::memory::policy_exit_code(&results);
894889
Ok(json!({
895890
"root": root,
896891
"policies_path": policies_dir,
897-
"pass": pass,
892+
"pass": exit == 0,
893+
"exit": exit,
898894
"policies": payload,
899895
"total": results.len(),
900896
}))

0 commit comments

Comments
 (0)