You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(context): primary change detection, metadata breaking, symbol tri-state
- PRIMARY_CHANGE: line in prompt anchors subject to most significant
change (ranked: new public API > removed > largest file)
- Metadata-aware breaking detection: scans diffs for MSRV changes,
engines.node tightening, requires-python tightening, removed exports
- Symbol tri-state: AddedOnly/RemovedOnly/ModifiedSignature — public
modified symbols contribute to breaking risk
- Focus instruction for groups >5 files
- String::with_capacity in truncate_diff_adaptive
- PromptContext gains file_count, primary_change, group_rationale,
metadata_breaking_signals, symbols_modified fields
Copy file name to clipboardExpand all lines: src/domain/context.rs
+147-5Lines changed: 147 additions & 5 deletions
Original file line number
Diff line number
Diff line change
@@ -10,32 +10,92 @@ pub struct PromptContext {
10
10
pubfile_breakdown:String,
11
11
pubsymbols_added:String,
12
12
pubsymbols_removed:String,
13
+
pubsymbols_modified:String,
14
+
pubpublic_api_removed:String,
13
15
pubsuggested_type:CommitType,
14
16
pubsuggested_scope:Option<String>,
15
17
pubtruncated_diff:String,
18
+
// Evidence flags for constraint-based anti-hallucination
19
+
pubis_mechanical:bool,
20
+
pubhas_bug_evidence:bool,
21
+
pubpublic_api_removed_count:usize,
22
+
pubhas_new_public_api:bool,
23
+
pubis_dependency_only:bool,
24
+
/// Number of files in this group (for focus instruction on large groups)
25
+
pubfile_count:usize,
26
+
/// Most significant change for subject anchoring (e.g., "added CommitValidator struct")
27
+
pubprimary_change:Option<String>,
28
+
/// Short rationale for why these files were grouped together
29
+
pubgroup_rationale:Option<String>,
30
+
/// Metadata-level breaking signals detected from diff content
31
+
pubmetadata_breaking_signals:Vec<String>,
16
32
}
17
33
18
34
implPromptContext{
19
35
pubfnto_prompt(&self) -> String{
20
36
let symbols_section = self.format_symbols_section();
37
+
let breaking_warning = self.format_breaking_warning();
38
+
let evidence_section = self.format_evidence_section();
39
+
let constraints_section = self.format_constraints_section();
40
+
41
+
// Calculate available chars for subject after type(scope): prefix
42
+
let prefix_len = self.suggested_type.as_str().len()
43
+
+ self
44
+
.suggested_scope
45
+
.as_ref()
46
+
.map(|s| s.len() + 2)
47
+
.unwrap_or(0)// "(scope)"
48
+
+ 2;// ": "
49
+
let subject_budget = 72_usize.saturating_sub(prefix_len);
50
+
51
+
let focus_instruction = ifself.file_count > 5{
52
+
"\nFOCUS: This group contains many files. Focus the subject on the single most significant change. Do not try to describe every change — pick the primary one.\n"
53
+
}else{
54
+
""
55
+
};
56
+
57
+
let primary_change_line = self
58
+
.primary_change
59
+
.as_ref()
60
+
.map(|pc| format!("\nPRIMARY_CHANGE: {}\n", pc))
61
+
.unwrap_or_default();
62
+
63
+
let group_rationale_line = self
64
+
.group_rationale
65
+
.as_ref()
66
+
.map(|gr| format!("GROUP_REASON: {}\n", gr))
67
+
.unwrap_or_default();
68
+
69
+
let metadata_breaking_section = ifself.metadata_breaking_signals.is_empty(){
70
+
String::new()
71
+
}else{
72
+
format!(
73
+
"\n⚠ METADATA BREAKING CHANGES DETECTED:\n{}\n",
74
+
self.metadata_breaking_signals
75
+
.iter()
76
+
.map(|s| format!("- {}", s))
77
+
.collect::<Vec<_>>()
78
+
.join("\n")
79
+
)
80
+
};
21
81
22
82
format!(
23
83
r#"Analyze this git diff and generate a commit message.
Write a JSON commit message describing the changes shown in the diff.
33
-
The subject must be specific - describe WHAT was changed (e.g., "add system prompt to ollama provider", "update dependency versions").
93
+
The subject must be specific and under {subject_budget} chars — describe WHAT was changed (e.g., "add system prompt to ollama provider", "update dependency versions").
34
94
For the body: if the change is trivial (single rename, typo fix), use null. Otherwise write a short body (1-3 sentences) explaining WHY the change was made or what it enables.
35
95
For breaking_change: only set this if existing users or dependents must change their code, config, or scripts to keep working — e.g., a public function/endpoint removed or renamed, a required parameter or field added, a config key changed. New optional features, bug fixes, and internal refactors are NOT breaking. Default to null.
36
96
37
97
Output format:
38
-
{{"type": "{commit_type}", "scope": {scope_json}, "subject": "<imperative verb + what changed>", "body": "<why this change was made, or null if trivial>", "breaking_change": null}}"#,
98
+
{{"type": "<type>", "scope": {scope_json}, "subject": "<imperative verb + what changed>", "body": "<why this change was made, or null if trivial>", "breaking_change": null}}"#,
39
99
summary = self.change_summary,
40
100
files = self.file_breakdown.trim(),
41
101
commit_type = self.suggested_type.as_str(),
@@ -44,7 +104,15 @@ Output format:
44
104
.as_ref()
45
105
.map(|s| format!("\nSCOPE: {}", s))
46
106
.unwrap_or_default(),
107
+
evidence = evidence_section,
47
108
symbols = symbols_section,
109
+
breaking = breaking_warning,
110
+
constraints = constraints_section,
111
+
focus = focus_instruction,
112
+
primary_change = primary_change_line,
113
+
group_rationale = group_rationale_line,
114
+
metadata_breaking = metadata_breaking_section,
115
+
subject_budget = subject_budget,
48
116
scope_json = self
49
117
.suggested_scope
50
118
.as_ref()
@@ -57,8 +125,9 @@ Output format:
57
125
fnformat_symbols_section(&self) -> String{
58
126
let has_added = !self.symbols_added.is_empty();
59
127
let has_removed = !self.symbols_removed.is_empty();
128
+
let has_modified = !self.symbols_modified.is_empty();
60
129
61
-
if !has_added && !has_removed {
130
+
if !has_added && !has_removed && !has_modified {
62
131
returnString::new();
63
132
}
64
133
@@ -75,7 +144,80 @@ Output format:
75
144
self.symbols_removed.replace('\n',"\n ")
76
145
));
77
146
}
147
+
if has_modified {
148
+
section.push_str(&format!(
149
+
"\n Modified (signature changed):\n {}",
150
+
self.symbols_modified.replace('\n',"\n ")
151
+
));
152
+
}
78
153
section.push('\n');
79
154
section
80
155
}
156
+
157
+
fnformat_breaking_warning(&self) -> String{
158
+
ifself.public_api_removed.is_empty(){
159
+
returnString::new();
160
+
}
161
+
162
+
format!(
163
+
"\n⚠ PUBLIC API REMOVED — describe this in breaking_change field:\n {}\n",
164
+
self.public_api_removed.replace('\n',"\n ")
165
+
)
166
+
}
167
+
168
+
fnformat_evidence_section(&self) -> String{
169
+
let yn = |b:bool| if b {"yes"}else{"no"};
170
+
171
+
format!(
172
+
"\nEVIDENCE:\n\
173
+
- Is this a mechanical/formatting change? {}\n\
174
+
- Does the diff contain bug-fix comments? {}\n\
175
+
- How many public APIs were removed? {}\n\
176
+
- Were new public APIs added? {}\n\
177
+
- Are all changes in dependency/config files? {}\n",
178
+
yn(self.is_mechanical),
179
+
yn(self.has_bug_evidence),
180
+
self.public_api_removed_count,
181
+
yn(self.has_new_public_api),
182
+
yn(self.is_dependency_only),
183
+
)
184
+
}
185
+
186
+
fnformat_constraints_section(&self) -> String{
187
+
letmut rules = Vec::new();
188
+
189
+
if !self.has_bug_evidence{
190
+
rules.push(
191
+
"- No bug-fix comments found: prefer \"refactor\" over \"fix\". \
192
+
Only use \"fix\" if the diff clearly corrects wrong behavior.",
193
+
);
194
+
}
195
+
ifself.is_mechanical{
196
+
rules.push(
197
+
"- Mechanical/formatting change detected: use \"style\" or \"refactor\", not \"feat\" or \"fix\".",
198
+
);
199
+
}
200
+
ifself.public_api_removed_count > 0{
201
+
rules.push(
202
+
"- Public APIs were removed: set breaking_change to describe what was removed \
203
+
(e.g., \"removed `old_fn()`, use `new_fn()` instead\"). \
204
+
Never copy labels from this prompt as the description.",
205
+
);
206
+
}
207
+
ifself.is_dependency_only{
208
+
rules.push("- All changes are in dependency/config files: use \"chore\".");
0 commit comments