Skip to main content

mempal_runtime/core/
phase3.rs

1use std::collections::BTreeMap;
2
3use serde::Serialize;
4use serde_json::{Map, Value};
5
6use super::{
7    anchor,
8    types::{
9        AnchorKind, BootstrapIdentityParts, MemoryDomain, MemoryKind, Provenance,
10        RuntimeAdoptionEvent, RuntimeAdoptionSignal,
11    },
12    utils::build_bootstrap_drawer_id_from_parts,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
16pub struct RuntimeAdoptionGuidance {
17    pub version: u32,
18    pub recording_rule: String,
19    pub required_fields: Vec<String>,
20    pub optional_fields: Vec<String>,
21    pub signals: Vec<RuntimeAdoptionSignalGuidance>,
22    pub tracks: Vec<RuntimeAdoptionTrackGuidance>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26pub struct RuntimeAdoptionSignalGuidance {
27    pub signal: String,
28    pub when: String,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
32pub struct RuntimeAdoptionTrackGuidance {
33    pub track: String,
34    pub when: String,
35    pub feature_examples: Vec<String>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
39pub struct RuntimeAdoptionInstrumentationPolicy {
40    pub version: u32,
41    pub writes: bool,
42    pub default_mode: String,
43    pub allowed_modes: Vec<RuntimeAdoptionInstrumentationMode>,
44    pub forbidden_modes: Vec<String>,
45    pub requirements: Vec<String>,
46    pub rollback_requirements: Vec<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
50pub struct RuntimeAdoptionInstrumentationMode {
51    pub mode: String,
52    pub description: String,
53    pub requires_execute: bool,
54    pub requires_checked_capture: bool,
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize)]
58pub struct RuntimeAdoptionRecordPlan {
59    pub writes: bool,
60    pub record_command: Vec<String>,
61    pub record_payload: Value,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
65pub struct RuntimeAdoptionRecordQualityReport {
66    pub writes: bool,
67    pub valid: bool,
68    pub quality: String,
69    pub errors: Vec<String>,
70    pub warnings: Vec<String>,
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize)]
74pub struct RuntimeAdoptionCheckedRecordReport {
75    pub writes: bool,
76    pub blocked: bool,
77    pub record_quality: RuntimeAdoptionRecordQualityReport,
78    pub event: Option<RuntimeAdoptionEvent>,
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize)]
82pub struct RuntimeAdoptionCaptureReport {
83    pub writes: bool,
84    pub execute: bool,
85    pub surface: String,
86    pub outcome: String,
87    pub record_plan: RuntimeAdoptionRecordPlan,
88    pub record_quality: RuntimeAdoptionRecordQualityReport,
89    pub record_checked: Option<RuntimeAdoptionCheckedRecordReport>,
90}
91
92#[derive(Debug, Clone, PartialEq)]
93pub struct EvaluatorAdviceInput {
94    pub evaluator_id: String,
95    pub subject_kind: String,
96    pub subject_id: String,
97    pub proposed_action: String,
98    pub evidence_refs: Vec<String>,
99    pub counterexample_refs: Vec<String>,
100    pub risk_notes: Vec<String>,
101    pub note: Option<String>,
102}
103
104#[derive(Debug, Clone, PartialEq, Serialize)]
105pub struct EvaluatorAdviceReport {
106    pub writes: bool,
107    pub evaluator_id: String,
108    pub subject_kind: String,
109    pub subject_id: String,
110    pub proposed_action: String,
111    pub recommendation: String,
112    pub lifecycle_authority: bool,
113    pub deterministic_gate_required: bool,
114    pub requires_human_review: bool,
115    pub evidence_refs: Vec<String>,
116    pub counterexample_refs: Vec<String>,
117    pub risk_notes: Vec<String>,
118    pub reasons: Vec<String>,
119    pub adoption_capture: RuntimeAdoptionRecordPlan,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
123pub struct CardContextDefaultProposalReport {
124    pub writes: bool,
125    pub candidate: String,
126    pub proposal_ready: bool,
127    pub decision: String,
128    pub readiness: Phase3ReadinessReport,
129    pub rollback_criteria: Vec<String>,
130    pub reasons: Vec<String>,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
134pub struct CardContextRollbackControlReport {
135    pub writes: bool,
136    pub candidate: String,
137    pub execute: bool,
138    pub rollback_required: bool,
139    pub applied: bool,
140    pub include_cards_default_before: bool,
141    pub include_cards_default_after: bool,
142    pub review: RuntimeAdoptionReviewReport,
143    pub reasons: Vec<String>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
147pub struct RuntimeAdoptionReviewFilters {
148    pub track: Option<String>,
149    pub feature: Option<String>,
150    pub signal: Option<String>,
151    pub limit: usize,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
155pub struct RuntimeAdoptionSignalCounts {
156    pub total: usize,
157    pub used: usize,
158    pub accepted: usize,
159    pub rejected: usize,
160    pub misses: usize,
161    pub rollbacks: usize,
162    pub contradictions: usize,
163    pub neutral: usize,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
167pub struct RuntimeAdoptionFeatureReview {
168    pub feature: String,
169    pub stats: RuntimeAdoptionSignalCounts,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
173pub struct RuntimeAdoptionReviewReport {
174    pub writes: bool,
175    pub filters: RuntimeAdoptionReviewFilters,
176    pub total: usize,
177    pub stats: RuntimeAdoptionSignalCounts,
178    pub features: Vec<RuntimeAdoptionFeatureReview>,
179    pub conclusion: String,
180    pub reasons: Vec<String>,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
184pub struct Phase3ReadinessReport {
185    pub writes: bool,
186    pub candidate: String,
187    pub ready: bool,
188    pub decision: String,
189    pub required_track: String,
190    pub required_feature: String,
191    pub review: RuntimeAdoptionReviewReport,
192    pub reasons: Vec<String>,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
196pub struct ResearchEvidenceDrawerPlan {
197    pub drawer_id: String,
198    pub finding_index: usize,
199    pub source_file: String,
200    pub created: bool,
201    pub skipped: bool,
202    #[serde(skip)]
203    pub content: String,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
207pub struct ResearchCandidateInsightPlan {
208    pub statement: String,
209    pub supporting_refs: Vec<String>,
210    pub suggested_command: Vec<String>,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
214pub struct ResearchIngestPlanReport {
215    pub valid: bool,
216    pub writes: bool,
217    pub report_id: String,
218    pub title: String,
219    pub source_count: usize,
220    pub finding_count: usize,
221    pub candidate_insight_count: usize,
222    pub planned_evidence_count: usize,
223    pub created_count: usize,
224    pub skipped_count: usize,
225    pub errors: Vec<String>,
226    pub evidence_drawers: Vec<ResearchEvidenceDrawerPlan>,
227    pub candidate_insights: Vec<ResearchCandidateInsightPlan>,
228}
229
230#[derive(Debug, Clone, PartialEq)]
231pub struct RuntimeAdoptionRecordPlanInput {
232    pub id: Option<String>,
233    pub track: String,
234    pub signal: String,
235    pub feature: String,
236    pub query: Option<String>,
237    pub context_hash: Option<String>,
238    pub card_id: Option<String>,
239    pub evaluator_id: Option<String>,
240    pub research_report_id: Option<String>,
241    pub note: Option<String>,
242    pub metadata: Option<Value>,
243}
244
245#[derive(Debug, Clone, PartialEq)]
246pub struct RuntimeAdoptionCaptureInput {
247    pub id: Option<String>,
248    pub surface: String,
249    pub outcome: String,
250    pub query: Option<String>,
251    pub context_hash: Option<String>,
252    pub card_id: Option<String>,
253    pub evaluator_id: Option<String>,
254    pub research_report_id: Option<String>,
255    pub note: Option<String>,
256    pub metadata: Option<Value>,
257}
258
259pub fn runtime_adoption_guidance() -> RuntimeAdoptionGuidance {
260    RuntimeAdoptionGuidance {
261        version: 1,
262        recording_rule: "record only concrete runtime outcomes, not speculation".to_string(),
263        required_fields: vec![
264            "track".to_string(),
265            "signal".to_string(),
266            "feature".to_string(),
267        ],
268        optional_fields: vec![
269            "query".to_string(),
270            "context_hash".to_string(),
271            "card_id".to_string(),
272            "evaluator_id".to_string(),
273            "research_report_id".to_string(),
274            "note".to_string(),
275            "metadata".to_string(),
276        ],
277        signals: vec![
278            RuntimeAdoptionSignalGuidance {
279                signal: "used".to_string(),
280                when: "record when guidance was actually consumed during a task".to_string(),
281            },
282            RuntimeAdoptionSignalGuidance {
283                signal: "accepted".to_string(),
284                when: "record when the consumed guidance materially helped the outcome".to_string(),
285            },
286            RuntimeAdoptionSignalGuidance {
287                signal: "rejected".to_string(),
288                when: "record when guidance was considered and intentionally not followed"
289                    .to_string(),
290            },
291            RuntimeAdoptionSignalGuidance {
292                signal: "miss".to_string(),
293                when: "record when useful guidance should have appeared but did not".to_string(),
294            },
295            RuntimeAdoptionSignalGuidance {
296                signal: "rollback".to_string(),
297                when: "record when behavior was reverted because guidance degraded the outcome"
298                    .to_string(),
299            },
300            RuntimeAdoptionSignalGuidance {
301                signal: "contradiction".to_string(),
302                when: "record when guidance conflicted with stronger evidence or instructions"
303                    .to_string(),
304            },
305            RuntimeAdoptionSignalGuidance {
306                signal: "neutral".to_string(),
307                when: "record when guidance was consumed but had no clear outcome impact"
308                    .to_string(),
309            },
310        ],
311        tracks: vec![
312            RuntimeAdoptionTrackGuidance {
313                track: "runtime_adoption".to_string(),
314                when: "general agent-runtime behavior evidence".to_string(),
315                feature_examples: vec!["context_pack".to_string(), "skill_selection".to_string()],
316            },
317            RuntimeAdoptionTrackGuidance {
318                track: "card_context".to_string(),
319                when: "card-aware context affected or should have affected behavior".to_string(),
320                feature_examples: vec!["include_cards".to_string()],
321            },
322            RuntimeAdoptionTrackGuidance {
323                track: "card_embedding".to_string(),
324                when: "linked-evidence card retrieval missed statement-level matches".to_string(),
325                feature_examples: vec!["card_statement_recall".to_string()],
326            },
327            RuntimeAdoptionTrackGuidance {
328                track: "evaluator".to_string(),
329                when: "evaluator advice affected or should have affected a lifecycle decision"
330                    .to_string(),
331                feature_examples: vec!["advisory_gate".to_string()],
332            },
333            RuntimeAdoptionTrackGuidance {
334                track: "research_adapter".to_string(),
335                when: "external research report validation or ingestion planning affected behavior"
336                    .to_string(),
337                feature_examples: vec![
338                    "research_validate_plan".to_string(),
339                    "research_ingest_plan".to_string(),
340                ],
341            },
342        ],
343    }
344}
345
346pub fn runtime_adoption_instrumentation_policy() -> RuntimeAdoptionInstrumentationPolicy {
347    RuntimeAdoptionInstrumentationPolicy {
348        version: 1,
349        writes: false,
350        default_mode: "manual_only".to_string(),
351        allowed_modes: vec![
352            RuntimeAdoptionInstrumentationMode {
353                mode: "manual_only".to_string(),
354                description: "agents explicitly call guidance, capture, or record_checked after observing a concrete runtime outcome".to_string(),
355                requires_execute: false,
356                requires_checked_capture: true,
357            },
358            RuntimeAdoptionInstrumentationMode {
359                mode: "opt_in_wrapper".to_string(),
360                description: "a user-enabled wrapper may prepare capture inputs around a tool call, but writes still require execute=true and checked capture quality gates".to_string(),
361                requires_execute: true,
362                requires_checked_capture: true,
363            },
364        ],
365        forbidden_modes: vec![
366            "implicit_background_capture".to_string(),
367            "silent_event_append".to_string(),
368            "quality_gate_bypass".to_string(),
369        ],
370        requirements: vec![
371            "live instrumentation is opt-in; users must have an opt-out before any capture is written".to_string(),
372            "wrappers must preserve source surface, outcome, query/context identifiers, and relevant metadata".to_string(),
373            "writes must go through existing capture or record_checked quality gates".to_string(),
374        ],
375        rollback_requirements: vec![
376            "rollback criteria must be recorded before any future default-on instrumentation proposal".to_string(),
377            "rollback signals must be captured when instrumentation degrades task behavior or creates noisy evidence".to_string(),
378        ],
379    }
380}
381
382pub fn prepare_runtime_adoption_record(
383    input: RuntimeAdoptionRecordPlanInput,
384) -> RuntimeAdoptionRecordPlan {
385    let mut command = vec![
386        "mempal".to_string(),
387        "phase3".to_string(),
388        "adoption".to_string(),
389        "record".to_string(),
390    ];
391    push_command_arg(&mut command, "--track", &input.track);
392    push_command_arg(&mut command, "--signal", &input.signal);
393    push_command_arg(&mut command, "--feature", &input.feature);
394    if let Some(value) = input.query.as_deref() {
395        push_command_arg(&mut command, "--query", value);
396    }
397    if let Some(value) = input.context_hash.as_deref() {
398        push_command_arg(&mut command, "--context-hash", value);
399    }
400    if let Some(value) = input.card_id.as_deref() {
401        push_command_arg(&mut command, "--card-id", value);
402    }
403    if let Some(value) = input.evaluator_id.as_deref() {
404        push_command_arg(&mut command, "--evaluator-id", value);
405    }
406    if let Some(value) = input.research_report_id.as_deref() {
407        push_command_arg(&mut command, "--research-report-id", value);
408    }
409    if let Some(value) = input.note.as_deref() {
410        push_command_arg(&mut command, "--note", value);
411    }
412    if let Some(value) = input.id.as_deref() {
413        push_command_arg(&mut command, "--id", value);
414    }
415    if let Some(metadata) = input.metadata.as_ref() {
416        push_command_arg(&mut command, "--metadata-json", &metadata.to_string());
417    }
418
419    let mut payload = Map::new();
420    payload.insert("action".to_string(), Value::String("record".to_string()));
421    insert_payload_string(&mut payload, "id", input.id);
422    insert_payload_string(&mut payload, "track", Some(input.track));
423    insert_payload_string(&mut payload, "signal", Some(input.signal));
424    insert_payload_string(&mut payload, "feature", Some(input.feature));
425    insert_payload_string(&mut payload, "query", input.query);
426    insert_payload_string(&mut payload, "context_hash", input.context_hash);
427    insert_payload_string(&mut payload, "card_id", input.card_id);
428    insert_payload_string(&mut payload, "evaluator_id", input.evaluator_id);
429    insert_payload_string(&mut payload, "research_report_id", input.research_report_id);
430    insert_payload_string(&mut payload, "note", input.note);
431    if let Some(metadata) = input.metadata {
432        payload.insert("metadata".to_string(), metadata);
433    }
434
435    RuntimeAdoptionRecordPlan {
436        writes: false,
437        record_command: command,
438        record_payload: Value::Object(payload),
439    }
440}
441
442pub fn capture_runtime_adoption_record_input(
443    input: RuntimeAdoptionCaptureInput,
444) -> Result<RuntimeAdoptionRecordPlanInput, String> {
445    let (track, feature) = capture_surface_mapping(&input.surface)?;
446    let signal = capture_outcome_signal(&input.outcome)?;
447    Ok(RuntimeAdoptionRecordPlanInput {
448        id: input.id,
449        track,
450        signal,
451        feature,
452        query: input.query,
453        context_hash: input.context_hash,
454        card_id: input.card_id,
455        evaluator_id: input.evaluator_id,
456        research_report_id: input.research_report_id,
457        note: input.note,
458        metadata: input.metadata,
459    })
460}
461
462pub fn prepare_runtime_adoption_capture(
463    surface: String,
464    outcome: String,
465    execute: bool,
466    record_input: RuntimeAdoptionRecordPlanInput,
467) -> RuntimeAdoptionCaptureReport {
468    let record_plan = prepare_runtime_adoption_record(record_input.clone());
469    let record_quality = check_runtime_adoption_record(&record_input);
470    RuntimeAdoptionCaptureReport {
471        writes: false,
472        execute,
473        surface,
474        outcome,
475        record_plan,
476        record_quality,
477        record_checked: None,
478    }
479}
480
481pub fn evaluator_advice(input: EvaluatorAdviceInput) -> Result<EvaluatorAdviceReport, String> {
482    if is_blank(&input.evaluator_id) {
483        return Err("evaluator-id is required".to_string());
484    }
485    if is_blank(&input.subject_kind) {
486        return Err("subject-kind is required".to_string());
487    }
488    if is_blank(&input.subject_id) {
489        return Err("subject-id is required".to_string());
490    }
491    if is_blank(&input.proposed_action) {
492        return Err("proposed-action is required".to_string());
493    }
494
495    let evidence_refs = normalize_non_empty(input.evidence_refs);
496    let counterexample_refs = normalize_non_empty(input.counterexample_refs);
497    let risk_notes = normalize_non_empty(input.risk_notes);
498    let subject_kind = input.subject_kind.trim().to_string();
499    let proposed_action = input.proposed_action.trim().to_string();
500    let requires_human_review = subject_kind == "dao_tian" && proposed_action.contains("canonical");
501
502    let mut reasons = Vec::new();
503    reasons.push("evaluator output is advisory-only and has no lifecycle authority".to_string());
504    reasons
505        .push("deterministic promotion gates remain required before lifecycle changes".to_string());
506    if requires_human_review {
507        reasons.push("dao_tian canonicalization requires human review".to_string());
508    }
509
510    let recommendation = if !counterexample_refs.is_empty() || !risk_notes.is_empty() {
511        reasons
512            .push("counterexample refs or risk notes block supportive recommendation".to_string());
513        "do_not_promote"
514    } else if evidence_refs.is_empty() {
515        reasons.push("supporting evidence refs are required before promotion advice".to_string());
516        "needs_evidence"
517    } else if requires_human_review {
518        "requires_human_review"
519    } else {
520        reasons.push(
521            "supporting evidence refs are present and no risk blockers were supplied".to_string(),
522        );
523        "advisory_support"
524    }
525    .to_string();
526
527    let adoption_capture = prepare_runtime_adoption_record(RuntimeAdoptionRecordPlanInput {
528        id: None,
529        track: "evaluator".to_string(),
530        signal: "used".to_string(),
531        feature: "advisory_gate".to_string(),
532        query: Some(format!(
533            "{subject_kind}:{} {proposed_action}",
534            input.subject_id.trim()
535        )),
536        context_hash: None,
537        card_id: None,
538        evaluator_id: Some(input.evaluator_id.trim().to_string()),
539        research_report_id: None,
540        note: input.note,
541        metadata: Some(evaluator_advice_metadata(
542            &subject_kind,
543            input.subject_id.trim(),
544            &proposed_action,
545            &recommendation,
546        )),
547    });
548
549    Ok(EvaluatorAdviceReport {
550        writes: false,
551        evaluator_id: input.evaluator_id.trim().to_string(),
552        subject_kind,
553        subject_id: input.subject_id.trim().to_string(),
554        proposed_action,
555        recommendation,
556        lifecycle_authority: false,
557        deterministic_gate_required: true,
558        requires_human_review,
559        evidence_refs,
560        counterexample_refs,
561        risk_notes,
562        reasons,
563        adoption_capture,
564    })
565}
566
567pub fn card_context_default_proposal(
568    events: &[RuntimeAdoptionEvent],
569    rollback_criteria: Vec<String>,
570) -> CardContextDefaultProposalReport {
571    let readiness = card_context_default_readiness(events);
572    let rollback_criteria = normalize_non_empty(rollback_criteria);
573    let mut reasons = Vec::new();
574    if readiness.ready {
575        reasons.push("card context default readiness threshold satisfied".to_string());
576    } else {
577        reasons.push("card context default readiness threshold not satisfied".to_string());
578    }
579    if rollback_criteria.is_empty() {
580        reasons.push("rollback criteria are required before default-on proposal".to_string());
581    } else {
582        reasons.push("explicit rollback criteria are present".to_string());
583    }
584    let proposal_ready = readiness.ready && !rollback_criteria.is_empty();
585    if proposal_ready {
586        reasons.push("proposal is eligible for a future default-on spec".to_string());
587    }
588
589    CardContextDefaultProposalReport {
590        writes: false,
591        candidate: "card-context".to_string(),
592        proposal_ready,
593        decision: if proposal_ready {
594            "eligible_for_default_on_spec"
595        } else {
596            "keep_opt_in"
597        }
598        .to_string(),
599        readiness,
600        rollback_criteria,
601        reasons,
602    }
603}
604
605pub fn card_context_rollback_control(
606    events: &[RuntimeAdoptionEvent],
607    include_cards_default: bool,
608    execute: bool,
609) -> CardContextRollbackControlReport {
610    let review = review_runtime_adoption_events(
611        events,
612        RuntimeAdoptionReviewFilters {
613            track: Some("card_context".to_string()),
614            feature: Some("include_cards".to_string()),
615            signal: Some("rollback".to_string()),
616            limit: 10_000,
617        },
618    );
619    let rollback_required = review.stats.rollbacks > 0;
620    let applied = execute && rollback_required && include_cards_default;
621    let include_cards_default_after = include_cards_default && !applied;
622    let mut reasons = Vec::new();
623    if rollback_required {
624        reasons.push("rollback evidence exists for card_context/include_cards".to_string());
625    } else {
626        reasons.push("no rollback evidence exists for card_context/include_cards".to_string());
627    }
628    if !execute {
629        reasons.push("execute=false leaves config unchanged".to_string());
630    } else if applied {
631        reasons.push("card context default disabled by rollback control".to_string());
632    } else if !include_cards_default {
633        reasons.push("card context default already disabled".to_string());
634    } else {
635        reasons.push("rollback not required; config unchanged".to_string());
636    }
637
638    CardContextRollbackControlReport {
639        writes: applied,
640        candidate: "card-context".to_string(),
641        execute,
642        rollback_required,
643        applied,
644        include_cards_default_before: include_cards_default,
645        include_cards_default_after,
646        review,
647        reasons,
648    }
649}
650
651pub fn check_runtime_adoption_record(
652    input: &RuntimeAdoptionRecordPlanInput,
653) -> RuntimeAdoptionRecordQualityReport {
654    let mut errors = Vec::new();
655    let mut warnings = Vec::new();
656
657    if is_blank(&input.feature) {
658        errors.push("feature must not be empty".to_string());
659    }
660
661    if requires_outcome_context(&input.signal)
662        && input.query.as_deref().is_none_or(is_blank)
663        && input.note.as_deref().is_none_or(is_blank)
664    {
665        warnings.push(
666            "missing concrete outcome context: provide note or query for this signal".to_string(),
667        );
668    }
669
670    match input.track.as_str() {
671        "card_context" | "card_embedding" if input.card_id.as_deref().is_none_or(is_blank) => {
672            warnings.push("missing card_id for card-related adoption evidence".to_string());
673        }
674        "evaluator" if input.evaluator_id.as_deref().is_none_or(is_blank) => {
675            warnings.push("missing evaluator_id for evaluator adoption evidence".to_string());
676        }
677        "research_adapter" if input.research_report_id.as_deref().is_none_or(is_blank) => {
678            warnings.push(
679                "missing research_report_id for research adapter adoption evidence".to_string(),
680            );
681        }
682        _ => {}
683    }
684
685    let quality = if !errors.is_empty() {
686        "invalid"
687    } else if !warnings.is_empty() {
688        "warning"
689    } else {
690        "ready"
691    }
692    .to_string();
693
694    RuntimeAdoptionRecordQualityReport {
695        writes: false,
696        valid: errors.is_empty(),
697        quality,
698        errors,
699        warnings,
700    }
701}
702
703fn normalize_non_empty(values: Vec<String>) -> Vec<String> {
704    values
705        .into_iter()
706        .map(|value| value.trim().to_string())
707        .filter(|value| !value.is_empty())
708        .collect()
709}
710
711fn evaluator_advice_metadata(
712    subject_kind: &str,
713    subject_id: &str,
714    proposed_action: &str,
715    recommendation: &str,
716) -> Value {
717    let mut metadata = Map::new();
718    metadata.insert(
719        "subject_kind".to_string(),
720        Value::String(subject_kind.to_string()),
721    );
722    metadata.insert(
723        "subject_id".to_string(),
724        Value::String(subject_id.to_string()),
725    );
726    metadata.insert(
727        "proposed_action".to_string(),
728        Value::String(proposed_action.to_string()),
729    );
730    metadata.insert(
731        "recommendation".to_string(),
732        Value::String(recommendation.to_string()),
733    );
734    Value::Object(metadata)
735}
736
737fn capture_surface_mapping(surface: &str) -> Result<(String, String), String> {
738    match surface.trim() {
739        "card-context" | "card_context" => {
740            Ok(("card_context".to_string(), "include_cards".to_string()))
741        }
742        "card-embedding" | "card_embedding" => Ok((
743            "card_embedding".to_string(),
744            "card_statement_recall".to_string(),
745        )),
746        "research-adapter" | "research_adapter" => Ok((
747            "research_adapter".to_string(),
748            "research_ingest_plan".to_string(),
749        )),
750        "evaluator" => Ok(("evaluator".to_string(), "advisory_gate".to_string())),
751        "runtime-context" | "runtime_context" => {
752            Ok(("runtime_adoption".to_string(), "context_pack".to_string()))
753        }
754        "skill-selection" | "skill_selection" => Ok((
755            "runtime_adoption".to_string(),
756            "skill_selection".to_string(),
757        )),
758        other => Err(format!("unsupported adoption capture surface: {other}")),
759    }
760}
761
762fn capture_outcome_signal(outcome: &str) -> Result<String, String> {
763    match outcome.trim() {
764        "used" | "accepted" | "rejected" | "miss" | "rollback" | "contradiction" | "neutral" => {
765            Ok(outcome.trim().to_string())
766        }
767        other => Err(format!("unsupported adoption capture outcome: {other}")),
768    }
769}
770
771pub fn should_write_checked_record(
772    quality: &RuntimeAdoptionRecordQualityReport,
773    allow_warnings: bool,
774) -> bool {
775    quality.valid
776        && (quality.quality == "ready" || (allow_warnings && quality.quality == "warning"))
777}
778
779pub fn review_runtime_adoption_events(
780    events: &[RuntimeAdoptionEvent],
781    filters: RuntimeAdoptionReviewFilters,
782) -> RuntimeAdoptionReviewReport {
783    let filtered_events = events
784        .iter()
785        .filter(|event| {
786            filters
787                .signal
788                .as_deref()
789                .is_none_or(|signal| runtime_adoption_signal_slug(&event.signal) == signal)
790        })
791        .collect::<Vec<_>>();
792
793    let stats = RuntimeAdoptionSignalCounts::from_events(filtered_events.iter().copied());
794    let mut by_feature = BTreeMap::<String, Vec<&RuntimeAdoptionEvent>>::new();
795    for event in &filtered_events {
796        by_feature
797            .entry(event.feature.clone())
798            .or_default()
799            .push(event);
800    }
801    let features = by_feature
802        .into_iter()
803        .map(|(feature, events)| RuntimeAdoptionFeatureReview {
804            feature,
805            stats: RuntimeAdoptionSignalCounts::from_events(events),
806        })
807        .collect::<Vec<_>>();
808
809    let (conclusion, reasons) = review_conclusion(&stats);
810
811    RuntimeAdoptionReviewReport {
812        writes: false,
813        filters,
814        total: stats.total,
815        stats,
816        features,
817        conclusion,
818        reasons,
819    }
820}
821
822impl RuntimeAdoptionSignalCounts {
823    fn from_events<'a>(events: impl IntoIterator<Item = &'a RuntimeAdoptionEvent>) -> Self {
824        let mut stats = Self::default();
825        for event in events {
826            stats.total += 1;
827            match event.signal {
828                RuntimeAdoptionSignal::Used => stats.used += 1,
829                RuntimeAdoptionSignal::Accepted => stats.accepted += 1,
830                RuntimeAdoptionSignal::Rejected => stats.rejected += 1,
831                RuntimeAdoptionSignal::Miss => stats.misses += 1,
832                RuntimeAdoptionSignal::Rollback => stats.rollbacks += 1,
833                RuntimeAdoptionSignal::Contradiction => stats.contradictions += 1,
834                RuntimeAdoptionSignal::Neutral => stats.neutral += 1,
835            }
836        }
837        stats
838    }
839}
840
841fn review_conclusion(stats: &RuntimeAdoptionSignalCounts) -> (String, Vec<String>) {
842    if stats.total == 0 {
843        return (
844            "no_evidence".to_string(),
845            vec!["no runtime adoption events match the requested filters".to_string()],
846        );
847    }
848    if stats.rollbacks > 0 {
849        return (
850            "rollback_risk".to_string(),
851            vec!["rollback evidence exists for this adoption surface".to_string()],
852        );
853    }
854    if stats.accepted > 0 && stats.accepted >= stats.rejected + stats.misses + stats.contradictions
855    {
856        return (
857            "positive".to_string(),
858            vec!["accepted evidence is at least as strong as negative evidence".to_string()],
859        );
860    }
861    (
862        "mixed".to_string(),
863        vec!["evidence is present but not clearly positive".to_string()],
864    )
865}
866
867pub fn card_context_default_readiness(events: &[RuntimeAdoptionEvent]) -> Phase3ReadinessReport {
868    let review = review_runtime_adoption_events(
869        events,
870        RuntimeAdoptionReviewFilters {
871            track: Some("card_context".to_string()),
872            feature: Some("include_cards".to_string()),
873            signal: None,
874            limit: 10_000,
875        },
876    );
877    let stats = &review.stats;
878    let mut reasons = Vec::new();
879    if stats.accepted < 3 {
880        reasons.push("insufficient accepted evidence for include_cards default".to_string());
881    }
882    if stats.rollbacks > 0 {
883        reasons.push("rollback evidence blocks card context default readiness".to_string());
884    }
885    if stats.contradictions > 0 {
886        reasons.push("contradiction evidence requires review before defaulting".to_string());
887    }
888    if stats.accepted < stats.rejected + stats.misses {
889        reasons.push("negative evidence outweighs accepted evidence".to_string());
890    }
891
892    let ready = reasons.is_empty();
893    if ready {
894        reasons.push("card context default readiness threshold satisfied".to_string());
895    }
896
897    Phase3ReadinessReport {
898        writes: false,
899        candidate: "card-context-default".to_string(),
900        ready,
901        decision: if ready {
902            "eligible_for_future_default_spec"
903        } else {
904            "keep_opt_in"
905        }
906        .to_string(),
907        required_track: "card_context".to_string(),
908        required_feature: "include_cards".to_string(),
909        review,
910        reasons,
911    }
912}
913
914pub fn build_research_ingest_plan_from_value(value: &Value) -> ResearchIngestPlanReport {
915    let mut errors = Vec::new();
916    let report_id = required_json_string(value, "report_id", &mut errors);
917    let title = required_json_string(value, "title", &mut errors);
918    let source_count = value
919        .get("sources")
920        .and_then(Value::as_array)
921        .map_or(0, Vec::len);
922    if source_count == 0 {
923        errors.push("sources must contain at least one item".to_string());
924    }
925    let findings = value
926        .get("findings")
927        .and_then(Value::as_array)
928        .cloned()
929        .unwrap_or_default();
930    if findings.is_empty() {
931        errors.push("findings must contain at least one item".to_string());
932    }
933    let candidate_values = value
934        .get("candidate_insights")
935        .and_then(Value::as_array)
936        .cloned()
937        .unwrap_or_default();
938
939    let evidence_drawers = findings
940        .iter()
941        .enumerate()
942        .map(|(index, finding)| {
943            let content = research_evidence_content_from_value(
944                report_id.as_str(),
945                title.as_str(),
946                value.get("sources"),
947                finding,
948                index,
949            );
950            let drawer_id = research_evidence_drawer_id(&content);
951            ResearchEvidenceDrawerPlan {
952                drawer_id,
953                finding_index: index,
954                source_file: format!("research://{}#finding-{index}", report_id),
955                created: false,
956                skipped: false,
957                content,
958            }
959        })
960        .collect::<Vec<_>>();
961    let supporting_refs = evidence_drawers
962        .iter()
963        .map(|plan| plan.drawer_id.clone())
964        .collect::<Vec<_>>();
965    let candidate_insights = candidate_values
966        .iter()
967        .filter_map(|candidate| candidate_statement(candidate).map(str::to_string))
968        .map(|statement| ResearchCandidateInsightPlan {
969            suggested_command: research_distill_command(&statement, &supporting_refs),
970            statement,
971            supporting_refs: supporting_refs.clone(),
972        })
973        .collect::<Vec<_>>();
974
975    ResearchIngestPlanReport {
976        valid: errors.is_empty(),
977        writes: false,
978        report_id,
979        title,
980        source_count,
981        finding_count: findings.len(),
982        candidate_insight_count: candidate_values.len(),
983        planned_evidence_count: evidence_drawers.len(),
984        created_count: 0,
985        skipped_count: 0,
986        errors,
987        evidence_drawers,
988        candidate_insights,
989    }
990}
991
992fn research_evidence_content_from_value(
993    report_id: &str,
994    title: &str,
995    sources: Option<&Value>,
996    finding: &Value,
997    finding_index: usize,
998) -> String {
999    let summary = finding_summary(finding);
1000    let sources = sources
1001        .map(|value| serde_json::to_string_pretty(value).unwrap_or_else(|_| "[]".to_string()))
1002        .unwrap_or_else(|| "[]".to_string());
1003    let raw_finding = serde_json::to_string_pretty(finding).unwrap_or_else(|_| finding.to_string());
1004    format!(
1005        "# Research finding: {title}\n\nreport_id: {report_id}\nfinding_index: {finding_index}\n\nsummary: {summary}\n\nsources:\n```json\n{sources}\n```\n\nraw_finding:\n```json\n{raw_finding}\n```\n"
1006    )
1007}
1008
1009fn finding_summary(finding: &Value) -> String {
1010    if let Some(raw) = finding.as_str() {
1011        return raw.trim().to_string();
1012    }
1013    for field in ["summary", "statement", "content", "text"] {
1014        if let Some(raw) = finding.get(field).and_then(Value::as_str)
1015            && !raw.trim().is_empty()
1016        {
1017            return raw.trim().to_string();
1018        }
1019    }
1020    serde_json::to_string(finding).unwrap_or_else(|_| finding.to_string())
1021}
1022
1023fn candidate_statement(candidate: &Value) -> Option<&str> {
1024    if let Some(raw) = candidate.as_str()
1025        && !raw.trim().is_empty()
1026    {
1027        return Some(raw.trim());
1028    }
1029    for field in ["statement", "summary", "content", "text"] {
1030        if let Some(raw) = candidate.get(field).and_then(Value::as_str)
1031            && !raw.trim().is_empty()
1032        {
1033            return Some(raw.trim());
1034        }
1035    }
1036    None
1037}
1038
1039fn research_evidence_drawer_id(content: &str) -> String {
1040    let memory_kind = MemoryKind::Evidence;
1041    let domain = MemoryDomain::Project;
1042    let anchor_kind = AnchorKind::Repo;
1043    let provenance = Provenance::Research;
1044    let empty_refs: &[String] = &[];
1045    build_bootstrap_drawer_id_from_parts(
1046        "mempal",
1047        Some("research"),
1048        content,
1049        BootstrapIdentityParts {
1050            memory_kind: &memory_kind,
1051            domain: &domain,
1052            field: "research",
1053            anchor_kind: &anchor_kind,
1054            anchor_id: anchor::LEGACY_REPO_ANCHOR_ID,
1055            parent_anchor_id: None,
1056            provenance: Some(&provenance),
1057            statement: None,
1058            tier: None,
1059            status: None,
1060            supporting_refs: empty_refs,
1061            counterexample_refs: empty_refs,
1062            teaching_refs: empty_refs,
1063            verification_refs: empty_refs,
1064            scope_constraints: None,
1065            trigger_hints: None,
1066        },
1067    )
1068}
1069
1070fn research_distill_command(statement: &str, supporting_refs: &[String]) -> Vec<String> {
1071    let mut command = vec![
1072        "mempal".to_string(),
1073        "knowledge".to_string(),
1074        "distill".to_string(),
1075        "--tier".to_string(),
1076        "qi".to_string(),
1077        "--statement".to_string(),
1078        statement.to_string(),
1079        "--content".to_string(),
1080        statement.to_string(),
1081        "--field".to_string(),
1082        "research".to_string(),
1083    ];
1084    for supporting_ref in supporting_refs {
1085        push_command_arg(&mut command, "--supporting-ref", supporting_ref);
1086    }
1087    command
1088}
1089
1090fn required_json_string(value: &Value, field: &'static str, errors: &mut Vec<String>) -> String {
1091    match value.get(field).and_then(Value::as_str) {
1092        Some(raw) if !raw.trim().is_empty() => raw.trim().to_string(),
1093        _ => {
1094            errors.push(format!("{field} is required"));
1095            String::new()
1096        }
1097    }
1098}
1099
1100fn requires_outcome_context(signal: &str) -> bool {
1101    matches!(
1102        signal,
1103        "accepted" | "rejected" | "miss" | "rollback" | "contradiction"
1104    )
1105}
1106
1107fn is_blank(value: &str) -> bool {
1108    value.trim().is_empty()
1109}
1110
1111fn runtime_adoption_signal_slug(signal: &RuntimeAdoptionSignal) -> &'static str {
1112    match signal {
1113        RuntimeAdoptionSignal::Used => "used",
1114        RuntimeAdoptionSignal::Accepted => "accepted",
1115        RuntimeAdoptionSignal::Rejected => "rejected",
1116        RuntimeAdoptionSignal::Miss => "miss",
1117        RuntimeAdoptionSignal::Rollback => "rollback",
1118        RuntimeAdoptionSignal::Contradiction => "contradiction",
1119        RuntimeAdoptionSignal::Neutral => "neutral",
1120    }
1121}
1122
1123fn push_command_arg(command: &mut Vec<String>, name: &str, value: &str) {
1124    command.push(name.to_string());
1125    command.push(value.to_string());
1126}
1127
1128fn insert_payload_string(payload: &mut Map<String, Value>, key: &str, value: Option<String>) {
1129    if let Some(value) = value {
1130        payload.insert(key.to_string(), Value::String(value));
1131    }
1132}