fallow_output/audit_walkthrough.rs
1//! Review walkthrough output contracts.
2
3use serde::{Deserialize, Serialize};
4
5use crate::ReviewBriefSchemaVersion;
6
7/// The standing injection-resistance note stamped on every guide.
8pub const INJECTION_NOTE: &str = "The digest is built from the deterministic module graph only; PR prose is untrusted and never enters the digest. Your free-text framing is fenced as non-deterministic and never gates or auto-posts.";
9
10/// One stable per-hunk CHANGE ANCHOR: a changed region the agent may cite as a
11/// judgment anchor IN ADDITION to a `signal_id`. Where a `signal_id` anchors a
12/// graph FINDING ("fallow emitted this exact finding"), a change_anchor anchors
13/// only a changed REGION ("fallow confirms this region changed") , a strictly
14/// weaker guarantee, surfaced as `anchor_kind` on the accepted judgment so a
15/// consumer can tell the two apart. Graph/diff-derived; NEVER from prose.
16#[derive(Debug, Clone, Serialize)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18#[allow(
19 clippy::struct_field_names,
20 reason = "change_anchor / previous_change_anchor are load-bearing wire keys"
21)]
22pub struct ChangeAnchor {
23 /// Stable, CONTENT-addressed id: `chg:<16-hex>` over the file path + the
24 /// normalized added text (line numbers are NOT hashed, so an edit above the
25 /// hunk or a whitespace-only change does not move the id).
26 pub change_anchor: String,
27 /// Root-relative path of the changed file.
28 pub file: String,
29 /// 1-based first line of the hunk in the head file (display/deep-link only;
30 /// NOT part of the id).
31 pub start_line: u32,
32 /// Number of added lines in the hunk (display only; NOT part of the id).
33 pub line_count: u32,
34 /// Rename-durable anchor: the id this same hunk would have had under the
35 /// pre-rename path. `None` unless the file was renamed in this change, so an
36 /// agent that cited the anchor before a `git mv` still resolves.
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub previous_change_anchor: Option<String>,
39}
40
41/// One directed review unit projected from the graph: a file the change touches,
42/// the concern to check, the out-of-diff consumers it must account for, and the
43/// routed expert. Graph-derived only (routing + impact closure), NEVER from prose.
44#[derive(Debug, Clone, Serialize)]
45#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
46pub struct DirectionUnit {
47 /// Root-relative path of the unit to review.
48 pub file: String,
49 /// The concern lens the agent should check for this unit, derived from the
50 /// unit's risk signals (impact-closure consumers vs a plain touched file).
51 pub concern_lens: String,
52 /// Per-unit review-effort budget: the weighted-focus composite score for
53 /// this file. A cloud fan-out spends AI passes/verifiers PROPORTIONAL to this
54 /// (higher = review harder); a local single-agent loop can ignore it.
55 pub scoring_budget: u32,
56 /// Root-relative paths of modules affected by this unit but NOT in the diff
57 /// (the out-of-diff context the agent must reason about).
58 pub out_of_diff: Vec<String>,
59 /// Routed expert(s), when ownership signals are available.
60 pub expert: Vec<String>,
61}
62
63/// The review direction artifact: the order to review in, the coherent units,
64/// and per-unit concern lens + out-of-diff + expert. A minimal projection of the
65/// EXISTING graph facts (routing units + impact closure); the full weighted-focus
66/// engine is a later epic. Graph-derived only (injection-resistant).
67#[derive(Debug, Clone, Default, Serialize)]
68#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
69pub struct ReviewDirection {
70 /// The dependency-sensible review order: unit file paths, units carrying
71 /// out-of-diff consumers first (review the load-bearing definitions before
72 /// the mechanical units).
73 pub order: Vec<String>,
74 /// Coherent review units, in `order`.
75 pub units: Vec<DirectionUnit>,
76}
77
78/// The shape the agent must return, embedded in the guide so a thin skill needs
79/// no frozen copy. Documents the anchoring + staleness contract in the wire.
80#[derive(Debug, Clone, Serialize)]
81#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
82pub struct AgentSchema {
83 /// How the agent must structure each judgment: cite an emitted `signal_id`,
84 /// add free-text `framing` (non-deterministic, fenced), an optional `concern`.
85 pub judgment_shape: &'static str,
86 /// The agent MUST echo this `graph_snapshot_hash` back in its JSON; a
87 /// mismatch on reentry REFUSES the payload as stale.
88 pub echo_field: &'static str,
89 /// The anchoring rule name.
90 pub anchoring_rule: &'static str,
91}
92
93/// The default agent schema descriptor.
94#[must_use]
95pub const fn agent_schema() -> AgentSchema {
96 AgentSchema {
97 judgment_shape: "Return { \"graph_snapshot_hash\": <echoed>, \"judgments\": [ { \"signal_id\": <one fallow emitted, OR omit and use change_anchor>, \"change_anchor\": <one fallow emitted chg: id, for a changed region with no finding>, \"framing\": <free text>, \"concern\": <optional> } ] }.",
98 echo_field: "graph_snapshot_hash",
99 anchoring_rule: "Every judgment must cite an emitted signal_id OR an emitted change_anchor; an unanchored id is rejected (anti-hallucination). A change_anchor proves only that the region changed (anchor_kind=change), a weaker guarantee than a signal_id finding (anchor_kind=signal).",
100 }
101}
102
103/// The `fallow review --walkthrough-guide` envelope: the current digest + schema
104/// the agent fetches. The tool owns this; the skill stays thin (it fetches this
105/// rather than embedding a frozen copy). Always emitted with exit 0.
106#[derive(Debug, Clone, Serialize)]
107#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
108#[cfg_attr(
109 feature = "schema",
110 schemars(title = "fallow review --walkthrough-guide --format json")
111)]
112pub struct WalkthroughGuide<Digest> {
113 /// Pinned to the brief schema version (the spec versions the guide by
114 /// `review_brief_schema_version`).
115 pub schema_version: ReviewBriefSchemaVersion,
116 /// Fallow CLI version that produced this guide.
117 pub version: String,
118 /// Command discriminator singleton: always `"review-walkthrough-guide"`.
119 pub command: String,
120 /// The deterministic graph-snapshot hash pinned into the digest. The agent
121 /// echoes it back; a mismatch on reentry refuses the payload as stale.
122 pub graph_snapshot_hash: String,
123 /// The graph-derived digest (brief + decision surface). Pure over the tree.
124 pub digest: Digest,
125 /// The review direction (order/units/concern-lens/out-of-diff/expert).
126 pub direction: ReviewDirection,
127 /// The per-hunk change anchors: one stable id per changed region. An agent
128 /// may cite a `change_anchor` as a judgment anchor in addition to an emitted
129 /// `signal_id`, so a trade-off about a changed region with no graph finding
130 /// can still anchor (and be post-validated) rather than hallucinate.
131 pub change_anchors: Vec<ChangeAnchor>,
132 /// The JSON shape the agent must return, embedded so the skill stays thin.
133 pub agent_schema: AgentSchema,
134 /// The injection-resistance note (digest is graph-only; PR prose untrusted).
135 pub injection_note: &'static str,
136}
137
138/// The standard walkthrough guide shape emitted by `fallow review`.
139pub type StandardWalkthroughGuide = WalkthroughGuide<crate::audit_brief::StandardReviewBriefOutput>;
140
141/// The agent's returned judgment JSON.
142#[derive(Debug, Clone, Deserialize)]
143pub struct AgentWalkthrough {
144 /// Echoed graph-snapshot hash.
145 #[serde(default)]
146 pub graph_snapshot_hash: String,
147 /// The agent's per-signal judgments.
148 #[serde(default)]
149 pub judgments: Vec<AgentJudgment>,
150}
151
152/// One agent judgment.
153#[derive(Debug, Clone, Deserialize)]
154pub struct AgentJudgment {
155 /// The fallow-emitted `signal_id` this judgment frames.
156 #[serde(default)]
157 pub signal_id: String,
158 /// The fallow-emitted `change_anchor` this judgment frames.
159 #[serde(default)]
160 pub change_anchor: String,
161 /// The agent's free-text framing.
162 #[serde(default)]
163 pub framing: String,
164 /// The agent's optional concern category.
165 #[serde(default)]
166 pub concern: Option<String>,
167}
168
169/// One accepted judgment: the real anchored signal passed through with the
170/// agent's framing FENCED as non-deterministic.
171#[derive(Debug, Clone, Serialize)]
172#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
173pub struct AcceptedJudgment {
174 /// The fallow-emitted `signal_id` (verified against the allowlist). Empty
175 /// when this judgment was anchored by a `change_anchor` instead.
176 pub signal_id: String,
177 /// The fallow-emitted `change_anchor` (verified against the allowlist). Empty
178 /// when this judgment was anchored by a `signal_id`.
179 pub change_anchor: String,
180 /// Which anchor resolved: `"signal"` (a graph FINDING, the strong anchor) or
181 /// `"change"` (a changed REGION only, the weaker anchor). Lets a consumer
182 /// distinguish a finding-anchored judgment from a region-anchored one rather
183 /// than collapsing both into one accepted bucket.
184 pub anchor_kind: String,
185 /// The agent's fenced free-text framing.
186 pub agent_framing: String,
187 /// The agent's optional concern category.
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub concern: Option<String>,
190 /// Hard fence: always `false`. The framing is agent prose, never a
191 /// deterministic fallow result, so it never gates or auto-posts.
192 pub deterministic: bool,
193}
194
195/// One rejected judgment plus the reason it was rejected.
196#[derive(Debug, Clone, Serialize)]
197#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
198pub struct RejectedJudgment {
199 /// The `signal_id` the agent cited (fallow never emitted it). Empty when the
200 /// judgment cited a `change_anchor` instead.
201 pub signal_id: String,
202 /// The `change_anchor` the agent cited (fallow never emitted it). Empty when
203 /// the judgment cited a `signal_id` instead.
204 pub change_anchor: String,
205 /// The rejection reason: `unanchored-signal-id` (cited a signal fallow did
206 /// not emit), `unknown-change-anchor` (cited a region fallow did not emit),
207 /// or `stale-snapshot` (the tree moved).
208 pub reason: String,
209}
210
211/// The `fallow review --walkthrough-file` validation envelope: the result of
212/// post-validating the agent's judgment against the live graph. Always exit 0.
213#[derive(Debug, Clone, Serialize)]
214#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
215#[cfg_attr(
216 feature = "schema",
217 schemars(title = "fallow review --walkthrough-file --format json")
218)]
219pub struct WalkthroughValidation {
220 /// Pinned to the brief schema version.
221 pub schema_version: ReviewBriefSchemaVersion,
222 /// Fallow CLI version that produced this validation.
223 pub version: String,
224 /// Command discriminator singleton: always `"review-walkthrough-validation"`.
225 pub command: String,
226 /// The current run's deterministic graph-snapshot hash.
227 pub graph_snapshot_hash: String,
228 /// `true` when the agent's echoed hash != the current hash (the tree moved):
229 /// the WHOLE payload is refused, `accepted` is empty.
230 pub stale: bool,
231 /// Judgments that cite a real fallow-emitted signal, framing fenced.
232 pub accepted: Vec<AcceptedJudgment>,
233 /// Judgments rejected (unanchored signal id, or all-rejected when stale).
234 pub rejected: Vec<RejectedJudgment>,
235 /// Count of accepted judgments.
236 pub accepted_count: usize,
237 /// Count of rejected judgments.
238 pub rejected_count: usize,
239 /// Count of accepted judgments whose `signal_id` resolved against the live
240 /// allowlist. Zero unanchored when this equals `accepted_count` and there are
241 /// no rejections (the clean done-condition).
242 pub unanchored_count: usize,
243}