From 6f4d92ea95d6a05459cf3acc8e691a88eaf627a3 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:12:34 +0800 Subject: [PATCH 1/5] feat(driver): serve compact rule metadata in the lint result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wright lint and the agent lint operation inlined the full lintRules response — roughly 7.5 KB of static rule prose on every call, 78% of a clean result — while lintRules already serves the same metadata on demand. The lint result now keeps only each rule's id and effectiveSeverity, enough to interpret a finding's code and severity; summary, rationale, documentation, known limits, evidence, and tags stay exclusive to lintRules. This is a recorded exception to wright-result/v1, decided ahead of the 1.0 freeze; machine-contract.md now documents the envelope's evolution policy alongside the wright-agent/v1 rule. Closes #431 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- crates/wright-cli/tests/cli.rs | 17 +++-- crates/wright-driver/src/result.rs | 21 ++++++ crates/wright-driver/src/service.rs | 16 +++-- crates/wright-driver/src/session/semantic.rs | 10 +-- crates/wright-driver/tests/service.rs | 11 +++ docs/agent-contract.md | 7 +- docs/cli/commands.md | 2 +- docs/cli/lint.md | 76 ++++---------------- docs/cli/machine-contract.md | 14 ++++ docs/specs/SPEC-99-stability-rules.md | 5 +- schemas/wright-agent-v1.schema.json | 11 ++- 11 files changed, 103 insertions(+), 87 deletions(-) diff --git a/crates/wright-cli/tests/cli.rs b/crates/wright-cli/tests/cli.rs index c63b7b9..915a6ac 100644 --- a/crates/wright-cli/tests/cli.rs +++ b/crates/wright-cli/tests/cli.rs @@ -364,6 +364,13 @@ fn lint_over_workshop_input_reports_findings_in_text_and_json() { .len(), rules.len() ); + for rule in rules { + assert!(rule["effectiveSeverity"].is_string()); + assert!( + rule.get("summary").is_none() && rule.get("knownLimits").is_none(), + "lint inlines only id and effectiveSeverity; full metadata is lintRules (#431)" + ); + } let _ = std::fs::remove_dir_all(path.parent().unwrap()); } @@ -454,12 +461,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 diff --git a/crates/wright-driver/src/result.rs b/crates/wright-driver/src/result.rs index 227f326..3e0ff85 100644 --- a/crates/wright-driver/src/result.rs +++ b/crates/wright-driver/src/result.rs @@ -96,6 +96,27 @@ 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() + .map(|rule| { + serde_json::json!({ + "id": rule["id"], + "effectiveSeverity": rule["effectiveSeverity"], + }) + }) + .collect(), + ) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)] #[serde(rename_all = "lowercase")] pub enum ConvertTarget { diff --git a/crates/wright-driver/src/service.rs b/crates/wright-driver/src/service.rs index e1514ee..c2956eb 100644 --- a/crates/wright-driver/src/service.rs +++ b/crates/wright-driver/src/service.rs @@ -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, @@ -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) { @@ -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!([])), diff --git a/crates/wright-driver/src/session/semantic.rs b/crates/wright-driver/src/session/semantic.rs index 47a01d9..2f8797f 100644 --- a/crates/wright-driver/src/session/semantic.rs +++ b/crates/wright-driver/src/session/semantic.rs @@ -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. @@ -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!({})), diff --git a/crates/wright-driver/tests/service.rs b/crates/wright-driver/tests/service.rs index 7881cd7..d9e5f36 100644 --- a/crates/wright-driver/tests/service.rs +++ b/crates/wright-driver/tests/service.rs @@ -80,6 +80,17 @@ 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["effectiveSeverity"], "error"); + assert!(lint_rule.get("summary").is_none()); + assert!(lint_rules["rules"][0]["summary"].is_string()); let configured_finding = lint["findings"] .as_array() .unwrap() diff --git a/docs/agent-contract.md b/docs/agent-contract.md index cf9633e..f8a2050 100644 --- a/docs/agent-contract.md +++ b/docs/agent-contract.md @@ -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 | @@ -126,7 +126,8 @@ 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). The optional guide distributed by `wrightkit/skills` teaches clients to discover and use these capabilities. It is not required to expose, execute, or diff --git a/docs/cli/commands.md b/docs/cli/commands.md index 97730e4..9dbafed 100644 --- a/docs/cli/commands.md +++ b/docs/cli/commands.md @@ -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 ` | Generate static completion script for bash, zsh, fish, or powershell | the generated completion script | diff --git a/docs/cli/lint.md b/docs/cli/lint.md index 77b660d..13c8531 100644 --- a/docs/cli/lint.md +++ b/docs/cli/lint.md @@ -30,7 +30,11 @@ 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`, and `findings`. `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 { @@ -43,66 +47,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": [ @@ -125,8 +75,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 diff --git a/docs/cli/machine-contract.md b/docs/cli/machine-contract.md index 49ef257..aa1cc1d 100644 --- a/docs/cli/machine-contract.md +++ b/docs/cli/machine-contract.md @@ -76,6 +76,20 @@ source-located). Named/keyword argument binding adds `unknown-keyword`, `keyword-required`, `keyword-unsupported`, and `invalid-argument` (variable-required parameters; #110). +## `wright-result/v1` evolution + +The envelope follows the same rule as `wright-agent/v1`: additive optional +`result` fields are permitted within v1; removing or renaming a field, +changing a field's type or meaning, or changing the envelope or exit-code +model requires a new major contract such as `wright-result/v2`. + +One recorded exception applies, 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 the +`lintRules` agent operation instead of being inlined into every `lint` +result. + ## Determinism For identical inputs and configuration, JSON output is byte-deterministic diff --git a/docs/specs/SPEC-99-stability-rules.md b/docs/specs/SPEC-99-stability-rules.md index 6401476..d4bd4b2 100644 --- a/docs/specs/SPEC-99-stability-rules.md +++ b/docs/specs/SPEC-99-stability-rules.md @@ -265,8 +265,9 @@ indicator). No new `heuristic`-class rules are added in this set. plugin loading remains out of scope. - **#98 lint surface** ([`../cli.md`](../cli.md) "`wright lint` and the lint configuration"; `CompilerSession::lint`; `ToolRequest::Lint`/`LintRules`): structured findings - with `evidence`, rule metadata in the result envelope, deterministic config - across CLI and tool/agent paths. Constraint: new rules surface through the + with `evidence`, per-rule id/effective severity in the result envelope (#431; + full metadata via `lintRules`), deterministic config across CLI and + tool/agent paths. Constraint: new rules surface through the existing path with no new protocol surface. - **EvidenceClass contract** (`../../crates/wright-analyzer/src/analysis.rs`): `exact` / `static-indicator` / `heuristic` / `runtime-validated` (reserved). diff --git a/schemas/wright-agent-v1.schema.json b/schemas/wright-agent-v1.schema.json index 04c2149..70540b4 100644 --- a/schemas/wright-agent-v1.schema.json +++ b/schemas/wright-agent-v1.schema.json @@ -247,6 +247,15 @@ }, "additionalProperties": true }, + "LintRuleSummary": { + "type": "object", + "required": ["id", "effectiveSeverity"], + "properties": { + "id": { "type": "string" }, + "effectiveSeverity": { "type": "string" } + }, + "additionalProperties": true + }, "LintConfiguration": { "type": "object", "required": ["rules"], @@ -436,7 +445,7 @@ "required": ["inputIdentity", "rules", "config", "findings", "skipped"], "properties": { "inputIdentity": { "type": "string" }, - "rules": { "type": "array", "items": { "$ref": "#/$defs/LintRule" } }, + "rules": { "type": "array", "items": { "$ref": "#/$defs/LintRuleSummary" } }, "config": { "$ref": "#/$defs/LintConfiguration" }, "findings": { "type": "array", "items": { "$ref": "#/$defs/Finding" } }, "skipped": { "type": "array", "items": { "$ref": "#/$defs/SkippedRule" } } From 8750fb7be29e39e9272f05699fa1a477052a6ae5 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:42:51 +0800 Subject: [PATCH 2/5] test(driver): pin the exact compact lint rule shape Assert compact lint rules carry exactly id and effectiveSeverity, and look up lintRules metadata by id instead of array position. Refs #431 --- crates/wright-cli/tests/cli.rs | 9 +++++---- crates/wright-driver/tests/service.rs | 14 +++++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/wright-cli/tests/cli.rs b/crates/wright-cli/tests/cli.rs index 915a6ac..9aed56b 100644 --- a/crates/wright-cli/tests/cli.rs +++ b/crates/wright-cli/tests/cli.rs @@ -365,10 +365,11 @@ fn lint_over_workshop_input_reports_findings_in_text_and_json() { rules.len() ); for rule in rules { - assert!(rule["effectiveSeverity"].is_string()); - assert!( - rule.get("summary").is_none() && rule.get("knownLimits").is_none(), - "lint inlines only id and effectiveSeverity; full metadata is lintRules (#431)" + 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()); diff --git a/crates/wright-driver/tests/service.rs b/crates/wright-driver/tests/service.rs index d9e5f36..16a89d3 100644 --- a/crates/wright-driver/tests/service.rs +++ b/crates/wright-driver/tests/service.rs @@ -88,9 +88,17 @@ fn tool_service_lint_queries_keep_the_session_configuration() { .iter() .find(|rule| rule["id"] == "min-wait-loop") .expect("lint lists every registered rule"); - assert_eq!(lint_rule["effectiveSeverity"], "error"); - assert!(lint_rule.get("summary").is_none()); - assert!(lint_rules["rules"][0]["summary"].is_string()); + 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() From b3251bcebd9a8056b8286c25a0c6964d27bba77f Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:44:30 +0800 Subject: [PATCH 3/5] fix(driver): align the agent lint payload and harden compact rules The agent lint result now carries the same program summary as CompilerSession::lint, and compact_lint_rules skips malformed entries instead of emitting null id/effectiveSeverity fields. Schema, contract docs, and the service test updated to match. Refs #431 --- crates/wright-driver/src/result.rs | 12 +++++++----- crates/wright-driver/src/service.rs | 16 +++++++++++----- crates/wright-driver/tests/service.rs | 4 ++++ docs/agent-contract.md | 2 +- schemas/wright-agent-v1.schema.json | 3 ++- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/wright-driver/src/result.rs b/crates/wright-driver/src/result.rs index 3e0ff85..870e304 100644 --- a/crates/wright-driver/src/result.rs +++ b/crates/wright-driver/src/result.rs @@ -107,11 +107,13 @@ pub(crate) fn compact_lint_rules(rules: Option<&serde_json::Value>) -> serde_jso serde_json::Value::Array( rules .iter() - .map(|rule| { - serde_json::json!({ - "id": rule["id"], - "effectiveSeverity": rule["effectiveSeverity"], - }) + .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(), ) diff --git a/crates/wright-driver/src/service.rs b/crates/wright-driver/src/service.rs index c2956eb..a54bb4c 100644 --- a/crates/wright-driver/src/service.rs +++ b/crates/wright-driver/src/service.rs @@ -61,8 +61,9 @@ pub enum ToolRequest { Findings, /// Persistent Workshop object facts, separate from lint diagnostics. PersistentObjects, - /// Lint findings plus per-rule id/effective severity and the effective - /// configuration (#98); `lintRules` serves full rule metadata (#431). + /// Lint findings plus the program summary, per-rule id/effective severity, + /// and the effective configuration (#98); `lintRules` serves full rule + /// metadata (#431). Lint, /// The registered lint rules with full metadata and the effective lint /// configuration. @@ -441,9 +442,9 @@ impl<'a> ToolService<'a> { self.lint_semantic.as_ref().unwrap_or(&self.semantic) } - /// `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). + /// `lint`: program summary, 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 (#98). /// Full rule metadata is served once by `lintRules` rather than inlined /// into every `lint` response (#431). fn lint(&self) -> ToolResponse { @@ -452,6 +453,10 @@ impl<'a> ToolService<'a> { Response::Ok { result } => result, Response::Error { .. } => serde_json::json!({}), }; + let program = match service.handle(&Request::Program) { + Response::Ok { result } => result, + Response::Error { .. } => serde_json::Value::Null, + }; let mut findings = match service.handle(&Request::GetFindings) { Response::Ok { result } => result, Response::Error { .. } => serde_json::json!([]), @@ -459,6 +464,7 @@ impl<'a> ToolService<'a> { crate::session::resolve_span_paths(&mut findings, &self.loaded); self.ok(json!({ "inputIdentity": self.loaded.input.identity, + "program": program, "rules": crate::result::compact_lint_rules(lint_rules.get("rules")), "config": lint_rules.get("config").cloned().unwrap_or_else(|| json!({})), "findings": findings, diff --git a/crates/wright-driver/tests/service.rs b/crates/wright-driver/tests/service.rs index 16a89d3..9466c70 100644 --- a/crates/wright-driver/tests/service.rs +++ b/crates/wright-driver/tests/service.rs @@ -80,6 +80,10 @@ fn tool_service_lint_queries_keep_the_session_configuration() { ToolResponse::Error { error } => panic!("lint failed: {error:?}"), }; assert_eq!(lint["config"], lint_rules["config"]); + assert!( + lint["program"].is_object(), + "lint carries the program summary" + ); // #431: `lint` inlines only the finding-interpretation fields; // `lintRules` remains the full-metadata surface. let lint_rule = lint["rules"] diff --git a/docs/agent-contract.md b/docs/agent-contract.md index f8a2050..2b00180 100644 --- a/docs/agent-contract.md +++ b/docs/agent-contract.md @@ -82,7 +82,7 @@ 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, per-rule id/effective severity, and effective configuration | +| `lint` | none | Lint findings, program summary, 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 | diff --git a/schemas/wright-agent-v1.schema.json b/schemas/wright-agent-v1.schema.json index 70540b4..8d1742d 100644 --- a/schemas/wright-agent-v1.schema.json +++ b/schemas/wright-agent-v1.schema.json @@ -442,9 +442,10 @@ }, "LintResult": { "type": "object", - "required": ["inputIdentity", "rules", "config", "findings", "skipped"], + "required": ["inputIdentity", "program", "rules", "config", "findings", "skipped"], "properties": { "inputIdentity": { "type": "string" }, + "program": { "anyOf": [{ "type": "null" }, { "$ref": "#/$defs/ProgramSummary" }] }, "rules": { "type": "array", "items": { "$ref": "#/$defs/LintRuleSummary" } }, "config": { "$ref": "#/$defs/LintConfiguration" }, "findings": { "type": "array", "items": { "$ref": "#/$defs/Finding" } }, From 60e438a8072601a619696f6ab032cdb778daea71 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:39:06 +0800 Subject: [PATCH 4/5] test(driver): cover compact_lint_rules edge cases Pin the compacting projection against non-array input and non-object or non-string entries so malformed analyzer output can never produce schema-invalid lint rules. Refs #431 --- crates/wright-driver/src/result.rs | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/crates/wright-driver/src/result.rs b/crates/wright-driver/src/result.rs index 870e304..af49b3a 100644 --- a/crates/wright-driver/src/result.rs +++ b/crates/wright-driver/src/result.rs @@ -157,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!([])); + } +} From 003a151702f09cfe8e0b38f3fce1d71682f52614 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:39:07 +0800 Subject: [PATCH 5/5] docs: record the lint rules exception on both contracts The agent contract's versioning policy now names the same pre-freeze exception as machine-contract.md, SPEC-99 REQ-006 no longer points at the compacted lint rules entries for rule documentation, and lint.md lists the skipped member and the findings operation by their contract names. Refs #431 --- docs/agent-contract.md | 6 ++++++ docs/cli/lint.md | 16 +++++++++------- docs/specs/SPEC-99-stability-rules.md | 3 ++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/agent-contract.md b/docs/agent-contract.md index 2b00180..46df0cf 100644 --- a/docs/agent-contract.md +++ b/docs/agent-contract.md @@ -129,6 +129,12 @@ the response/error model requires a new major contract such as 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 validate any semantic operation. diff --git a/docs/cli/lint.md b/docs/cli/lint.md index 13c8531..a9e9845 100644 --- a/docs/cli/lint.md +++ b/docs/cli/lint.md @@ -30,11 +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`. `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): +`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 { @@ -63,7 +64,8 @@ operation rather than inlined into every `lint` result (#431): "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": [] } } ``` @@ -86,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`). diff --git a/docs/specs/SPEC-99-stability-rules.md b/docs/specs/SPEC-99-stability-rules.md index d4bd4b2..145dd37 100644 --- a/docs/specs/SPEC-99-stability-rules.md +++ b/docs/specs/SPEC-99-stability-rules.md @@ -194,7 +194,8 @@ indicator). No new `heuristic`-class rules are added in this set. - **REQ-006** [documentation and evidence labeling]: Each new rule's `documentation` and `known_limits` (rendered through the #98 lint surface: - the `rules` envelope entries and the tool/agent `lintRules` response) state + the tool/agent `lintRules` response; `lint`'s `rules` entries carry only + `id`/`effectiveSeverity` after #431) state the rationale tied to the evidence case, the evidence classification, and the known limitations: loop coverage is `While` + `For Global Variable` only (the loop analysis does not model `For Player Variable` loops); `repeated-value`