Phase 45 (first slice): DocRef + doc_refs field on PlaybookEntry
Phase J keeps asking for: playbooks know which external docs they
used, get flagged when those docs drift. This commit ships the data
model; context7 bridge + drift check endpoints land in follow-ups.
Added to crates/vectord/src/playbook_memory.rs:
- pub struct DocRef { tool, version_seen, snippet_hash, source_url,
seen_at } — one external doc reference
- PlaybookEntry.doc_refs: Vec<DocRef> — empty on legacy entries,
serde default ensures pre-Phase-45 persisted state loads cleanly
- PlaybookEntry.doc_drift_flagged_at: Option<String> — set by the
(future) drift-check code when context7 reports newer version
- PlaybookEntry.doc_drift_reviewed_at: Option<String> — set by
human via /resolve endpoint after reviewing the diagnosis
- impl Default for PlaybookEntry — collapses most test-helper
constructors from 17 explicit fields to 6-9 fields +
..Default::default()
Updated SeedPlaybookRequest + RevisePlaybookRequest (service.rs) to
accept optional doc_refs: the seed/revise endpoints already take the
field, downstream drift detection (Phase 45.2) consumes it.
Docs: docs/CONTROL_PLANE_PRD.md gains full Phase 45 spec with gate
criteria, non-goals, and risk notes.
Tests: 51/51 vectord lib tests green (same count as before, field
additions are backward-compat).
Memory: project_doc_drift_vision.md written so this keeps coming
back to the front of mind.
Next slices (same phase): context7 HTTP bridge in mcp-server,
/vectors/playbook_memory/doc_drift/check/{id} endpoint, overview-
model drift synthesis writing to data/_kb/doc_drift_corrections.jsonl,
boost exclusion for flagged+unreviewed entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
75a0f424ef
commit
2a4b81bf48
@@ -134,10 +134,87 @@ pub struct PlaybookEntry {
|
||||
/// full version chain.
|
||||
#[serde(default)]
|
||||
pub superseded_by: Option<String>,
|
||||
/// Phase 45 — external documentation references captured at seal
|
||||
/// time. One entry per tool/library the procedure consulted.
|
||||
/// Drives drift detection: when context7 reports a newer version
|
||||
/// for any entry here than what's in `version_seen`, the playbook
|
||||
/// is `doc_drift_flagged_at` and excluded from boost until human
|
||||
/// review clears it. Legacy entries (pre-Phase-45) load with an
|
||||
/// empty vec — they simply never drift-flag, same as entries
|
||||
/// without a `schema_fingerprint` in Phase 25.
|
||||
#[serde(default)]
|
||||
pub doc_refs: Vec<DocRef>,
|
||||
/// Phase 45 — set by `flag_doc_drift()` when one or more
|
||||
/// `doc_refs` entries have a newer version available than
|
||||
/// `version_seen`. Flagged entries are excluded from boost until
|
||||
/// `doc_drift_reviewed_at` is set via the /resolve endpoint.
|
||||
#[serde(default)]
|
||||
pub doc_drift_flagged_at: Option<String>,
|
||||
/// Phase 45 — set by human operator via
|
||||
/// `/vectors/playbook_memory/doc_drift/resolve/{id}` after
|
||||
/// reviewing the drift diagnosis. Either re-admits the entry to
|
||||
/// boost (if still applicable) or pairs with `retired_at` /
|
||||
/// `superseded_by` if the procedure changed.
|
||||
#[serde(default)]
|
||||
pub doc_drift_reviewed_at: Option<String>,
|
||||
}
|
||||
|
||||
fn default_version() -> u32 { 1 }
|
||||
|
||||
/// Phase 45 — one external doc reference. Recorded at seal time so
|
||||
/// drift detection knows what version was consulted. `snippet_hash`
|
||||
/// lets us detect "same version, different passage" when a library
|
||||
/// patches docs without bumping the version number.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct DocRef {
|
||||
/// Canonical tool/library name as context7 knows it, e.g.
|
||||
/// "docker", "terraform", "react", "next.js". Case-insensitive
|
||||
/// on compare.
|
||||
pub tool: String,
|
||||
/// Version string exactly as seen at seal time. Context7 typically
|
||||
/// returns semver-like; we store raw string to avoid parsing
|
||||
/// ambiguity ("latest", "next", "canary" are all valid).
|
||||
pub version_seen: String,
|
||||
/// Optional hash of the specific doc passage the procedure
|
||||
/// referenced. Useful when version hasn't bumped but content
|
||||
/// rewrote.
|
||||
#[serde(default)]
|
||||
pub snippet_hash: Option<String>,
|
||||
/// Optional direct URL back to the doc (context7 can resolve
|
||||
/// tool+version → URL, so this is cache not source-of-truth).
|
||||
#[serde(default)]
|
||||
pub source_url: Option<String>,
|
||||
/// When this reference was captured. RFC3339.
|
||||
pub seen_at: String,
|
||||
}
|
||||
|
||||
impl Default for PlaybookEntry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
playbook_id: String::new(),
|
||||
operation: String::new(),
|
||||
approach: String::new(),
|
||||
context: String::new(),
|
||||
timestamp: String::new(),
|
||||
endorsed_names: Vec::new(),
|
||||
city: None,
|
||||
state: None,
|
||||
embedding: None,
|
||||
schema_fingerprint: None,
|
||||
valid_until: None,
|
||||
retired_at: None,
|
||||
retirement_reason: None,
|
||||
version: 1,
|
||||
parent_id: None,
|
||||
superseded_at: None,
|
||||
superseded_by: None,
|
||||
doc_refs: Vec::new(),
|
||||
doc_drift_flagged_at: None,
|
||||
doc_drift_reviewed_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A recorded failure — worker who didn't deliver on a contract.
|
||||
/// Tracked per (city, state, name) so a single worker's failures on
|
||||
/// Toledo Welder contracts don't penalize the same name in Chicago.
|
||||
@@ -1201,19 +1278,10 @@ pub async fn rebuild(
|
||||
endorsed_names: names,
|
||||
city,
|
||||
state,
|
||||
embedding: None,
|
||||
// Rebuild doesn't know fingerprints; historical entries
|
||||
// get no retirement signal until a seed with a
|
||||
// fingerprint supersedes them or the operator calls
|
||||
// /retire manually.
|
||||
schema_fingerprint: None,
|
||||
valid_until: None,
|
||||
retired_at: None,
|
||||
retirement_reason: None,
|
||||
version: 1,
|
||||
parent_id: None,
|
||||
superseded_at: None,
|
||||
superseded_by: None,
|
||||
// Rebuild doesn't know fingerprints or doc_refs;
|
||||
// historical entries get no drift signal until a seed
|
||||
// supersedes them or /retire is called manually.
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -1386,20 +1454,12 @@ mod tests {
|
||||
playbook_id: format!("pb-{i}"),
|
||||
operation: "fill: Welder x1 in Toledo, OH".into(),
|
||||
approach: "transfer".into(),
|
||||
context: "".into(),
|
||||
timestamp: "2026-04-20".into(),
|
||||
endorsed_names: vec!["Deborah Powell".into()],
|
||||
city: Some("Toledo".into()),
|
||||
state: Some("OH".into()),
|
||||
embedding: Some(vec![1.0, 0.0, 0.0]),
|
||||
schema_fingerprint: None,
|
||||
valid_until: None,
|
||||
retired_at: None,
|
||||
retirement_reason: None,
|
||||
version: 1,
|
||||
parent_id: None,
|
||||
superseded_at: None,
|
||||
superseded_by: None,
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
@@ -1431,12 +1491,7 @@ mod validity_window_tests {
|
||||
embedding: Some(vec![1.0, 0.0, 0.0]),
|
||||
schema_fingerprint: fingerprint,
|
||||
valid_until,
|
||||
retired_at: None,
|
||||
retirement_reason: None,
|
||||
version: 1,
|
||||
parent_id: None,
|
||||
superseded_at: None,
|
||||
superseded_by: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1536,14 +1591,7 @@ mod upsert_tests {
|
||||
city: Some("Nashville".into()),
|
||||
state: Some("TN".into()),
|
||||
embedding: Some(vec![1.0, 0.0, 0.0]),
|
||||
schema_fingerprint: None,
|
||||
valid_until: None,
|
||||
retired_at: None,
|
||||
retirement_reason: None,
|
||||
version: 1,
|
||||
parent_id: None,
|
||||
superseded_at: None,
|
||||
superseded_by: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1632,14 +1680,7 @@ mod version_tests {
|
||||
city: Some(city.into()),
|
||||
state: Some(state.into()),
|
||||
embedding: Some(vec![1.0, 0.0, 0.0]),
|
||||
schema_fingerprint: None,
|
||||
valid_until: None,
|
||||
retired_at: None,
|
||||
retirement_reason: None,
|
||||
version: 1,
|
||||
parent_id: None,
|
||||
superseded_at: None,
|
||||
superseded_by: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user