Update an intent's fields.
(
&self,
intent_id: &str,
options: IntentUpdateOptions,
)
| 644 | |
| 645 | /// Update an intent's fields. |
| 646 | pub fn vault_intent_update( |
| 647 | &self, |
| 648 | intent_id: &str, |
| 649 | options: IntentUpdateOptions, |
| 650 | ) -> Result<IntentInfo, RepositoryError> { |
| 651 | let full_id = self.normalize_intent_id(intent_id)?; |
| 652 | let intent_file = |
| 653 | self.find_intent_path(&full_id)? |
| 654 | .ok_or_else(|| RepositoryError::InvalidOperation { |
| 655 | message: format!("Intent '{}' not found", full_id), |
| 656 | })?; |
| 657 | |
| 658 | // Read from disk first so we pick up any user/agent edits to the |
| 659 | // markdown body. If the file doesn't exist on disk, fall back to |
| 660 | // the redb entry. |
| 661 | let disk_path = self.vault_dir().join(&intent_file); |
| 662 | let (content_bytes, frontmatter_json) = if disk_path.exists() { |
| 663 | let file_content = std::fs::read_to_string(&disk_path)?; |
| 664 | let (fm_json, body) = crate::repository::vault::parse_vault_frontmatter(&file_content); |
| 665 | (body.into_bytes(), fm_json) |
| 666 | } else { |
| 667 | let entry = self.vault_retrieve(&intent_file)?.ok_or_else(|| { |
| 668 | RepositoryError::InvalidOperation { |
| 669 | message: format!("Intent '{}' not found", full_id), |
| 670 | } |
| 671 | })?; |
| 672 | (entry.content_bytes.clone(), entry.frontmatter_json.clone()) |
| 673 | }; |
| 674 | |
| 675 | // Update frontmatter |
| 676 | let mut fm: serde_json::Map<String, serde_json::Value> = |
| 677 | serde_json::from_str(&frontmatter_json).unwrap_or_default(); |
| 678 | |
| 679 | // The status as stored on disk BEFORE this update mutates anything. The |
| 680 | // T5b lapse check keys off this: a substance edit on an already-`done` |
| 681 | // intent (that is not itself re-asserting a status) can lapse the grant. |
| 682 | let prior_status = fm |
| 683 | .get("status") |
| 684 | .and_then(|v| v.as_str()) |
| 685 | .map(str::to_string); |
| 686 | // The authoring view is frozen in frontmatter at create time. |
| 687 | // TODO(T5b-follow-up): scope-gating uses this frozen authoring `view` |
| 688 | // (recon gaps #3/#4). A Draft-authored intent whose changes were already |
| 689 | // inserted into a Shared view still reads its original Draft view here; |
| 690 | // precise "Shared doneness is immutable" needs the visibility walk. |
| 691 | let fm_view_name = fm.get("view").and_then(|v| v.as_str()).map(str::to_string); |
| 692 | |
| 693 | if options.content.is_some() && !options.force { |
| 694 | let manifest = self.vault_manifest()?; |
| 695 | let summary = manifest.intents.get(&full_id); |
| 696 | let current_status = summary |
| 697 | .map(|intent| intent.status.as_str()) |
| 698 | .or_else(|| fm.get("status").and_then(|value| value.as_str())) |
| 699 | .unwrap_or("unknown"); |
| 700 | let has_linked_goal = summary.is_some_and(|intent| intent.goals > 0) |
| 701 | || manifest.goals.values().any(|goal| { |
| 702 | goal.intent |
| 703 | .as_deref() |