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
49 changes: 43 additions & 6 deletions crates/wright-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,14 @@ fn lint_over_workshop_input_reports_findings_in_text_and_json() {
.len(),
rules.len()
);
for rule in rules {
assert!(rule["id"].is_string() && rule["effectiveSeverity"].is_string());
assert_eq!(
rule.as_object().unwrap().len(),
2,
"lint inlines only id and effectiveSeverity; full metadata is lintRules (#431): {rule}"
);
}
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}

Expand Down Expand Up @@ -454,12 +462,10 @@ fn lint_rule_flags_control_findings() {
.all(|finding| finding["code"] != "min-wait-loop"),
"the disabled rule must produce no findings"
);
let rules = envelope["result"]["rules"].as_array().unwrap();
let min_wait = rules
.iter()
.find(|rule| rule["id"] == "min-wait-loop")
.unwrap();
assert_eq!(min_wait["enabled"], false);
assert_eq!(
envelope["result"]["config"]["rules"]["min-wait-loop"]["enabled"],
false
);

// --rule-severity overrides the effective severity of a rule. The
// control-flow fixture produces no expensive-loop-check findings, so
Expand Down Expand Up @@ -487,6 +493,37 @@ fn lint_rule_flags_control_findings() {
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}

#[test]
fn lint_lists_a_local_yaml_rule_loaded_with_the_rule_flag() {
let path = temp_file("flow.txt", &corpus_workshop("synthetic/control-flow"));
let rule_file = temp_file(
"cli-smoke.yaml",
"id: community/cli-smoke\nmetadata:\n summary: summary\n rationale: rationale\n documentation: documentation\n known-limits: limits\n tags: []\nmatcher: {}\n",
);
let output = run(&[
"lint",
path.to_str().unwrap(),
"--rule",
rule_file.to_str().unwrap(),
"-f",
"json",
]);
assert!(output.status.success(), "{}", command_result(&output));
let envelope = parse_json(&output.stdout);
let rules = envelope["result"]["rules"].as_array().unwrap();
let local = rules
.iter()
.find(|rule| rule["id"] == "community/cli-smoke")
.expect("every registered rule, including a --rule YAML rule, appears");
assert_eq!(
local,
&serde_json::json!({ "id": "community/cli-smoke", "effectiveSeverity": "warning" }),
"local rules carry the same compact shape (#431)"
);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
let _ = std::fs::remove_dir_all(rule_file.parent().unwrap());
}

#[test]
fn lint_flags_are_usage_errors_for_other_commands() {
for flags in [
Expand Down
67 changes: 67 additions & 0 deletions crates/wright-driver/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,29 @@ pub struct LintResult {
pub skipped: serde_json::Value,
}

/// `lint` results keep only the per-rule identity needed to interpret a
/// finding — the stable id and its effective severity (#431). Full rule
/// metadata (summary, rationale, documentation, known limits, evidence, tags)
/// is served by the `lintRules` operation instead of being inlined per call.
pub(crate) fn compact_lint_rules(rules: Option<&serde_json::Value>) -> serde_json::Value {
let Some(rules) = rules.and_then(serde_json::Value::as_array) else {
return serde_json::json!([]);
};
serde_json::Value::Array(
rules
.iter()
.filter_map(|rule| {
let id = rule.get("id").filter(|v| v.is_string())?;
let effective_severity = rule.get("effectiveSeverity").filter(|v| v.is_string())?;
Some(serde_json::json!({
"id": id,
"effectiveSeverity": effective_severity,
}))
})
.collect(),
)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ConvertTarget {
Expand Down Expand Up @@ -134,3 +157,47 @@ pub fn version_info() -> VersionInfo {
contract: RESULT_CONTRACT.to_string(),
}
}

#[cfg(test)]
mod tests {
use super::compact_lint_rules;
use serde_json::json;

#[test]
fn compact_lint_rules_keeps_only_id_and_effective_severity() {
let rules = json!([
{
"id": "min-wait-loop",
"defaultSeverity": "warning",
"effectiveSeverity": "error",
"enabled": false,
"summary": "loop body waits at the workshop minimum rate",
"rationale": "…",
"documentation": "…",
"knownLimits": "…",
"evidence": "static-indicator",
"tags": ["stability"],
"kind": "builtin"
}
]);
assert_eq!(
compact_lint_rules(Some(&rules)),
json!([{ "id": "min-wait-loop", "effectiveSeverity": "error" }])
);
}

#[test]
fn compact_lint_rules_drops_malformed_entries() {
for rules in [
json!(null),
json!("not-an-array"),
json!(["not-an-object"]),
json!([{ "id": "min-wait-loop" }]),
json!([{ "id": "min-wait-loop", "effectiveSeverity": null }]),
json!([{ "id": 3, "effectiveSeverity": "warning" }]),
] {
assert_eq!(compact_lint_rules(Some(&rules)), json!([]), "{rules}");
}
assert_eq!(compact_lint_rules(None), json!([]));
}
}
16 changes: 10 additions & 6 deletions crates/wright-driver/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@ pub enum ToolRequest {
Findings,
/// Persistent Workshop object facts, separate from lint diagnostics.
PersistentObjects,
/// Lint findings plus rule metadata and effective configuration (#98).
/// Lint findings plus per-rule id/effective severity and the effective
/// configuration (#98); `lintRules` serves full rule metadata (#431).
Lint,
/// The registered lint rules and the effective lint configuration.
/// The registered lint rules with full metadata and the effective lint
/// configuration.
LintRules,
/// The subroutine call graph (caller rules → callee subroutines).
CallGraph,
Expand Down Expand Up @@ -439,9 +441,11 @@ impl<'a> ToolService<'a> {
self.lint_semantic.as_ref().unwrap_or(&self.semantic)
}

/// `lint`: rule metadata, effective configuration, and findings over the
/// loaded program through the same semantic-service path as the CLI
/// `lint` workflow (no duplicated rule execution, #98).
/// `lint`: per-rule id and effective severity, effective configuration,
/// and findings over the loaded program through the same semantic-service
/// path as the CLI `lint` workflow (no duplicated rule execution, #98).
/// Full rule metadata is served once by `lintRules` rather than inlined
/// into every `lint` response (#431).
fn lint(&self) -> ToolResponse {
let service = self.configured_semantic();
let lint_rules = match service.handle(&Request::LintRules) {
Expand All @@ -455,7 +459,7 @@ impl<'a> ToolService<'a> {
crate::session::resolve_span_paths(&mut findings, &self.loaded);
self.ok(json!({
"inputIdentity": self.loaded.input.identity,
"rules": lint_rules.get("rules").cloned().unwrap_or_else(|| json!([])),
"rules": crate::result::compact_lint_rules(lint_rules.get("rules")),
"config": lint_rules.get("config").cloned().unwrap_or_else(|| json!({})),
"findings": findings,
"skipped": lint_rules.get("skipped").cloned().unwrap_or_else(|| json!([])),
Expand Down
10 changes: 5 additions & 5 deletions crates/wright-driver/src/session/semantic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,10 @@ impl CompilerSession {
)
}

/// `lint`: load and produce the source identity, program summary, rule
/// metadata, effective configuration, and findings (#98).
/// `lint`: load and produce the source identity, program summary, per-rule
/// id and effective severity, effective configuration, and findings (#98).
/// Full rule metadata is served by `lintRules` rather than inlined into
/// every `lint` result (#431).
///
/// Lint rule findings are reported in `result.findings`; frontend and
/// Workshop semantic-completeness diagnostics remain in the envelope.
Expand Down Expand Up @@ -242,9 +244,7 @@ impl CompilerSession {
let (rules, config, skipped) =
if let serde_json::Value::Object(mut object) = lint_rules {
(
object
.remove("rules")
.unwrap_or_else(|| serde_json::json!([])),
crate::result::compact_lint_rules(object.remove("rules").as_ref()),
object
.remove("config")
.unwrap_or_else(|| serde_json::json!({})),
Expand Down
19 changes: 19 additions & 0 deletions crates/wright-driver/tests/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,25 @@ fn tool_service_lint_queries_keep_the_session_configuration() {
ToolResponse::Error { error } => panic!("lint failed: {error:?}"),
};
assert_eq!(lint["config"], lint_rules["config"]);
// #431: `lint` inlines only the finding-interpretation fields;
// `lintRules` remains the full-metadata surface.
let lint_rule = lint["rules"]
.as_array()
.unwrap()
.iter()
.find(|rule| rule["id"] == "min-wait-loop")
.expect("lint lists every registered rule");
assert_eq!(
lint_rule,
&serde_json::json!({ "id": "min-wait-loop", "effectiveSeverity": "error" })
);
let full_rule = lint_rules["rules"]
.as_array()
.unwrap()
.iter()
.find(|rule| rule["id"] == "min-wait-loop")
.expect("lintRules lists every registered rule");
assert!(full_rule["summary"].is_string());
let configured_finding = lint["findings"]
.as_array()
.unwrap()
Expand Down
13 changes: 10 additions & 3 deletions docs/agent-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ the successful `result` payload.
| `cfg` | required `rule` | Control-flow graph for the rule id |
| `findings` | none | Wright static-analysis findings |
| `persistentObjects` | none | Persistent Workshop object facts |
| `lint` | none | Lint findings, rule metadata, and effective configuration |
| `lintRules` | none | Registered lint rules and effective configuration |
| `lint` | none | Lint findings, per-rule id/effective severity, and effective configuration |
| `lintRules` | none | Registered lint rules with full metadata and effective configuration |
| `callGraph` | none | Subroutine call graph |
| `costEstimate` | none | Exact generated-resource counts and separate static findings |
| `targetMetadata` | none | Canonical target/catalog metadata |
Expand Down Expand Up @@ -126,7 +126,14 @@ operations whose requests remain valid for existing clients. Removing or
renaming an operation or field, changing a field's type or meaning, or changing
the response/error model requires a new major contract such as
`wright-agent/v2`; the v1 schema and its compatibility tests remain in place.
The CLI result envelope has its independent `wright-result/v1` version.
The CLI result envelope has its independent `wright-result/v1` version; its
evolution policy is defined in [`docs/cli/machine-contract.md`](cli/machine-contract.md).

One recorded exception applies to the same rule in both contracts, decided
before the 1.0 contract freeze (#134): the `lint` result's `rules` member
lists only each rule's `id` and `effectiveSeverity` (#431). Full rule
metadata — summary, rationale, documentation, known limits, evidence, tags —
is served once by `lintRules`.

The optional guide distributed by `wrightkit/skills` teaches clients to
discover and use these capabilities. It is not required to expose, execute, or
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ result.
| `wright convert [INPUT] --target opy\|ostw` | Reconstruct validated Workshop input as canonical OPY or OSTW source | the reconstructed source |
| `wright check [INPUT]` | Parse, lower, validate, and report correctness diagnostics | verdict and validation diagnostics |
| `wright analyze [INPUT]` | Summarize project structure, ranked CFG hotspots, and cross-cutting state | bounded semantic report with static evidence labels |
| `wright lint [INPUT]` | Parse, lower, lint; report findings | findings, rule metadata, and effective-configuration summary |
| `wright lint [INPUT]` | Parse, lower, lint; report findings | findings, rule id/severity summary, and effective configuration |
| `wright inspect [INPUT]` | Parse, lower, and inspect exhaustive semantic facts | rules, symbols, references summary |
| `wright serve [INPUT]` | Serve `wright-agent/v1` over stdio or JSON-RPC 2.0 | one structured response per request |
| `wright completion <SHELL>` | Generate static completion script for bash, zsh, fish, or powershell | the generated completion script |
Expand Down
82 changes: 17 additions & 65 deletions docs/cli/lint.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ any condition.

The `lint` result envelope carries `input_identity` (the SHA-256 source
identity; the tool/agent API exposes the same value as `inputIdentity`),
`program`, `rules`, `config`, and `findings`:
`program`, `rules`, `config`, `findings`, and `skipped`. `rules` lists each
registered rule's stable `id` and `effectiveSeverity` — enough to interpret a
finding's `code` and `severity`. Full rule metadata (summary, rationale,
documentation, known limits, evidence class, tags) is served once by the
`lintRules` agent operation rather than inlined into every `lint` result
(#431):

```json
{
Expand All @@ -43,66 +48,12 @@ identity; the tool/agent API exposes the same value as `inputIdentity`),
"input_identity": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"program": { "origin": { "kind": "workshop", "locale": "en-us" }, "rules": 2, "findings": 1 },
"rules": [
{
"id": "min-wait-loop",
"defaultSeverity": "warning",
"effectiveSeverity": "warning",
"enabled": true,
"summary": "loop body waits at the workshop minimum rate",
"evidence": "static-indicator",
"tags": ["performance", "stability"],
"knownLimits": "Wait durations that are not statically known ..."
},
{
"id": "duplicate-condition",
"defaultSeverity": "warning",
"effectiveSeverity": "warning",
"enabled": true,
"summary": "condition is evaluated more than once within one rule",
"evidence": "exact",
"tags": ["correctness"],
"knownLimits": "Detection is structural (not value-flow) and rule-local ..."
},
{
"id": "expensive-loop-check",
"defaultSeverity": "info",
"effectiveSeverity": "info",
"enabled": true,
"summary": "geometry predicate evaluated inside a loop body",
"evidence": "heuristic",
"tags": ["performance"],
"knownLimits": "The expensive-call list is a fixed heuristic ..."
},
{
"id": "ongoing-condition-hot-path",
"defaultSeverity": "info",
"effectiveSeverity": "info",
"enabled": true,
"summary": "geometry predicate evaluated in an ongoing-rule condition",
"evidence": "heuristic",
"tags": ["performance", "stability"],
"knownLimits": "The geometry-predicate list is a fixed heuristic; the analysis does not measure runtime cost or infer selectivity ..."
},
{
"id": "repeated-value",
"defaultSeverity": "warning",
"effectiveSeverity": "warning",
"enabled": true,
"summary": "identical value expression evaluated more than once in one loop scope",
"evidence": "exact",
"tags": ["performance", "stability"],
"knownLimits": "Detection is rule-local and structural ..."
},
{
"id": "while-without-wait",
"defaultSeverity": "warning",
"effectiveSeverity": "warning",
"enabled": true,
"summary": "while loop body contains no wait call",
"evidence": "static-indicator",
"tags": ["stability"],
"knownLimits": "Counter-pattern detection is conservative and structural: only literal-bound comparisons (<, <=, >, >=) are recognized. A statically-bounded claim additionally requires every direct child ..."
}
{ "id": "min-wait-loop", "effectiveSeverity": "warning" },
{ "id": "duplicate-condition", "effectiveSeverity": "warning" },
{ "id": "expensive-loop-check", "effectiveSeverity": "info" },
{ "id": "ongoing-condition-hot-path", "effectiveSeverity": "info" },
{ "id": "repeated-value", "effectiveSeverity": "warning" },
{ "id": "while-without-wait", "effectiveSeverity": "warning" }
],
"config": { "rules": { "min-wait-loop": { "enabled": true, "severity": "warning" } } },
"findings": [
Expand All @@ -113,7 +64,8 @@ identity; the tool/agent API exposes the same value as `inputIdentity`),
"message": "loop body waits at the workshop minimum rate; ...",
"span": { "file": 0, "path": "program.txt", "start": { "line": 28, "col": 9 }, "end": { "line": 31, "col": 13 } }
}
]
],
"skipped": []
}
}
```
Expand All @@ -125,8 +77,8 @@ The core workflows have separate contracts:
findings such as `duplicate-condition` and `min-wait-loop` are not emitted
by default.
* `lint` executes the configurable `LintRegistry` and returns stable rule IDs,
severity, evidence class, boundedness where applicable, source spans, rule
metadata, and effective configuration.
severity, evidence class, boundedness where applicable, source spans,
a per-rule id/effective-severity list, and effective configuration.
* `analyze` returns semantic facts rather than lint findings. Human text output
is a bounded report with a program overview, aggregate CFG measurements,
ranked rule hotspots, and ranked cross-cutting variables. The displayed
Expand All @@ -136,7 +88,7 @@ The core workflows have separate contracts:
exhaustive structural/semantic view. These facts can inform future lint
rules without making analysis a view of the registry.

Analysis findings (`lint` and the tool/agent `getFindings`/`lint` responses)
Analysis findings (`lint` and the tool/agent `findings`/`lint` responses)
carry an `evidence` field classifying how strongly the finding is supported
(`exact`, `static-indicator`, `heuristic`, `runtime-validated`).

Expand Down
Loading
Loading