use super::guidance::ResolvedGuidance;
use super::resolve::{ResolvedIngest, ResolvedSource};
use super::slice::{NoSignalReason, Slice};
use crate::binding::BuildMode;
use crate::pipeline::{MediumType, PatternMode};
const SLICE_CAP: usize = 25;
pub const PROCESS_MEM_SCHEMA: &str = "ingest@0.1.0";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProcessMemInfo {
pub present: bool,
pub skipped: bool,
pub notice: Option<String>,
pub leaf_name: String,
pub mem_label: String,
}
fn mode_label(mode: BuildMode) -> &'static str {
match mode {
BuildMode::Discovery => "discovery",
BuildMode::OneShot => "one-shot",
}
}
fn medium_type_label(t: MediumType) -> &'static str {
match t {
MediumType::Codebase => "codebase",
MediumType::Filesystem => "filesystem",
MediumType::Graph => "graph",
MediumType::Git => "git",
MediumType::Web => "web",
}
}
pub fn render_goal_and_avoid(guidance: &ResolvedGuidance) -> String {
let mut lines: Vec<String> = Vec::new();
if let Some(goal) = guidance
.goal
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
lines.push("## Goal".to_string());
lines.push(String::new());
lines.push(goal.to_string());
lines.push(String::new());
}
if let Some(avoid) = guidance
.avoid
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
lines.push("## Failure modes to avoid".to_string());
lines.push(String::new());
lines.push(avoid.to_string());
lines.push(String::new());
}
format!("{}\n", lines.join("\n"))
}
pub fn render_situation(resolved: &ResolvedIngest, process_mem: &ProcessMemInfo) -> String {
let mode = mode_label(resolved.mode);
let name = &resolved.name;
let mut lines: Vec<String> = Vec::new();
lines.push("## Situation".to_string());
lines.push(String::new());
lines.push(format!(
"You are running one iteration of `{name}` ({mode} mode) inside a loop. \
Each iteration is a fresh agent with no memory of prior runs; the destination \
graph persists between runs and is your continuity. Backoff is mechanical — \
when nothing has changed since the last run, the loop skips this ingest silently. \
Reporting \"no changes\" is therefore a valid outcome."
));
lines.push(String::new());
lines.push(
"Mutating the destination is this run's mandate: within the destination mem(s) and \
paired process mem named under Operative data, create, update, relate, and delete \
entities without asking. Project-level instructions that make entity creation/deletion \
ask-first govern interactive dev sessions, not ingest iterations — parking creatable \
work as a coverage_gap because of that rule defeats the loop. Mems outside the declared \
destinations remain off-limits."
.to_string(),
);
lines.push(String::new());
lines.push(
"Context budget is finite. The `PreCompact` hook fires near the limit and asks you to \
stop and report. Multiple cycles inside one run are fine when context allows; depth on \
a coherent area beats breadth across unrelated ones."
.to_string(),
);
lines.push(String::new());
if process_mem.present {
lines.push(format!(
"A paired process mem `{}` (schema `{PROCESS_MEM_SCHEMA}`) carries destination-quality \
debt prior runs could not address. Its entries are objective claims about destination \
state — read them on orientation, write to it when this run also cannot fix some debt, \
delete entries the destination has since resolved. Call \
`memstead_schema(name={PROCESS_MEM_SCHEMA})` once for the type vocabulary and write rules.",
process_mem.mem_label
));
} else if let Some(notice) = &process_mem.notice {
lines.push(format!(
"Note: paired process mem `{}` could not be auto-created — {notice}. The run continues \
without it; the operator can retry with `memstead mem init {name} --org-path ingest \
--schema {PROCESS_MEM_SCHEMA}`.",
process_mem.mem_label
));
} else if process_mem.skipped {
lines.push(format!(
"No process mem is paired with this ingest (mode={mode}; one-shot ingests are \
by-design ephemeral)."
));
}
lines.push(String::new());
format!("{}\n", lines.join("\n"))
}
pub fn render_intent(resolved: &ResolvedIngest) -> String {
match resolved
.intent
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
Some(intent) => format!("## About the source\n\n{intent}\n\n"),
None => String::new(),
}
}
pub fn render_operative_data(
resolved: &ResolvedIngest,
process_mem: &ProcessMemInfo,
destination_schema: Option<&str>,
) -> String {
let mut lines: Vec<String> = Vec::new();
lines.push("## Operative data".to_string());
lines.push(String::new());
if !resolved.sources.is_empty() {
lines.push("### Sources".to_string());
lines.push(String::new());
let mut reference_mems: Vec<String> = Vec::new();
for source in &resolved.sources {
match source {
ResolvedSource::Primary(p) => {
lines.push(format!(
"- **{}** (primary)",
medium_type_label(p.medium_type)
));
let allows: Vec<&str> = p
.scope
.iter()
.filter(|r| r.mode == PatternMode::Allow)
.map(|r| r.path.as_str())
.collect();
let denies: Vec<&str> = p
.scope
.iter()
.filter(|r| r.mode == PatternMode::Deny)
.map(|r| r.path.as_str())
.collect();
if !allows.is_empty() {
lines.push(format!(" - Paths: {}", allows.join(", ")));
}
if !denies.is_empty() {
lines.push(format!(" - Ignore: {}", denies.join(", ")));
}
}
ResolvedSource::Reference { mem } => {
lines.push(format!("- **graph** (reference) — mem: {mem}"));
reference_mems.push(mem.clone());
}
}
}
lines.push(String::new());
if !reference_mems.is_empty() {
lines.push(
"Sources tagged `(reference)` are read-only context for cross-mem edges — search \
them, never write into them. Only `(primary)` sources are ingested into the \
destination."
.to_string(),
);
lines.push(String::new());
let mem_list = reference_mems
.iter()
.map(|v| format!("`memstead_search mem={v}`"))
.collect::<Vec<_>>()
.join(", ");
lines.push(format!(
"**Cross-mem references:** consult {mem_list} before authoring cross-mem edges. \
The target entity must exist — a wiki-link or relationship to a missing target \
either auto-stubs (silent) or fails authorization (`CROSS_MEM_RELATION`)."
));
lines.push(String::new());
}
}
lines.push("### Destination".to_string());
lines.push(String::new());
let schema_bit = destination_schema
.map(|s| format!(" — schema: `{s}`"))
.unwrap_or_default();
lines.push(format!("- **{}**{schema_bit}", resolved.destination_mem));
lines.push(String::new());
if process_mem.present {
lines.push("### Paired process mem".to_string());
lines.push(String::new());
lines.push(format!(
"- **{}** — schema: `{PROCESS_MEM_SCHEMA}`. Inspect via `memstead_overview` / \
`memstead_search mem={}`.",
process_mem.mem_label, process_mem.leaf_name
));
lines.push(String::new());
}
format!("{}\n", lines.join("\n"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncCommand {
pub key: String,
pub token: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NoSignalNote {
pub source: String,
pub reason: NoSignalReason,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceCursor {
pub union: Slice,
pub write_commands: Vec<SyncCommand>,
pub reseed: Vec<SyncCommand>,
pub no_signal: Vec<NoSignalNote>,
pub any_changes: bool,
pub degraded: bool,
pub dead_denies: Vec<String>,
pub dest_mem: String,
pub binding_id: String,
}
fn shell_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
fn render_slice_class(lines: &mut Vec<String>, label: &str, paths: &[String]) {
if paths.is_empty() {
return;
}
let shown = paths.len().min(SLICE_CAP);
lines.push(format!("**{label}:**"));
for path in &paths[..shown] {
lines.push(format!("- `{path}`"));
}
if paths.len() > shown {
lines.push(format!(
"- …and {} more {}",
paths.len() - shown,
label.to_lowercase()
));
}
lines.push(String::new());
}
fn no_signal_reason_text(reason: NoSignalReason) -> &'static str {
match reason {
NoSignalReason::Unscoped => {
"unscoped facet (no allow patterns) — nothing is monitored; write `**/*` in the \
facet scope to watch the whole medium"
}
NoSignalReason::DetectionNone => {
"`signal:none` — change detection is disabled for this source (declared `none`)"
}
NoSignalReason::GitUnavailable => {
"git signal unavailable — no work tree, an unreadable `HEAD`, or an unknown baseline; \
a full re-roam is warranted this pass"
}
NoSignalReason::GraphSnapshotMissing => {
"graph snapshot missing — the source mem has no comparable baseline this pass"
}
}
}
pub fn render_changed_slice(cursor: &SourceCursor) -> String {
if !cursor.any_changes
&& cursor.reseed.is_empty()
&& cursor.no_signal.is_empty()
&& cursor.dead_denies.is_empty()
{
return String::new();
}
let mut lines: Vec<String> = Vec::new();
lines.push("## Source changes since the last sync\n".to_string());
if cursor.any_changes {
lines.push(
"The source moved since this graph was last synced. Steer this pass at these changed \
artifacts **first** — they are where the graph is most likely now wrong.\n"
.to_string(),
);
render_slice_class(&mut lines, "Deleted", &cursor.union.deleted);
render_slice_class(&mut lines, "Modified", &cursor.union.modified);
render_slice_class(&mut lines, "Added", &cursor.union.added);
if cursor.degraded {
lines.push(
"_(Precise change history for one or more facets was unavailable, so its full \
current file set is listed above. Detection still fired from the durable baseline; \
targeting is coarser this pass only.)_\n"
.to_string(),
);
}
}
if !cursor.reseed.is_empty() {
let keys = cursor
.reseed
.iter()
.map(|r| format!("`{}`", r.key))
.collect::<Vec<_>>()
.join(", ");
let it = if cursor.reseed.len() == 1 {
"it"
} else {
"them"
};
lines.push(format!(
"No prior sync baseline exists for {keys} — treating the current source state as the \
baseline (first sync). No priority slice from {it} this pass; proceed as usual.\n"
));
}
if !cursor.no_signal.is_empty() {
lines.push(
"Some sources produced **no change signal** this pass — detection could not compare \
them against a baseline, so they were not steered (roam them as usual). This is \
distinct from a source that was checked and had not moved:\n"
.to_string(),
);
for note in &cursor.no_signal {
lines.push(format!(
"- `{}`: {}",
note.source,
no_signal_reason_text(note.reason)
));
}
lines.push(String::new());
}
if !cursor.dead_denies.is_empty() {
lines.push(
"**Warning — some `deny_paths` entries match nothing.** The following ingest \
`deny_paths` selected **no file** in the project tree, so they exclude nothing from \
the slice and hide nothing from the ingest agent. This is usually a typo or a legacy \
bare name that never migrated to the workspace-relative glob dialect (e.g. `dev` → \
`dev/**`, `VISION.md` → `**/VISION.md`). Fix or remove them:\n"
.to_string(),
);
for entry in &cursor.dead_denies {
lines.push(format!("- `{entry}`"));
}
lines.push(String::new());
}
let has_baseline_to_advance = !cursor.write_commands.is_empty() || !cursor.reseed.is_empty();
if has_baseline_to_advance {
lines.push("### Recording your dispositions (do this LAST)\n".to_string());
lines.push(
"Only after you have worked the changed artifacts above — and only for the artifacts \
you actually judged — record a disposition for each, so the next pass targets just \
what changes next. This advance is resumable and non-stalling: a partial pass is \
honored, and if the source moves mid-pass the remaining slice re-presents \
(remaining + new) without losing your recorded work.\n"
.to_string(),
);
lines.push(
"In this window you supply a disposition for **every** artifact explicitly \
(auto-derivation lands in a later cycle). The gate accepts only artifact ids listed \
above — an unknown id refuses the whole call. When every artifact is disposed, the \
sync baseline advances automatically. Run:\n"
.to_string(),
);
lines.push("```sh".to_string());
lines.push(format!(
"memstead projection advance {} --dispositions {}",
cursor.binding_id,
shell_quote(r#"{"<artifact>": "<disposition>", ...}"#)
));
lines.push("```".to_string());
lines.push(
"If you were interrupted before finishing, that is fine — your recorded dispositions \
persist, and the next run re-presents only what is left.\n"
.to_string(),
);
}
format!("{}\n", lines.join("\n"))
}
pub fn assemble_discovery_brief(
resolved: &ResolvedIngest,
guidance: &ResolvedGuidance,
process_mem: &ProcessMemInfo,
destination_schema: Option<&str>,
changed_slice_preface: &str,
) -> String {
let parts = [
render_situation(resolved, process_mem),
render_intent(resolved),
render_goal_and_avoid(guidance),
render_operative_data(resolved, process_mem, destination_schema),
changed_slice_preface.to_string(),
];
parts
.into_iter()
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
.join("")
}
pub fn render_one_shot_lens(
resolved: &ResolvedIngest,
destination_schema: Option<&str>,
destination_purpose: Option<&str>,
) -> String {
let cell = |s: &str| s.replace('|', "\\|").replace('\n', " ");
let mut lines: Vec<String> = vec![
"## Mode: one-shot — lens routing".to_string(),
String::new(),
"A lens iterates entities once and writes per-destination, then exits. The agent decides \
per-entity which destinations to target (Routing rule). Re-runs use `memstead_update`; \
never duplicate."
.to_string(),
String::new(),
];
lines.push("### Destination set".to_string());
lines.push(String::new());
lines.push("| Mem | Schema | Purpose |".to_string());
lines.push("|-------|--------|---------|".to_string());
let schema = destination_schema.unwrap_or("(none)");
let purpose = destination_purpose
.filter(|s| !s.is_empty())
.unwrap_or("(no purpose declared)");
lines.push(format!(
"| {} | {} | {} |",
cell(&resolved.destination_mem),
cell(schema),
cell(purpose)
));
lines.push(String::new());
if let Some(routing) = resolved
.rules
.as_ref()
.and_then(|r| r.get("routing"))
.and_then(|v| v.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
{
lines.push("### Routing rule".to_string());
lines.push(String::new());
lines.push("```".to_string());
lines.push(routing.to_string());
lines.push("```".to_string());
lines.push(String::new());
}
lines.push("### Idempotency".to_string());
lines.push(String::new());
lines.push("- Search the destination before writing; route changes through `memstead_update` against the existing entity if present.".to_string());
lines.push("- Skip writes when the lifted content matches what is already there (record as `skipped: already-up-to-date`).".to_string());
lines.push(
"- Use `memstead_create` only when no entity for that concept exists yet.".to_string(),
);
lines.push(String::new());
lines.push("### End-of-run report".to_string());
lines.push(String::new());
lines.push("After every destination is processed, emit one block per destination on stdout, in Destination-set order:".to_string());
lines.push(String::new());
lines.push("```".to_string());
lines.push(format!("### Report: {}", resolved.name));
lines.push(String::new());
lines.push("Destination: <mem>".to_string());
lines.push(" created: <count>".to_string());
lines.push(" updated: <count>".to_string());
lines.push(" skipped: <count>".to_string());
lines.push(" failed: <count>".to_string());
lines.push(" failures:".to_string());
lines.push(" - <entity-key>: <error verbatim>".to_string());
lines.push(" skipped-detail:".to_string());
lines.push(" - <entity-key>: <one-line reason>".to_string());
lines.push("```".to_string());
lines.push(String::new());
lines.push("Per-destination commits are independent — partial success is the accepted failure mode. No rollback.".to_string());
lines.push(String::new());
let archive = resolved
.post_actions
.as_ref()
.and_then(|p| p.get("archive_source"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
if archive {
lines.push("### Archive after run".to_string());
lines.push(String::new());
lines.push("After the report has been emitted, archive the source planning mem — `post_actions.archive_source` is set on this ingest.".to_string());
lines.push(String::new());
}
format!("{}\n", lines.join("\n"))
}
pub fn assemble_one_shot_brief(
resolved: &ResolvedIngest,
guidance: &ResolvedGuidance,
process_mem: &ProcessMemInfo,
destination_schema: Option<&str>,
destination_purpose: Option<&str>,
) -> String {
let parts = [
render_situation(resolved, process_mem),
render_intent(resolved),
render_goal_and_avoid(guidance),
render_operative_data(resolved, process_mem, destination_schema),
render_one_shot_lens(resolved, destination_schema, destination_purpose),
];
parts
.into_iter()
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
.join("")
}
use super::findings::{Finding, FindingClass, FindingTarget};
use super::prune::{PruneDisposition, PruneProposal};
const FINDINGS_CAP: usize = SLICE_CAP;
pub fn render_verify_brief(resolved: &ResolvedIngest, backlog: usize) -> String {
let mut lines: Vec<String> = vec![
"## Verify — measure fidelity, do not mutate".to_string(),
String::new(),
];
lines.push(format!(
"You are measuring the fidelity of `{}` — how faithfully the destination mem \
`{}` still matches its source. This pass **only measures**: read the source \
and the mem's anchors, judge whether the graph still holds, and record what \
you find. Nothing here writes into the destination mem.",
resolved.name, resolved.destination_mem
));
lines.push(String::new());
lines.push("### Adjudicate the queued findings (capped)".to_string());
lines.push(String::new());
if backlog == 0 {
lines.push(
"No findings are queued for adjudication this pass. Spot-check the resolving \
anchors and the uncovered-artifact sample the fidelity report lists, and \
record any drift you observe as a finding."
.to_string(),
);
} else {
lines.push(format!(
"{backlog} finding(s) are queued for adjudication. Working up to the per-run \
adjudication cap (an operations knob — the remainder stays queued and \
re-presents on a later pass), take each queued finding and compare the \
anchored source content against what the entity records. Classify it: still \
accurate, or drifted. **Record the verdict — this is a measurement, not a \
repair.** A drift you record becomes a finding the sync pass repairs; you do \
not fix it here."
));
}
lines.push(String::new());
lines.push("### Out of scope for verify — no mutation".to_string());
lines.push(String::new());
lines.push(
"Verify writes **nothing** into the destination mem. Do not update a \
`specifies` / `constraints` section, do not create or delete an entity, do not \
add or remove a relationship. When measurement shows the graph is wrong, that \
is a **finding** — the sync brief (`memstead projection brief --sync`) is the \
one place those repairs are made. Leave every fix to it."
.to_string(),
);
lines.push(String::new());
format!("{}\n", lines.join("\n"))
}
fn finding_target_label(target: &FindingTarget) -> String {
match target {
FindingTarget::Anchor { entity, artifact } => format!("`{entity}` → `{artifact}`"),
FindingTarget::Artifact { artifact } => format!("`{artifact}`"),
}
}
fn render_findings_group(
lines: &mut Vec<String>,
heading: &str,
guidance: &str,
items: &[&Finding],
) {
if items.is_empty() {
return;
}
lines.push(format!("### {heading}"));
lines.push(String::new());
lines.push(guidance.to_string());
lines.push(String::new());
let shown = items.len().min(FINDINGS_CAP);
for f in &items[..shown] {
lines.push(format!(
"- {} — {}",
finding_target_label(&f.target),
f.detail
));
}
if items.len() > shown {
lines.push(format!("- …and {} more", items.len() - shown));
}
lines.push(String::new());
}
fn render_open_findings(findings: &[Finding]) -> String {
if findings.is_empty() {
return String::new();
}
let mut lines: Vec<String> = vec![
"## Open findings to repair".to_string(),
String::new(),
"The verify pass recorded these against the current source state. Repair them \
conservatively (see the rules below); a finding you judge already correct needs \
no write."
.to_string(),
String::new(),
];
let group = |class: FindingClass| -> Vec<&Finding> {
findings.iter().filter(|f| f.class == class).collect()
};
render_findings_group(
&mut lines,
"Drifted — the anchored content changed",
"The source the entity describes moved. Update the affected section to match — \
only the part that changed. If the entity is still accurate, leave it.",
&group(FindingClass::Drifted),
);
render_findings_group(
&mut lines,
"Wrong — an adjudicated content mismatch",
"Adjudication found the entity no longer matches its source. Correct the \
mismatched section; do not rewrite what still holds.",
&group(FindingClass::Wrong),
);
render_findings_group(
&mut lines,
"Unresolvable anchor — the artifact is gone",
"The source artifact an anchor references is no longer present. Delete the entity \
**only** if the concept is removed entirely; otherwise leave it. Concept-level \
removals are a prune concern with its own never-clobber / conflict-flag rules — \
do not delete on a hunch here.",
&group(FindingClass::UnresolvableAnchor),
);
render_findings_group(
&mut lines,
"Uncovered — a source artifact with no entity",
"An in-scope source artifact has no anchor in the mem. Create an entity for it \
**only** if it is a clearly-new concept with no existing entity; otherwise \
extend the entity that already owns the concept, or leave it for a discovery \
build.",
&group(FindingClass::Uncovered),
);
render_findings_group(
&mut lines,
"Queued for adjudication — not yet judged",
"These are not adjudicated yet — that is the verify pass's job, not sync's. \
**Skip them here**; they become repairable only after verify classifies them as \
drifted.",
&group(FindingClass::QueuedForAdjudication),
);
format!("{}\n", lines.join("\n"))
}
fn render_prune_proposals(proposals: &[PruneProposal]) -> String {
if proposals.is_empty() {
return String::new();
}
let mut lines: Vec<String> = vec![
"## Prune — proposed removals (you decide; nothing is auto-deleted)".to_string(),
String::new(),
"The source removed the artifacts these entities describe. Each item below is a \
**proposal**: prune writes nothing — you enact (or reject) the removal through the \
normal MCP mutation surface. An `authored` entity is never proposed here; a `derived` \
entity is flagged, never proposed for deletion."
.to_string(),
String::new(),
];
let group = |d: PruneDisposition| -> Vec<&PruneProposal> {
proposals.iter().filter(|p| p.disposition == d).collect()
};
let clean = group(PruneDisposition::CleanDelete);
if !clean.is_empty() {
lines.push("### Clean delete — never-clobber three-way merge is clean".to_string());
lines.push(String::new());
lines.push(
"The source base leg was retrievable and the three-way merge found no model-side \
divergence, so removal is safe. **Confirm, then delete via the mutation surface** — \
this is still your call, not an auto-delete."
.to_string(),
);
lines.push(String::new());
let shown = clean.len().min(FINDINGS_CAP);
for p in &clean[..shown] {
lines.push(format!(
"- `{}` — source artifact(s) gone: {}",
p.entity,
artifact_list(&p.artifacts)
));
}
if clean.len() > shown {
lines.push(format!("- …and {} more", clean.len() - shown));
}
lines.push(String::new());
}
let conflict = group(PruneDisposition::ConflictFlag);
if !conflict.is_empty() {
lines.push("### Conflict-flag — decide, never overwrite a model-side edit".to_string());
lines.push(String::new());
lines.push(
"No retrievable base leg to merge against (a non-git source, or an anchor with no \
pinned version). **Both sides are shown — decide deliberately.** If the concept is \
truly gone, delete via the mutation surface; if the model side was edited on \
purpose, keep it. Prune never overwrites a model-side edit for you."
.to_string(),
);
lines.push(String::new());
let shown = conflict.len().min(FINDINGS_CAP);
for p in &conflict[..shown] {
lines.push(format!(
"- `{}` — **source side:** artifact(s) gone: {}; **model side:** the entity is \
still present (may carry edits) — you decide.",
p.entity,
artifact_list(&p.artifacts)
));
}
if conflict.len() > shown {
lines.push(format!("- …and {} more", conflict.len() - shown));
}
lines.push(String::new());
}
let derived = group(PruneDisposition::DerivedFlagged);
if !derived.is_empty() {
lines.push("### Derived — flagged, NOT proposed for deletion".to_string());
lines.push(String::new());
lines.push(
"These entities were **derived** from other inputs. A derived entity is flagged, \
never auto-proposed for deletion — its inputs may still hold even though one source \
artifact vanished. Re-examine the inputs before removing anything."
.to_string(),
);
lines.push(String::new());
let shown = derived.len().min(FINDINGS_CAP);
for p in &derived[..shown] {
let inputs = if p.derived_inputs.is_empty() {
"(no recorded inputs)".to_string()
} else {
artifact_list(&p.derived_inputs)
};
lines.push(format!(
"- `{}` — derived from: {}; source artifact(s) gone: {}.",
p.entity,
inputs,
artifact_list(&p.artifacts)
));
}
if derived.len() > shown {
lines.push(format!("- …and {} more", derived.len() - shown));
}
lines.push(String::new());
}
format!("{}\n", lines.join("\n"))
}
fn artifact_list(artifacts: &[String]) -> String {
if artifacts.is_empty() {
return "(none)".to_string();
}
artifacts
.iter()
.map(|a| format!("`{a}`"))
.collect::<Vec<_>>()
.join(", ")
}
fn render_sync_situation(resolved: &ResolvedIngest) -> String {
format!(
"## Sync — repair the graph to match the source\n\n\
You are running the sync pass for `{}`. Sync is the graph's **sole maintenance \
writer**: the only place the destination mem `{}` is repaired to match its \
source. Two inputs steer this pass — the source changes since the last sync, and \
the open verify findings — both below. Work them: update, create, relate, and \
(rarely) delete entities so the graph again matches the source.\n\n\
Every mutation routes through the normal MCP mutation surface, and the engine \
commits each one **per-mutation** to the mem's own gitdir. You **stage nothing \
and commit nothing yourself** — not the graph, not the code. Sync commits \
nothing.\n\n",
resolved.name, resolved.destination_mem
)
}
fn render_adopt_framing(resolved: &ResolvedIngest) -> String {
format!(
"## First sync — adopting `{}`\n\n\
This mem predates its binding: it has no anchors and no prior sync baseline, so \
**0% anchored is expected — this is onboarding, not a failure.** Do not read it \
as drift or a red verdict. There is no cursor to diff against, so the baseline is \
the **current** source HEAD — do **not** replay the whole history; treat the \
current source state as the starting point, and this is a **first sync**.\n\n\
**Backfill path:** run `memstead projection verify {}` to enumerate the in-scope \
source artifacts that carry no entity yet, then cover the clearly-new concepts \
among them through the normal MCP mutation surface — the same conservative rules \
below apply. Backfilling is incremental: a partial pass is fine, and the next \
sync continues where you left off.\n\n",
resolved.destination_mem, resolved.name
)
}
fn render_sync_conservatism() -> String {
let lines: Vec<&str> = vec![
"## How to repair — be conservative",
"",
"Repair only what the source changes and the findings above actually justify:",
"",
"- **Unsure whether an entity is affected — skip it.** A missed update is a later \
finding; a wrong rewrite is damage.",
"- **Do not create a new entity unless the change clearly introduces a new concept \
with no existing entity.** Prefer updating the entity that already owns the \
concept.",
"- **Do not delete an entity unless the change removes the concept entirely.** \
Deletions a prune pass surfaces follow prune's own never-clobber / conflict-flag \
rules — never delete on a hunch here.",
"- **Never rewrite a section that has not changed** — touch only the part the \
change or finding actually affects.",
"- **No speculative edges — add only relationships the diff literally introduces** \
(a new `use` / `import` / dependency you can point at in the change).",
"- **A dropped dependency FLAGS, it does not auto-remove.** If the change removes an \
import or dependency, leave the matching edge intact and note it for a later \
audit — removals are ambiguous (temporary refactor vs. permanent cut), and a \
stale edge is less damaging than an erased real one. **Edge removal is out of \
scope for sync.**",
"- **Rationale is reasoning, not a changelog.** When you record why a change was \
made, append the *reasoning* (why this approach, which trade-offs) — never \
`[commit <hash>]` log-style entries.",
"",
];
format!("{}\n", lines.join("\n"))
}
pub fn render_sync_brief(
resolved: &ResolvedIngest,
cursor: &SourceCursor,
findings: &[Finding],
prune: &[PruneProposal],
adopt: bool,
) -> String {
let preface = render_changed_slice(cursor);
let open_findings = render_open_findings(findings);
let prune_block = render_prune_proposals(prune);
let has_work =
adopt || !preface.is_empty() || !open_findings.is_empty() || !prune_block.is_empty();
let mut parts: Vec<String> = vec![render_sync_situation(resolved)];
if !has_work {
parts.push(
"## Nothing to sync\n\nThe source has not moved since the last sync, no \
verify findings are open, and no prune proposals stand. There is nothing to \
repair this pass — reporting \"no changes\" is a valid outcome.\n\n"
.to_string(),
);
return parts
.into_iter()
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
.join("");
}
if adopt {
parts.push(render_adopt_framing(resolved));
}
parts.push(preface);
parts.push(open_findings);
parts.push(prune_block);
parts.push(render_sync_conservatism());
parts
.into_iter()
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
.join("")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ingest::resolve::ResolvedPrimarySource;
use crate::pipeline::{IngestTrigger, PatternEntry};
fn guidance(goal: Option<&str>, avoid: Option<&str>) -> ResolvedGuidance {
ResolvedGuidance {
goal: goal.map(str::to_string),
avoid: avoid.map(str::to_string),
}
}
#[test]
fn renders_goal_and_avoid_blocks() {
let out = render_goal_and_avoid(&guidance(Some(" build coverage "), Some("no stubs")));
assert_eq!(
out,
"## Goal\n\nbuild coverage\n\n## Failure modes to avoid\n\nno stubs\n\n"
);
}
#[test]
fn renders_goal_only() {
assert_eq!(
render_goal_and_avoid(&guidance(Some("build coverage"), None)),
"## Goal\n\nbuild coverage\n\n"
);
}
#[test]
fn renders_avoid_only() {
assert_eq!(
render_goal_and_avoid(&guidance(None, Some("no stubs"))),
"## Failure modes to avoid\n\nno stubs\n\n"
);
}
#[test]
fn empty_guidance_yields_a_newline() {
assert_eq!(render_goal_and_avoid(&guidance(None, None)), "\n");
assert_eq!(render_goal_and_avoid(&guidance(Some(" "), None)), "\n");
}
fn primary(medium_type: MediumType, scope: Vec<PatternEntry>) -> ResolvedSource {
ResolvedSource::Primary(ResolvedPrimarySource {
facet_ref: "f".to_string(),
medium: "m".to_string(),
medium_type,
medium_pointer: "../src".to_string(),
declared_change_detection: None,
scope,
preparation: None,
})
}
fn resolved(name: &str, intent: Option<&str>, sources: Vec<ResolvedSource>) -> ResolvedIngest {
ResolvedIngest {
name: name.to_string(),
mode: BuildMode::Discovery,
trigger: IngestTrigger::Loop,
batch_size: 20,
deny_paths: vec![],
projection_ref: format!("{name}/p"),
projection_mem: name.to_string(),
projection_name: "p".to_string(),
intent: intent.map(str::to_string),
sources,
destination_mem: name.to_string(),
rules: None,
post_actions: None,
}
}
fn process_present(name: &str) -> ProcessMemInfo {
ProcessMemInfo {
present: true,
skipped: false,
notice: None,
leaf_name: name.to_string(),
mem_label: format!("ingest/{name}"),
}
}
fn allow(path: &str) -> PatternEntry {
PatternEntry {
path: path.to_string(),
mode: PatternMode::Allow,
}
}
fn deny(path: &str) -> PatternEntry {
PatternEntry {
path: path.to_string(),
mode: PatternMode::Deny,
}
}
#[test]
fn renders_intent() {
let r = resolved("macos", Some(" Swift app source. "), vec![]);
assert_eq!(
render_intent(&r),
"## About the source\n\nSwift app source.\n\n"
);
let none = resolved("macos", None, vec![]);
assert_eq!(render_intent(&none), "");
}
#[test]
fn renders_situation_with_present_process_mem() {
let r = resolved("macos", None, vec![]);
let out = render_situation(&r, &process_present("macos"));
assert!(out.starts_with("## Situation\n\nYou are running one iteration of `macos` (discovery mode) inside a loop."));
assert!(out.contains("Mutating the destination is this run's mandate:"));
assert!(out.contains("The `PreCompact` hook fires near the limit"));
assert!(out.contains("A paired process mem `ingest/macos` (schema `ingest@0.1.0`) carries destination-quality debt"));
assert!(
out.ends_with("write rules.\n\n"),
"block ends in a blank line"
);
}
#[test]
fn situation_process_mem_branches() {
let mut r = resolved("os", None, vec![]);
r.mode = BuildMode::OneShot;
let skipped = ProcessMemInfo {
present: false,
skipped: true,
notice: None,
leaf_name: "os".to_string(),
mem_label: "ingest/os".to_string(),
};
assert!(
render_situation(&r, &skipped)
.contains("No process mem is paired with this ingest (mode=one-shot;")
);
let failed = ProcessMemInfo {
present: false,
skipped: false,
notice: Some("engine offline".to_string()),
leaf_name: "os".to_string(),
mem_label: "ingest/os".to_string(),
};
let out = render_situation(&resolved("os", None, vec![]), &failed);
assert!(out.contains("could not be auto-created — engine offline."));
assert!(out.contains("memstead mem init os --org-path ingest --schema ingest@0.1.0"));
}
#[test]
fn renders_operative_data_full() {
let r = resolved(
"macos",
None,
vec![
primary(
MediumType::Codebase,
vec![allow("src/**/*.swift"), deny("src/gen/**")],
),
ResolvedSource::Reference {
mem: "engine".to_string(),
},
],
);
let out = render_operative_data(&r, &process_present("macos"), Some("macos-code@0.1.0"));
let expected = "\
## Operative data
### Sources
- **codebase** (primary)
- Paths: src/**/*.swift
- Ignore: src/gen/**
- **graph** (reference) — mem: engine
Sources tagged `(reference)` are read-only context for cross-mem edges — search them, never write into them. Only `(primary)` sources are ingested into the destination.
**Cross-mem references:** consult `memstead_search mem=engine` before authoring cross-mem edges. The target entity must exist — a wiki-link or relationship to a missing target either auto-stubs (silent) or fails authorization (`CROSS_MEM_RELATION`).
### Destination
- **macos** — schema: `macos-code@0.1.0`
### Paired process mem
- **ingest/macos** — schema: `ingest@0.1.0`. Inspect via `memstead_overview` / `memstead_search mem=macos`.
\n";
assert_eq!(out, expected);
}
#[test]
fn renders_operative_data_minimal() {
let r = resolved("g", None, vec![primary(MediumType::Filesystem, vec![])]);
let skipped = ProcessMemInfo {
present: false,
skipped: true,
notice: None,
leaf_name: "g".to_string(),
mem_label: "ingest/g".to_string(),
};
let out = render_operative_data(&r, &skipped, None);
assert!(out.contains("- **filesystem** (primary)\n"));
assert!(!out.contains("Cross-mem references"), "no reference note");
assert!(out.contains("### Destination\n\n- **g**\n"));
assert!(
!out.contains("Paired process mem"),
"skipped process mem omitted"
);
}
#[test]
fn assembles_discovery_brief() {
let r = resolved(
"macos",
Some("Swift source."),
vec![primary(MediumType::Codebase, vec![allow("src/**")])],
);
let g = guidance(Some("build coverage"), None);
let pm = process_present("macos");
let brief = assemble_discovery_brief(&r, &g, &pm, Some("s@1"), "");
let sit = brief.find("## Situation").unwrap();
let src = brief.find("## About the source").unwrap();
let goal = brief.find("## Goal").unwrap();
let op = brief.find("## Operative data").unwrap();
assert!(
sit < src && src < goal && goal < op,
"blocks in brief order"
);
assert!(
!brief.contains("## Source changes"),
"no changed-slice block when preface empty"
);
let with_slice =
assemble_discovery_brief(&r, &g, &pm, Some("s@1"), "## Source changes\n\n…\n\n");
assert!(with_slice.ends_with("## Source changes\n\n…\n\n"));
}
fn slice(deleted: &[&str], modified: &[&str], added: &[&str]) -> Slice {
Slice {
deleted: deleted.iter().map(|s| s.to_string()).collect(),
modified: modified.iter().map(|s| s.to_string()).collect(),
added: added.iter().map(|s| s.to_string()).collect(),
}
}
fn cmd(key: &str, token: &str) -> SyncCommand {
SyncCommand {
key: key.to_string(),
token: token.to_string(),
}
}
fn note(source: &str, reason: NoSignalReason) -> NoSignalNote {
NoSignalNote {
source: source.to_string(),
reason,
}
}
#[test]
fn changed_slice_empty_when_nothing_moved() {
let cursor = SourceCursor {
union: slice(&[], &[], &[]),
write_commands: vec![],
reseed: vec![],
no_signal: vec![],
any_changes: false,
degraded: false,
dead_denies: vec![],
dest_mem: "engine".to_string(),
binding_id: "engine/graph".to_string(),
};
assert_eq!(render_changed_slice(&cursor), "");
}
#[test]
fn changed_slice_renders_dead_deny_warning() {
let cursor = SourceCursor {
union: slice(&[], &[], &[]),
write_commands: vec![],
reseed: vec![],
no_signal: vec![],
any_changes: false,
degraded: false,
dead_denies: vec!["dev".to_string(), "typo/**".to_string()],
dest_mem: "engine".to_string(),
binding_id: "engine/graph".to_string(),
};
let out = render_changed_slice(&cursor);
assert!(out.contains("deny_paths` entries match nothing"));
assert!(out.contains("- `dev`"));
assert!(out.contains("- `typo/**`"));
}
#[test]
fn changed_slice_renders_slice_and_recording() {
let cursor = SourceCursor {
union: slice(&["a.rs"], &["b.rs"], &[]),
write_commands: vec![cmd("engine-graph/source", "HEADSHA")],
reseed: vec![],
no_signal: vec![],
any_changes: true,
degraded: false,
dead_denies: vec![],
dest_mem: "engine".to_string(),
binding_id: "engine/graph".to_string(),
};
let expected_lines = [
"## Source changes since the last sync\n",
"The source moved since this graph was last synced. Steer this pass at these changed artifacts **first** — they are where the graph is most likely now wrong.\n",
"**Deleted:**",
"- `a.rs`",
"",
"**Modified:**",
"- `b.rs`",
"",
"### Recording your dispositions (do this LAST)\n",
"Only after you have worked the changed artifacts above — and only for the artifacts you actually judged — record a disposition for each, so the next pass targets just what changes next. This advance is resumable and non-stalling: a partial pass is honored, and if the source moves mid-pass the remaining slice re-presents (remaining + new) without losing your recorded work.\n",
"In this window you supply a disposition for **every** artifact explicitly (auto-derivation lands in a later cycle). The gate accepts only artifact ids listed above — an unknown id refuses the whole call. When every artifact is disposed, the sync baseline advances automatically. Run:\n",
"```sh",
r#"memstead projection advance engine/graph --dispositions '{"<artifact>": "<disposition>", ...}'"#,
"```",
"If you were interrupted before finishing, that is fine — your recorded dispositions persist, and the next run re-presents only what is left.\n",
];
assert_eq!(
render_changed_slice(&cursor),
format!("{}\n", expected_lines.join("\n"))
);
}
#[test]
fn changed_slice_reseed_only() {
let cursor = SourceCursor {
union: slice(&[], &[], &[]),
write_commands: vec![],
reseed: vec![cmd("ing/f", "TOK")],
no_signal: vec![],
any_changes: false,
degraded: false,
dead_denies: vec![],
dest_mem: "d".to_string(),
binding_id: "d/p".to_string(),
};
let out = render_changed_slice(&cursor);
assert!(out.starts_with("## Source changes since the last sync\n\n"));
assert!(out.contains(
"No prior sync baseline exists for `ing/f` — treating the current source state as the baseline (first sync). No priority slice from it this pass; proceed as usual."
));
assert!(out.contains(
r#"memstead projection advance d/p --dispositions '{"<artifact>": "<disposition>", ...}'"#
));
assert!(
!out.contains("The source moved"),
"no 'moved' copy when only reseeding"
);
}
#[test]
fn changed_slice_renders_no_signal_reasons_distinguishably() {
let cursor = SourceCursor {
union: slice(&[], &[], &[]),
write_commands: vec![],
reseed: vec![],
no_signal: vec![
note("code-facet", NoSignalReason::Unscoped),
note("plan-facet", NoSignalReason::DetectionNone),
note("git-facet", NoSignalReason::GitUnavailable),
note("ref-mem", NoSignalReason::GraphSnapshotMissing),
],
any_changes: false,
degraded: false,
dead_denies: vec![],
dest_mem: "d".to_string(),
binding_id: "d/p".to_string(),
};
let out = render_changed_slice(&cursor);
assert!(out.starts_with("## Source changes since the last sync\n"));
assert!(out.contains("Some sources produced **no change signal**"));
assert!(out.contains("- `code-facet`: unscoped facet (no allow patterns)"));
assert!(
out.contains("- `plan-facet`: `signal:none`"),
"detection-none renders the literal signal:none state"
);
assert!(out.contains("- `git-facet`: git signal unavailable"));
assert!(out.contains("- `ref-mem`: graph snapshot missing"));
let texts = [
no_signal_reason_text(NoSignalReason::Unscoped),
no_signal_reason_text(NoSignalReason::DetectionNone),
no_signal_reason_text(NoSignalReason::GitUnavailable),
no_signal_reason_text(NoSignalReason::GraphSnapshotMissing),
];
for (i, a) in texts.iter().enumerate() {
for b in &texts[i + 1..] {
assert_ne!(a, b, "each no-signal reason must render distinctly");
}
}
assert!(!out.contains("### Recording your dispositions"));
assert!(!out.contains("The source moved"));
}
#[test]
fn changed_slice_mixes_changes_and_no_signal() {
let cursor = SourceCursor {
union: slice(&[], &["b.rs"], &[]),
write_commands: vec![cmd("ing/f", "HEAD")],
reseed: vec![],
no_signal: vec![note("other", NoSignalReason::Unscoped)],
any_changes: true,
degraded: false,
dead_denies: vec![],
dest_mem: "d".to_string(),
binding_id: "d/p".to_string(),
};
let out = render_changed_slice(&cursor);
assert!(out.contains("The source moved"));
assert!(out.contains("**Modified:**"));
assert!(out.contains("- `other`: unscoped facet"));
assert!(out.contains("### Recording your dispositions"));
assert!(out.contains(
r#"memstead projection advance d/p --dispositions '{"<artifact>": "<disposition>", ...}'"#
));
}
#[test]
fn renders_one_shot_lens_block() {
let mut r = resolved("os", Some("plan source"), vec![]);
r.rules = Some(serde_json::json!({ "routing": "route each entity to its spec" }));
r.post_actions = Some(serde_json::json!({ "archive_source": true }));
let out = render_one_shot_lens(&r, Some("planning@0.1.0"), Some("the plan graph"));
assert!(out.starts_with("## Mode: one-shot — lens routing\n\n"));
assert!(out.contains(
"### Destination set\n\n| Mem | Schema | Purpose |\n|-------|--------|---------|\n| os | planning@0.1.0 | the plan graph |\n"
));
assert!(out.contains("### Routing rule\n\n```\nroute each entity to its spec\n```\n"));
assert!(out.contains("### Idempotency"));
assert!(out.contains("### Report: os"));
assert!(out.contains("### Archive after run"));
assert!(out.ends_with("is set on this ingest.\n\n"));
let bare = resolved("os", None, vec![]);
let out2 = render_one_shot_lens(&bare, None, None);
assert!(out2.contains("| os | (none) | (no purpose declared) |"));
assert!(!out2.contains("### Routing rule"));
assert!(!out2.contains("### Archive after run"));
assert!(out2.contains("### End-of-run report"));
}
#[test]
fn assembles_one_shot_brief() {
let mut r = resolved(
"os",
Some("src"),
vec![primary(MediumType::Filesystem, vec![])],
);
r.mode = BuildMode::OneShot;
let g = guidance(Some("goal"), None);
let skipped = ProcessMemInfo {
present: false,
skipped: true,
notice: None,
leaf_name: "os".to_string(),
mem_label: "ingest/os".to_string(),
};
let brief = assemble_one_shot_brief(&r, &g, &skipped, Some("s@1"), Some("purpose"));
assert!(brief.contains("(one-shot mode)"));
assert!(brief.contains("No process mem is paired with this ingest (mode=one-shot;"));
assert!(brief.contains("## Mode: one-shot — lens routing"));
assert!(
!brief.contains("## Source changes"),
"one-shot has no changed-slice"
);
}
#[test]
fn changed_slice_caps_and_degrades_and_quotes() {
let many: Vec<String> = (0..SLICE_CAP + 3).map(|i| format!("f{i}.rs")).collect();
let cursor = SourceCursor {
union: Slice {
deleted: vec![],
modified: vec![],
added: many,
},
write_commands: vec![cmd("ing/f", r#"{"v":1,"aggregate":"x"}"#)],
reseed: vec![],
no_signal: vec![],
any_changes: true,
degraded: true,
dead_denies: vec![],
dest_mem: "d".to_string(),
binding_id: "d/p".to_string(),
};
let out = render_changed_slice(&cursor);
assert!(out.contains(&format!("- …and {} more added", 3)));
assert!(out.contains("Precise change history for one or more facets was unavailable"));
assert!(out.contains(
r#"memstead projection advance d/p --dispositions '{"<artifact>": "<disposition>", ...}'"#
));
}
fn finding(class: FindingClass, target: FindingTarget, detail: &str) -> Finding {
Finding {
key: crate::ingest::findings::FindingKey {
binding_hash: "h".to_string(),
source_head: "s".to_string(),
},
facet: "src".to_string(),
target,
class,
detail: detail.to_string(),
created_at: "1".to_string(),
}
}
fn anchor_target(entity: &str, artifact: &str) -> FindingTarget {
FindingTarget::Anchor {
entity: entity.to_string(),
artifact: artifact.to_string(),
}
}
fn artifact_target(artifact: &str) -> FindingTarget {
FindingTarget::Artifact {
artifact: artifact.to_string(),
}
}
fn empty_cursor() -> SourceCursor {
SourceCursor {
union: slice(&[], &[], &[]),
write_commands: vec![],
reseed: vec![],
no_signal: vec![],
any_changes: false,
degraded: false,
dead_denies: vec![],
dest_mem: "engine".to_string(),
binding_id: "engine/graph".to_string(),
}
}
#[test]
fn verify_brief_measures_and_refuses_mutation() {
let r = resolved("engine", None, vec![]);
let out = render_verify_brief(&r, 3);
assert!(out.starts_with("## Verify — measure fidelity, do not mutate"));
assert!(out.contains("3 finding(s) are queued for adjudication"));
assert!(out.contains("per-run adjudication cap"));
assert!(out.contains("this is a measurement, not a repair"));
assert!(out.contains("Verify writes **nothing** into the destination mem"));
assert!(out.contains("memstead projection brief --sync"));
assert!(out.contains("do not create or delete an entity"));
assert!(!out.contains("via `memstead_create`"));
assert!(!out.contains("Run `memstead_update`"));
let zero = render_verify_brief(&r, 0);
assert!(zero.contains("No findings are queued for adjudication"));
assert!(zero.contains("record any drift you observe as a finding"));
assert!(zero.contains("Verify writes **nothing**"));
}
#[test]
fn sync_brief_carries_both_cursor_and_findings() {
let r = resolved("engine", None, vec![]);
let cursor = SourceCursor {
union: slice(&["gone.rs"], &["moved.rs"], &[]),
write_commands: vec![cmd("engine/graph/src#synced", "HEAD")],
reseed: vec![],
no_signal: vec![],
any_changes: true,
degraded: false,
dead_denies: vec![],
dest_mem: "engine".to_string(),
binding_id: "engine/graph".to_string(),
};
let findings = vec![
finding(
FindingClass::Drifted,
anchor_target("engine--e", "src/moved.rs"),
"prepared-content hash drifted",
),
finding(
FindingClass::Uncovered,
artifact_target("src/new.rs"),
"in scope, no anchor",
),
];
let out = render_sync_brief(&r, &cursor, &findings, &[], false);
assert!(out.contains("## Source changes since the last sync"));
assert!(out.contains("`moved.rs`"));
assert!(out.contains("## Open findings to repair"));
assert!(out.contains("`engine--e` → `src/moved.rs`"));
assert!(out.contains("`src/new.rs`"));
assert!(out.contains("sole maintenance writer"));
assert!(out.contains("commits each one **per-mutation**"));
assert!(out.contains("Sync commits nothing."));
}
#[test]
fn sync_brief_absorbs_reconcile_conservatism() {
let r = resolved("engine", None, vec![]);
let findings = vec![finding(
FindingClass::Uncovered,
artifact_target("src/x.rs"),
"d",
)];
let out = render_sync_brief(&r, &empty_cursor(), &findings, &[], false);
assert!(out.contains("Unsure whether an entity is affected — skip it."));
assert!(out.contains(
"Do not create a new entity unless the change clearly introduces a new concept"
));
assert!(
out.contains("Do not delete an entity unless the change removes the concept entirely.")
);
assert!(out.contains("Never rewrite a section that has not changed"));
assert!(out.contains(
"No speculative edges — add only relationships the diff literally introduces"
));
assert!(out.contains("A dropped dependency FLAGS, it does not auto-remove."));
assert!(out.contains("Edge removal is out of scope for sync."));
assert!(out.contains("Rationale is reasoning, not a changelog."));
assert!(out.contains("`[commit <hash>]` log-style entries"));
}
#[test]
fn sync_brief_renders_adopt_framing() {
let mut r = resolved("engine", None, vec![]);
r.name = "engine/graph".to_string();
let out = render_sync_brief(&r, &empty_cursor(), &[], &[], true);
assert!(out.contains("## First sync — adopting `engine`"));
assert!(out.contains("0% anchored is expected — this is onboarding, not a failure."));
assert!(out.contains("do **not** replay the whole history"));
assert!(out.contains("**Backfill path:**"));
assert!(out.contains("memstead projection verify engine/graph"));
}
#[test]
fn sync_brief_inherits_first_sync_reseed_framing() {
let r = resolved("engine", None, vec![]);
let mut cursor = empty_cursor();
cursor.reseed = vec![cmd("engine/graph/src#synced", "TOK")];
let out = render_sync_brief(&r, &cursor, &[], &[], false);
assert!(out.contains("No prior sync baseline exists for"));
assert!(out.contains("(first sync)"));
}
#[test]
fn sync_brief_nothing_to_sync() {
let r = resolved("engine", None, vec![]);
let out = render_sync_brief(&r, &empty_cursor(), &[], &[], false);
assert!(out.contains("## Nothing to sync"));
assert!(!out.contains("## How to repair"));
assert!(!out.contains("## Open findings"));
}
#[test]
fn only_sync_brief_carries_repair_instructions() {
let r = resolved("engine", None, vec![]);
let findings = vec![finding(
FindingClass::Drifted,
anchor_target("engine--e", "src/a.rs"),
"d",
)];
let verify = render_verify_brief(&r, 1);
let sync = render_sync_brief(&r, &empty_cursor(), &findings, &[], false);
assert!(!verify.contains("## How to repair"));
assert!(!verify.contains("Update the affected section"));
assert!(sync.contains("## How to repair — be conservative"));
assert!(sync.contains("## Open findings to repair"));
assert!(sync.contains("Update the affected section to match"));
}
#[test]
fn sync_brief_caps_large_findings_group() {
let r = resolved("engine", None, vec![]);
let findings: Vec<Finding> = (0..FINDINGS_CAP + 4)
.map(|i| {
finding(
FindingClass::Uncovered,
artifact_target(&format!("src/f{i}.rs")),
"d",
)
})
.collect();
let out = render_sync_brief(&r, &empty_cursor(), &findings, &[], false);
assert!(out.contains("- …and 4 more"));
assert!(!out.contains(&format!("src/f{}.rs", FINDINGS_CAP + 3)));
}
}