Build a Static-Analysis Rule
A Bifrost rule is a versioned CodeQuery plus code that interprets every returned result and its completeness metadata. This guide builds a small “direct calls to Python eval” rule, then shows the production concerns that remain the same for richer reference, call, hierarchy, and import pipelines.
This example is a structural policy: it finds parsed call expressions whose callee is named eval. It does not prove runtime dispatch, taint, reachability, or data flow. Choose a graph step and proof filter when declaration identity matters, and choose another analysis engine when the policy requires an unsupported guarantee.
1. Explore In RQL
Section titled “1. Explore In RQL”Start the interactive prompt from the target repository:
bifrost --root /path/to/project --replEnter the narrowest structural query that expresses the policy:
(result-detail full (limit 1000 (language python (call :callee (name "eval")))))Run :validate, then :run. Inspect positive matches and nearby non-matches before treating the shape as a policy. Use :ir python with representative source when you need to see the normalized syntax Bifrost actually matches.
2. Inspect And Pin Canonical JSON
Section titled “2. Inspect And Pin Canonical JSON”Run :json in the RQL prompt. Save the resulting canonical model under the analyzed workspace, for example as queries/no-direct-eval.json, and include the schema version explicitly:
{ "schema_version": 2, "languages": ["python"], "match": { "kind": "call", "callee": {"name": "eval"} }, "limit": 1000, "result_detail": "full"}RQL and JSON are peer frontends to the same query model. Pinning schema_version makes an incompatible future engine reject the rule instead of quietly interpreting a changed shape. Keep language and path scope in the saved query so every execution surface analyzes the same policy.
3. Execute Through One Supported Surface
Section titled “3. Execute Through One Supported Surface”Use one integration as the production owner of the rule. The other forms are useful for local debugging and parity checks.
bifrost --root /path/to/project --query-file queries/no-direct-eval.jsonThe CLI prints an envelope whose structuredContent is the CodeQueryResult. Check isError before reading it.
Agent MCP
Section titled “Agent MCP”Expose query_code with symbol|extended or searchtools, then call it with exactly:
{"query_file":"queries/no-direct-eval.json"}The path is relative to the active workspace. Do not send raw inline RQL or combine query_file with other fields.
Python
Section titled “Python”from bifrost_searchtools import SearchToolsClient
with SearchToolsClient("/path/to/project") as client: result = client.query_code( {"kind": "call", "callee": {"name": "eval"}}, languages=["python"], limit=1000, result_detail="full", schema_version=2, )The Python method accepts the canonical query fields as typed arguments; it does not currently accept query_file. Keep the checked-in JSON as the source of truth and test that the arguments constructed by the integration remain equivalent when the rule changes.
use brokk_bifrost::analyzer::structural::CodeQueryResult;use brokk_bifrost::SearchToolsService;use serde_json::json;use std::path::PathBuf;
fn run_rule(root: PathBuf) -> Result<CodeQueryResult, String> { let service = SearchToolsService::new_without_semantic_index(root)?; let result = service.query_code_result(json!({ "schema_version": 2, "languages": ["python"], "match": {"kind": "call", "callee": {"name": "eval"}}, "limit": 1000, "result_detail": "full" })).map_err(|error| error.to_string())?; Ok(result)}query_code_result returns the Rust CodeQueryResult directly. Constructing SearchToolsService owns workspace indexing and file watching for a long-lived integration. Use new_without_semantic_index when this rule does not need the optional embedding service.
4. Consume Every Result Variant
Section titled “4. Consume Every Result Variant”Dispatch on result_type in JSON, the corresponding Python dataclass, or CodeQueryResultValue in Rust. Do not assume a pipeline will always end in a structural match: changing or appending a typed step changes the terminal value.
result_type | Produced by | Consume as |
|---|---|---|
structural_match | A query with no steps | Parsed shape, source snippet, captures, enclosing symbol, and optional full ranges/ID. |
declaration | Enclosing, semantic-user, caller/callee, hierarchy, member, or owner steps | Exact indexed declaration identity and source range. |
reference_site | references_of | Exact source site, target declaration, optional enclosing declaration, reference kind, and proof. |
call_site | call_sites_to or call_sites_from | Caller, callee, call/callee ranges, receiver, arguments, call kind, and proof. |
expression_site | call_input | Direct receiver or formal-parameter expression at a resolved call site. |
file | file_of, imports_of, or importers_of | Exact project file reached by the pipeline. |
In Python, the six classes are CodeQueryMatch, CodeQueryDeclaration, CodeQueryReferenceSite, CodeQueryCallSite, CodeQueryExpressionSite, and CodeQueryFile. In Rust, match all variants of brokk_bifrost::analyzer::structural::CodeQueryResultValue without a wildcard so a future result type becomes a compile-time integration decision.
Request result_detail: "full" when a report needs stable IDs, byte/line/column ranges, capture ranges, or decorator ranges. Compact mode is designed for agent context, not durable finding identity.
5. Gate On Safety Metadata
Section titled “5. Gate On Safety Metadata”Before a rule passes, fails, or claims completeness:
- Treat a transport or validation error as a rule execution failure, not as zero findings.
- Inspect every capability and execution diagnostic. A diagnostic can mean part of the requested language or role was not searched.
- Require
truncated == falsefor a complete rule result. Raising onlylimitmay not remove a scan or pipeline-work budget diagnostic; narrow the query or split the scope when necessary. - Decide whether
unprovengraph edges are findings, warnings, or excluded evidence. Never present them as proven. - Check
provenance_truncatedon every derived result before claiming that all paths to that result are represented.
Use Agent Result Safety for the exact claim vocabulary and zero-result rules.
6. Add Fixture Regression Tests
Section titled “6. Add Fixture Regression Tests”Keep the smallest source project that demonstrates a true positive, a nearby false positive, and any important language diagnostic. Assert the canonical query, complete tagged result, proof fields, and truncated: false; do not assert only a match count.
For Bifrost’s own documentation, the executable marker harness in tests/code_query_tutorials.rs builds inline projects from code-query-fixture blocks and executes paired RQL/JSON cases. External integrations can use their normal test framework, but should preserve the same properties:
- RQL and canonical JSON lower to the same query while RQL is part of authoring.
- Every expected finding includes a stable project-relative path and exact range.
- Unsupported language features retain their diagnostic instead of becoming a zero-match assertion.
- Dynamic candidates test their
provenorunprovencontract explicitly. - A negative fixture is structurally similar enough to catch an over-broad rule.
7. Integrate With CI And Reports
Section titled “7. Integrate With CI And Reports”Run the saved query against the exact revision being reviewed. Record the Bifrost version or commit, schema_version, query file hash or revision, workspace root, source revision, and whether the run was cold or warm when timing matters.
For a CI gate, fail closed on execution errors, diagnostics that affect the intended scope, or truncation. Then apply the rule’s policy to the typed findings. For a report, map each finding from its result variant rather than guessing a location: structural matches use node_range in full mode, declarations use their declaration range, and reference/call/expression sites carry explicit ranges.
Bifrost does not currently emit a universal policy-report format from query_code. If the consumer needs SARIF or another interchange format, write a small adapter that preserves result_type, path/range, proof, diagnostics, and reproducibility metadata. Do not flatten importer files into symbol usages or structural matches into resolved declarations during conversion.
Production Checklist
Section titled “Production Checklist”- The saved canonical JSON pins
schema_versionand contains all scope. - The integration handles all six terminal result variants.
- Errors, diagnostics, result truncation, proof tiers, and provenance truncation have explicit policy.
- Fixture tests cover a true positive, a convincing negative, and relevant diagnostics.
- CI records the engine, query, workspace, and source revisions.
- Reports cite exact returned locations and do not claim unsupported control or data flow.