1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
//! Per-section status of the composed PM prompt (#8533).
//!
//! Why: `tm sessions instructions` logged "applying ... section=Identity" while
//! the package `## Identity` still opened the prompt, so the report and the
//! outcome disagreed. The operator needs one table that says, per section,
//! whether it is safety core, overridable, or overridden by the project — and
//! that table must be checked against the prompt actually composed.
//! What: [`section_statuses`] derives each section's state from the same scan
//! and the same [`InstructionPackage::with_overrides`] call the composer runs;
//! [`render_section_report`] prints it and flags any overridden section whose
//! text is not in the composed prompt.
//! Test: `instruction_section_report_tests.rs`.
use std::path::{Path, PathBuf};
use crate::core::claude_md_sections::{Rejection, scan_project, section_token};
use crate::core::instruction_package::{InstructionPackage, SectionId};
use crate::core::instruction_safety_core::{
is_fixed_core_section, missing_core_members, pinned_members_of,
};
/// What is in force for one section.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SectionState {
/// A safety-core section: the bundled text, which no override replaces.
Core,
/// A replaceable section carrying the bundled text.
Overridable,
/// The project's override text replaces the bundled text.
OverriddenByProject,
/// A safety-core section whose project override was declined; the reason.
CoreDeclined(String),
/// A replaceable section whose override was not applied; the reason.
OverridableDeclined(String),
}
impl SectionState {
/// The state as the report prints it.
pub fn label(&self) -> String {
match self {
SectionState::Core => "core".to_string(),
SectionState::Overridable => "overridable".to_string(),
SectionState::OverriddenByProject => "overridden-by-project".to_string(),
SectionState::CoreDeclined(reason) => {
format!("core (project override declined: {reason})")
}
SectionState::OverridableDeclined(reason) => {
format!("overridable (project override declined: {reason})")
}
}
}
}
/// One row of the section report.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SectionStatus {
/// The section.
pub section: SectionId,
/// Its `CLAUDE.md` marker token.
pub token: &'static str,
/// What is in force.
pub state: SectionState,
/// Where the project's marker block sits, when it declared one.
pub source: Option<(PathBuf, usize)>,
/// First content line of the override body, used to check the prompt.
pub probe: Option<String>,
/// Pinned safety-core members that stay in force under the override.
pub kept: Vec<&'static str>,
}
/// The section a rejection names; `None` for a whole-package revert.
fn rejected_section(rejection: &Rejection) -> Option<SectionId> {
match rejection {
Rejection::NotOverridable { section, .. }
| Rejection::UnknownSection { section }
| Rejection::NoTextBlock { section }
| Rejection::EmptyBody { section }
| Rejection::WouldEmitNothing { section }
| Rejection::Invalidates { section, .. } => Some(*section),
Rejection::PackageInvalid(_) => None,
}
}
/// First line of `body` that is neither blank nor an HTML comment line.
fn probe_line(body: &str) -> Option<String> {
body.lines()
.map(str::trim)
.find(|l| !l.is_empty() && !l.starts_with("<!--"))
.map(str::to_string)
}
/// Per-section status of the prompt this project composes.
///
/// Why: see the module docs. Takes `roster_present` because the roster alone
/// selects the composer, and the roster-absent string assembly can place only
/// `WORKFLOW`, `MEMORY` and `AGENT-DELEGATION`.
/// What: one row per [`SectionId::CANONICAL`] entry. A section with no marker
/// block is [`SectionState::Core`] or [`SectionState::Overridable`] by the
/// safety core's list; one whose override the composer applied is
/// [`SectionState::OverriddenByProject`]; one whose override it declined
/// carries the reason.
/// Test: `identity_and_a_former_core_section_report_overridden`,
/// `a_core_override_reports_declined`, `the_roster_absent_path_reports_what_it_cannot_place`.
pub fn section_statuses(
package: &InstructionPackage,
project_dir: &Path,
roster_present: bool,
) -> Vec<SectionStatus> {
let scanned = scan_project(project_dir);
let mut declined: Vec<(SectionId, String)> = Vec::new();
if roster_present {
let (_, rejected) = package.with_overrides(&scanned.overrides);
for rejection in &rejected {
match rejected_section(rejection) {
Some(section) => declined.push((section, rejection.to_string())),
None => declined.extend(
scanned
.overrides
.iter()
.map(|o| (o.section, rejection.to_string())),
),
}
}
} else {
declined.extend(
scanned
.overrides
.iter()
.filter(|o| {
!matches!(
o.section,
SectionId::Workflow | SectionId::Memory | SectionId::AgentDelegation
)
})
.map(|o| {
(
o.section,
"no agent is deployed, so the roster-absent composer cannot place \
this section"
.to_string(),
)
}),
);
}
SectionId::CANONICAL
.into_iter()
.map(|section| {
let over = scanned.overrides.iter().find(|o| o.section == section);
let core = is_fixed_core_section(section);
let state = match (over, declined.iter().find(|(s, _)| *s == section)) {
(None, _) if core => SectionState::Core,
(None, _) => SectionState::Overridable,
(Some(_), Some((_, reason))) if core => SectionState::CoreDeclined(reason.clone()),
(Some(_), Some((_, reason))) => SectionState::OverridableDeclined(reason.clone()),
(Some(_), None) => SectionState::OverriddenByProject,
};
let kept = if state == SectionState::OverriddenByProject {
pinned_members_of(section)
} else {
Vec::new()
};
SectionStatus {
section,
token: section_token(section),
kept,
state,
source: over.map(|o| (o.host.clone(), o.line)),
probe: over.and_then(|o| probe_line(&o.body)),
}
})
.collect()
}
/// Render the report, checked against the composed `prompt`.
///
/// Why: a report computed beside the composition could still drift from it;
/// checking each override's text against the delivered prompt is what makes a
/// disagreement visible instead of silent (#8533).
/// What: one line per section — token, state, marker location — plus
/// `keeps: <member>` for an overridden section with a pinned safety-core block,
/// and `NOT FOUND in the composed prompt` when an overridden section's first
/// content line is absent from `prompt`. Then one `safety core <name> NOT
/// FOUND` line per [`missing_core_members`] of `prompt` — `roster` is the
/// rendered roster the prompt was composed with, `None` when no agent is
/// deployed. A `keeps:` claim is therefore checked, not asserted.
/// Test: `render_flags_an_override_missing_from_the_prompt`,
/// `render_flags_a_safety_core_member_missing_from_the_prompt`.
pub fn render_section_report(
statuses: &[SectionStatus],
prompt: &str,
roster: Option<&str>,
) -> String {
let mut out =
String::from("instruction sections (core / overridable / overridden-by-project):\n");
for row in statuses {
let location = row
.source
.as_ref()
.map(|(host, line)| format!(" {}:{line}", host.display()))
.unwrap_or_default();
let mut detail = row.state.label();
if !row.kept.is_empty() {
detail.push_str(&format!(" (keeps: {})", row.kept.join(", ")));
}
if row.state == SectionState::OverriddenByProject
&& row.probe.as_deref().is_some_and(|p| !prompt.contains(p))
{
detail.push_str(" (override text NOT FOUND in the composed prompt)");
}
out.push_str(&format!(" {:<34} {detail}{location}\n", row.token));
}
for name in missing_core_members(prompt, roster) {
out.push_str(&format!(
" safety core {name} NOT FOUND in the composed prompt\n"
));
}
out
}
/// The rendered section report for `project_dir`, checked against `prompt`.
///
/// Why: the one call `tm sessions instructions` makes, so the CLI cannot pick
/// a different package or roster test than the composer.
/// What: the bundled package, the live roster, then [`section_statuses`] and
/// [`render_section_report`]. The roster is scanned again here; the agent tiers
/// are machine-global, so a deploy between the two scans can flag the roster
/// NOT FOUND — a false alarm, never a false pass.
/// Test: `instructions_reports_section_status_and_project_style`.
pub fn section_report_for(
content: &crate::core::framework_content::FrameworkContent,
project_dir: &Path,
prompt: &str,
) -> String {
let roster = crate::core::delegation_authority::deployed_roster_section(project_dir);
// #9012: the package comes from the caller's content.
render_section_report(
§ion_statuses(content.pm_package(), project_dir, roster.is_some()),
prompt,
roster.as_deref(),
)
}
#[cfg(test)]
#[path = "instruction_section_report_tests.rs"]
mod tests;