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}