1use std::path::Path;
5
6use crate::entity::EntityId;
7use crate::ops::WarningHint;
8use crate::provenance::{Provenance, ProvenanceKind};
9use crate::vcs::{Actor, ClientId, CommitContext};
10use crate::workspace::MountCapability;
11
12use super::super::{DeleteEntityArgs, DeleteEntityOutcome, Engine, EngineError, ReferrerInfo};
13use super::{gc_orphan_stubs, make_stub};
14
15#[derive(Debug, Clone)]
23pub struct DeleteReferrers {
24 pub write_referrers: Vec<ReferrerInfo>,
25 pub readonly_referrers: Vec<EntityId>,
26}
27
28impl DeleteReferrers {
29 pub fn would_refuse(&self) -> bool {
31 !self.write_referrers.is_empty()
32 }
33}
34
35impl Engine {
36 pub fn classify_delete_referrers(&self, id: &EntityId) -> DeleteReferrers {
48 let mut write_referrers: Vec<ReferrerInfo> = Vec::new();
49 let mut readonly_referrers: Vec<EntityId> = Vec::new();
50 for edge in self.store.incoming(id) {
51 let from_mem = edge.from.mem().to_string();
52 let cap = self
53 .mounts
54 .iter()
55 .find(|m| m.mount.mem == from_mem)
56 .map(|m| m.mount.capability)
57 .unwrap_or(MountCapability::Write);
58 match cap {
59 MountCapability::Write => {
60 let from_id = edge.from.to_string();
61 if let Some(existing) =
62 write_referrers.iter_mut().find(|r| r.from_id == from_id)
63 {
64 if !existing.rel_types.contains(&edge.rel_type) {
65 existing.rel_types.push(edge.rel_type.clone());
66 }
67 } else {
68 write_referrers.push(ReferrerInfo {
69 from_id,
70 rel_types: vec![edge.rel_type.clone()],
71 mem: from_mem,
72 });
73 }
74 }
75 MountCapability::ReadOnly => readonly_referrers.push(edge.from.clone()),
76 }
77 }
78 DeleteReferrers {
79 write_referrers,
80 readonly_referrers,
81 }
82 }
83
84 pub fn delete_entity(
106 &mut self,
107 args: DeleteEntityArgs,
108 actor: Actor,
109 client: Option<&ClientId>,
110 note: Option<&str>,
111 ) -> Result<DeleteEntityOutcome, EngineError> {
112 let id = &args.id;
113 let mem = id.mem().to_string();
114
115 let mount_idx = self
116 .mounts
117 .iter()
118 .position(|m| m.mount.mem == mem)
119 .ok_or_else(|| self.unknown_mem_error(&mem))?;
120 if self.mounts[mount_idx].mount.capability != MountCapability::Write {
121 return Err(EngineError::ReadOnlyMount(mem));
122 }
123
124 self.ensure_mems_loaded(None);
132
133 let mut drift_warnings = self.reload_if_stale(Some(&mem));
138
139 let entity = self
140 .store
141 .get(id)
142 .ok_or_else(|| EngineError::NotFound { id: id.to_string() })?;
143
144 if let Some(expected) = args.expected_hash.as_deref()
145 && entity.content_hash != expected
146 {
147 return Err(EngineError::HashMismatch {
155 id: id.to_string(),
156 current: entity.content_hash.clone(),
157 is_stub: entity.stub,
158 });
159 }
160
161 let file_path = entity.file_path.clone();
162 let entity_is_stub = entity.stub;
163
164 let DeleteReferrers {
170 write_referrers,
171 readonly_referrers,
172 } = self.classify_delete_referrers(id);
173 if !write_referrers.is_empty() {
174 return Err(EngineError::HasIncomingRefs {
175 id: id.to_string(),
176 referrers: write_referrers,
177 });
178 }
179
180 let demote_to_stub = !readonly_referrers.is_empty();
181 let removed_incoming: Vec<String> =
182 readonly_referrers.iter().map(|id| id.to_string()).collect();
183
184 let relations_removed = if demote_to_stub {
190 self.store.outgoing(id).len()
191 } else {
192 self.store.outgoing(id).len() + self.store.incoming(id).len()
193 };
194
195 let backend = self.mounts[mount_idx].backend.as_ref();
204 let write_id = if entity_is_stub {
205 String::new()
206 } else {
207 backend.delete_entity(Path::new(&file_path))?;
208 super::stage_anchors_removal(backend, id)?;
212 let commit_subject = format!("memstead: delete {id}");
213 let ctx = CommitContext {
214 actor,
215 client: client.cloned(),
216 tool: Some("delete_entity"),
217 note: note.map(String::from),
218 role: self.current_role,
219 logical_operation_id: None,
220 entity_ids: None,
221 };
222 backend.commit(&commit_subject, &ctx)?
223 };
224
225 backend.append_provenance(
226 &Provenance::new(
227 std::time::SystemTime::now(),
228 ProvenanceKind::Delete,
229 Some(id.to_string()),
230 actor,
231 client.cloned(),
232 note.map(String::from),
233 )
234 .with_role(self.current_role),
235 )?;
236
237 let mut stamp_warnings: Vec<WarningHint> = Vec::new();
238 if !write_id.is_empty() {
239 self.record_self_write(mount_idx, &write_id);
240 stamp_warnings = self.stamp_mutation_versions(mount_idx);
241 }
242
243 let mut warnings: Vec<WarningHint> = Vec::new();
244 warnings.extend(std::mem::take(&mut stamp_warnings));
245 warnings.append(&mut drift_warnings);
247 let orphan_stubs_removed = if demote_to_stub {
248 self.store.remove_edges_from(id);
255 self.store.upsert(
256 id.clone(),
257 make_stub(
258 id,
259 crate::entity::StubKind::Residual {
260 since_commit: write_id.clone(),
261 readonly_referrers: readonly_referrers.clone(),
262 },
263 ),
264 );
265 warnings.push(WarningHint::ResidualStubForReadOnlyReferrers {
266 id: id.clone(),
267 referrers: readonly_referrers,
268 });
269 Vec::new()
275 } else {
276 self.store.remove(id);
277 gc_orphan_stubs(&mut self.store)
281 };
282
283 self.invalidate_communities();
284 self.maintain_search_indexes(std::slice::from_ref(id));
289
290 if !write_id.is_empty()
295 && let Some(w) = self.note_missing_warning("delete_entity", note)
296 {
297 warnings.push(w);
298 }
299
300 Ok(DeleteEntityOutcome {
301 id: id.clone(),
302 file_path,
303 removed_incoming,
304 relations_removed,
305 write_id,
306 orphan_stubs_removed,
307 warnings,
308 })
309 }
310
311 pub fn delete_entity_with_ctx(
315 &mut self,
316 id: &EntityId,
317 expected_hash: &str,
318 ctx: &CommitContext<'_>,
319 ) -> Result<DeleteEntityOutcome, EngineError> {
320 let args = DeleteEntityArgs {
321 id: id.clone(),
322 expected_hash: Some(expected_hash.to_string()),
323 };
324 self.delete_entity(args, ctx.actor, ctx.client.as_ref(), ctx.note.as_deref())
325 }
326}
327
328#[cfg(test)]
329mod tests {
330
331 use tempfile::TempDir;
332
333 use crate::backend::MemBackend;
334 use crate::engine::test_helpers::*;
335 use crate::engine::{
336 CreateEntityArgs, DeleteEntityArgs, Engine, EngineError, RelateEntityArgs,
337 };
338 use crate::ops::WarningHint;
339 use crate::storage::FilesystemMemWriter;
340
341 fn engine_with_anchored(
344 tmp: &TempDir,
345 title: &str,
346 ) -> (Engine, crate::engine::CreateEntityOutcome) {
347 let mem_dir = tmp.path().to_path_buf();
348 let writer = FilesystemMemWriter::new(mem_dir.clone());
349 let mut engine = Engine::from_mounts(vec![(
350 folder_mount("specs", mem_dir),
351 Box::new(writer) as Box<dyn MemBackend>,
352 )])
353 .unwrap();
354 let (actor, client) = cli_actor();
355 let mut args: CreateEntityArgs = empty_create_args("specs", title);
356 args.anchors = vec![crate::anchor::AnchorInput {
357 artifact: Some("src/lib.rs".into()),
358 grain: Some("file".into()),
359 class: Some("anchored".into()),
360 hash: Some("h1".into()),
361 hash_stability: Some("stable".into()),
362 ..Default::default()
363 }];
364 let outcome = engine
365 .create_entity(args, actor, Some(&client), None)
366 .unwrap();
367 (engine, outcome)
368 }
369
370 #[test]
371 fn delete_removes_entity_anchors_no_orphan() {
372 let tmp = TempDir::new().unwrap();
373 let (mut engine, seeded) = engine_with_anchored(&tmp, "Anchored Doomed");
374 assert_eq!(engine.entity_anchors(&seeded.id).len(), 1);
375 let (actor, client) = cli_actor();
376 engine
377 .delete_entity(
378 DeleteEntityArgs {
379 id: seeded.id.clone(),
380 expected_hash: Some(seeded.content_hash.clone()),
381 },
382 actor,
383 Some(&client),
384 None,
385 )
386 .unwrap();
387 assert!(engine.entity_anchors(&seeded.id).is_empty());
390 assert!(engine.anchors_referencing_artifact("src/lib.rs").is_empty());
391 }
392
393 #[test]
398 fn delete_through_the_engine_leaves_no_dangling_row() {
399 let tmp = TempDir::new().unwrap();
400 let (mut engine, seeded) = engine_with_anchored(&tmp, "Anchored Doomed");
401 let (actor, client) = cli_actor();
402 engine
403 .delete_entity(
404 DeleteEntityArgs {
405 id: seeded.id.clone(),
406 expected_hash: Some(seeded.content_hash.clone()),
407 },
408 actor,
409 Some(&client),
410 None,
411 )
412 .unwrap();
413 let report = engine.verify_mem_anchors("specs").unwrap();
414 assert_eq!(report.unreconciled, None, "the entity end was examined");
415 assert_eq!(
416 report.dangling, 0,
417 "the delete path took the row with it, so there is nothing to report"
418 );
419 }
420
421 #[test]
425 fn a_row_whose_entity_vanished_reads_dangling_not_resolved() {
426 let tmp = TempDir::new().unwrap();
427 let (_engine, seeded) = engine_with_anchored(&tmp, "Anchored Vanishing");
428 std::fs::remove_file(tmp.path().join(format!(
431 "{}.md",
432 seeded.id.as_ref().split_once("--").unwrap().1
433 )))
434 .unwrap();
435 let engine = Engine::from_mounts(vec![(
436 folder_mount("specs", tmp.path().to_path_buf()),
437 Box::new(FilesystemMemWriter::new(tmp.path().to_path_buf())) as Box<dyn MemBackend>,
438 )])
439 .unwrap();
440 drop(seeded);
441 let report = engine.verify_mem_anchors("specs").unwrap();
442 assert_eq!(report.dangling, 1);
443 assert_eq!(report.resolved, 0, "never evidence of health");
444 assert_eq!(
445 report.unresolvable, 0,
446 "and never folded into the artifact-end bucket, whose repair is the opposite"
447 );
448 assert_eq!(report.anchors[0].state, "dangling");
449 }
450
451 #[test]
452 fn delete_entity_removes_file_and_store_entry() {
453 let tmp = TempDir::new().unwrap();
454 let (mut engine, seeded) = engine_with_seed(&tmp, "Doomed");
455 let (actor, client) = cli_actor();
456 let outcome = engine
457 .delete_entity(
458 DeleteEntityArgs {
459 id: seeded.id.clone(),
460 expected_hash: Some(seeded.content_hash.clone()),
461 },
462 actor,
463 Some(&client),
464 None,
465 )
466 .unwrap();
467 assert_eq!(outcome.id, seeded.id);
468 assert_eq!(outcome.removed_incoming, Vec::<String>::new());
469 assert!(engine.get_entity(&seeded.id).is_none());
471 assert!(!tmp.path().join(&seeded.file_path).exists());
473 let log = std::fs::read_to_string(tmp.path().join(".memstead/changes.jsonl")).unwrap();
475 assert!(log.contains("\"kind\":\"delete\""));
476 }
477
478 #[test]
479 fn delete_entity_rejects_hash_mismatch() {
480 let tmp = TempDir::new().unwrap();
481 let (mut engine, seeded) = engine_with_seed(&tmp, "Locked");
482 let (actor, client) = cli_actor();
483 let err = engine
484 .delete_entity(
485 DeleteEntityArgs {
486 id: seeded.id.clone(),
487 expected_hash: Some("nope".to_string()),
488 },
489 actor,
490 Some(&client),
491 None,
492 )
493 .unwrap_err();
494 match err {
495 EngineError::HashMismatch {
496 id,
497 current,
498 is_stub,
499 } => {
500 assert_eq!(id, seeded.id.to_string());
501 assert_eq!(current, seeded.content_hash);
502 assert!(!is_stub, "real entity must not flag as stub");
503 }
504 other => panic!("expected HashMismatch, got {other:?}"),
505 }
506 }
507
508 #[test]
515 fn delete_entity_on_stub_with_non_empty_hash_surfaces_is_stub_flag() {
516 let tmp = TempDir::new().unwrap();
517 let (mut engine, _seed) = engine_with_seed(&tmp, "Anchor");
518 let (actor, client) = cli_actor();
519 let stub_id = crate::EntityId::new("specs", "stub-target");
528 let source = engine
529 .create_entity(
530 empty_create_args("specs", "Source With Link"),
531 actor,
532 Some(&client),
533 None,
534 )
535 .unwrap();
536 engine
537 .relate_entity(
538 RelateEntityArgs {
539 source: source.id.clone(),
540 expected_hash: Some(source.content_hash.clone()),
541 rel_type: "USES".to_string(),
542 target: stub_id.clone(),
543 remove: false,
544 description: None,
545 dry_run: false,
546 },
547 actor,
548 Some(&client),
549 None,
550 )
551 .unwrap();
552 assert!(
553 engine.store().contains(&stub_id),
554 "forward-reference relate must materialise stub"
555 );
556
557 let err = engine
561 .delete_entity(
562 DeleteEntityArgs {
563 id: stub_id.clone(),
564 expected_hash: Some("definitely-non-empty".to_string()),
565 },
566 actor,
567 Some(&client),
568 None,
569 )
570 .unwrap_err();
571 match err {
572 EngineError::HashMismatch {
573 id,
574 current,
575 is_stub,
576 } => {
577 assert_eq!(id, stub_id.to_string());
578 assert!(current.is_empty(), "stub has no content hash");
579 assert!(is_stub, "is_stub must be true on a stub mismatch");
580 let msg = format!(
581 "{}",
582 EngineError::HashMismatch {
583 id,
584 current,
585 is_stub
586 }
587 );
588 assert!(
589 msg.contains("stub") && msg.contains("expected_hash: \"\""),
590 "stub message must name the corrective action; got: {msg}",
591 );
592 }
593 other => panic!("expected HashMismatch, got {other:?}"),
594 }
595 }
596
597 #[test]
604 fn delete_entity_on_stub_with_empty_hash_succeeds_via_in_memory_route() {
605 let tmp = TempDir::new().unwrap();
606 let (mut engine, _anchor) = engine_with_seed(&tmp, "Anchor");
607 let (actor, client) = cli_actor();
608
609 let stub_id = crate::EntityId::new("specs", "ghost-target");
617 engine.store.upsert(
618 stub_id.clone(),
619 super::make_stub(&stub_id, crate::entity::StubKind::ForwardReference),
620 );
621 assert!(
622 engine.store().contains(&stub_id),
623 "injected orphan stub must be in store"
624 );
625
626 let outcome = engine
627 .delete_entity(
628 DeleteEntityArgs {
629 id: stub_id.clone(),
630 expected_hash: Some(String::new()),
631 },
632 actor,
633 Some(&client),
634 None,
635 )
636 .expect("stub delete with expected_hash=\"\" must succeed");
637
638 assert_eq!(outcome.id, stub_id);
639 assert!(
641 outcome.write_id.is_empty(),
642 "stub deletes skip the backend write; write_id is empty"
643 );
644 assert!(
646 !engine.store().contains(&stub_id),
647 "stub must be gone from the in-memory store"
648 );
649 assert!(engine.get_entity(&stub_id).is_none());
651 let log = std::fs::read_to_string(tmp.path().join(".memstead/changes.jsonl")).unwrap();
654 assert!(log.contains("\"kind\":\"delete\""));
655 }
656
657 #[test]
658 fn delete_entity_refuses_on_write_mem_referrers_with_typed_payload() {
659 let tmp = TempDir::new().unwrap();
660 let (mut engine, target) = engine_with_seed(&tmp, "Target");
661 let (actor, client) = cli_actor();
662 let source = engine
664 .create_entity(
665 empty_create_args("specs", "Source"),
666 actor,
667 Some(&client),
668 None,
669 )
670 .unwrap();
671 engine
672 .relate_entity(
673 RelateEntityArgs {
674 source: source.id.clone(),
675 expected_hash: Some(source.content_hash.clone()),
676 rel_type: "USES".to_string(),
677 target: target.id.clone(),
678 remove: false,
679 description: None,
680 dry_run: false,
681 },
682 actor,
683 Some(&client),
684 None,
685 )
686 .unwrap();
687
688 let err = engine
691 .delete_entity(
692 DeleteEntityArgs {
693 id: target.id.clone(),
694 expected_hash: None,
695 },
696 actor,
697 Some(&client),
698 None,
699 )
700 .unwrap_err();
701 match err {
702 EngineError::HasIncomingRefs { id, referrers } => {
703 assert_eq!(id, target.id.to_string());
704 assert_eq!(referrers.len(), 1);
705 let r = &referrers[0];
706 assert_eq!(r.from_id, source.id.to_string());
707 assert_eq!(r.rel_types, vec!["USES".to_string()]);
708 assert_eq!(r.mem, "specs");
709 }
710 other => panic!("expected HasIncomingRefs, got {other:?}"),
711 }
712
713 assert!(engine.get_entity(&target.id).is_some());
716 assert!(tmp.path().join(&target.file_path).exists());
717 }
718
719 #[test]
720 fn delete_entity_returns_write_id_and_relations_removed() {
721 let tmp = TempDir::new().unwrap();
722 let (mut engine, target) = engine_with_seed(&tmp, "Target");
723 let (actor, client) = cli_actor();
724
725 let source = engine
729 .create_entity(
730 empty_create_args("specs", "Source"),
731 actor,
732 Some(&client),
733 None,
734 )
735 .unwrap();
736 let related = engine
737 .relate_entity(
738 RelateEntityArgs {
739 source: source.id.clone(),
740 expected_hash: Some(source.content_hash.clone()),
741 rel_type: "USES".to_string(),
742 target: target.id.clone(),
743 remove: false,
744 description: None,
745 dry_run: false,
746 },
747 actor,
748 Some(&client),
749 None,
750 )
751 .unwrap();
752
753 let outcome = engine
754 .delete_entity(
755 DeleteEntityArgs {
756 id: source.id.clone(),
757 expected_hash: Some(related.content_hash.clone()),
758 },
759 actor,
760 Some(&client),
761 None,
762 )
763 .unwrap();
764
765 assert!(
767 !outcome.write_id.is_empty(),
768 "write_id must be populated on a real delete"
769 );
770 assert_eq!(outcome.relations_removed, 1);
772 assert!(outcome.orphan_stubs_removed.is_empty());
774 assert!(outcome.warnings.is_empty());
776 }
777
778 #[test]
779 fn delete_entity_garbage_collects_orphaned_stubs() {
780 let tmp = TempDir::new().unwrap();
781 let (mut engine, source) = engine_with_seed(&tmp, "Source");
782 let (actor, client) = cli_actor();
783 let stub_id = crate::EntityId::new("specs", "ghost-stub");
784
785 let related = engine
788 .relate_entity(
789 RelateEntityArgs {
790 source: source.id.clone(),
791 expected_hash: Some(source.content_hash.clone()),
792 rel_type: "USES".to_string(),
793 target: stub_id.clone(),
794 remove: false,
795 description: None,
796 dry_run: false,
797 },
798 actor,
799 Some(&client),
800 None,
801 )
802 .unwrap();
803 assert!(engine.store().contains(&stub_id), "stub must be in store");
804
805 let outcome = engine
808 .delete_entity(
809 DeleteEntityArgs {
810 id: source.id.clone(),
811 expected_hash: Some(related.content_hash.clone()),
812 },
813 actor,
814 Some(&client),
815 None,
816 )
817 .unwrap();
818
819 assert_eq!(outcome.orphan_stubs_removed, vec![stub_id.clone()]);
820 assert!(
821 !engine.store().contains(&stub_id),
822 "GC must drop the orphaned stub"
823 );
824 }
825
826 #[test]
835 fn delete_entity_demotes_to_stub_when_only_readonly_referrers_remain() {
836 use crate::engine::test_helpers::{archive_mount, build_archive};
837 use crate::workspace::{Mount, MountCapability, MountLifecycle, MountStorage};
838
839 let tmp = TempDir::new().unwrap();
840 let writable_dir = tmp.path().join("writable");
841 std::fs::create_dir_all(&writable_dir).unwrap();
842 let writer = FilesystemMemWriter::new(writable_dir.clone());
843
844 let archive_md = "---\ntype: spec\ncreated_date: 2026-01-15\nlast_modified: 2026-04-12\nlevel: M0\n---\n# Archived Source\n\n## Identity\n\nLinks to [[specs:target]].\n\n## Purpose\n\nFixture for residual-stub demotion.\n\n## Relationships\n\n- **REFERENCES**: [[specs:target]]\n";
849 let archive_path = build_archive(
850 tmp.path(),
851 "archive",
852 &[("archived-source.md", archive_md.as_bytes())],
853 );
854
855 let folder_mount = Mount {
856 mem: "specs".to_string(),
857 schema: Some(crate::engine::test_helpers::pin("default")),
858 storage: MountStorage::Folder {
859 path: writable_dir.clone(),
860 },
861 capability: MountCapability::Write,
862 lifecycle: MountLifecycle::Eager,
863 cross_linkable: true,
864 migration_target: None,
865 };
866 let archive_reader = crate::storage::ArchiveBackend::new(archive_path.clone());
867 let mut engine = Engine::from_mounts(vec![
868 (folder_mount, Box::new(writer) as Box<dyn MemBackend>),
869 (
870 archive_mount("archive", archive_path.clone()),
871 Box::new(archive_reader) as Box<dyn MemBackend>,
872 ),
873 ])
874 .unwrap();
875
876 let (actor, client) = cli_actor();
877 let target = engine
878 .create_entity(
879 empty_create_args("specs", "Target"),
880 actor,
881 Some(&client),
882 None,
883 )
884 .unwrap();
885
886 let archived_source_id = crate::EntityId::new("archive", "archived-source");
889 assert!(
890 engine.store().contains(&archived_source_id),
891 "archive entity must load into the store"
892 );
893 let incoming_pre: Vec<_> = engine
894 .store()
895 .incoming(&target.id)
896 .iter()
897 .map(|e| e.from.clone())
898 .collect();
899 assert!(
900 incoming_pre.contains(&archived_source_id),
901 "archive wiki-link must produce an incoming edge on target; got {incoming_pre:?}"
902 );
903
904 let outcome = engine
907 .delete_entity(
908 DeleteEntityArgs {
909 id: target.id.clone(),
910 expected_hash: Some(target.content_hash.clone()),
911 },
912 actor,
913 Some(&client),
914 None,
915 )
916 .unwrap();
917
918 assert!(!writable_dir.join(&target.file_path).exists());
920 let demoted = engine
922 .get_entity(&target.id)
923 .expect("residual stub must remain in store");
924 assert!(demoted.stub, "demoted entity must be flagged as stub");
925 assert!(demoted.entity_type.is_empty());
926 match &demoted.stub_kind {
930 Some(crate::entity::StubKind::Residual {
931 since_commit: _,
932 readonly_referrers,
933 }) => {
934 assert_eq!(
935 readonly_referrers,
936 &vec![archived_source_id.clone()],
937 "Residual.readonly_referrers must snapshot the surviving referrers at mutation time"
938 );
939 }
940 other => panic!("demoted stub must be tagged Residual; got {other:?}"),
941 }
942 let incoming_post: Vec<_> = engine
944 .store()
945 .incoming(&target.id)
946 .iter()
947 .map(|e| e.from.clone())
948 .collect();
949 assert!(
950 incoming_post.contains(&archived_source_id),
951 "archive incoming edge must survive demotion"
952 );
953 let referrers = outcome
955 .warnings
956 .iter()
957 .find_map(|w| match w {
958 WarningHint::ResidualStubForReadOnlyReferrers { referrers, .. } => {
959 Some(referrers.clone())
960 }
961 _ => None,
962 })
963 .expect("ResidualStubForReadOnlyReferrers warning must surface");
964 assert_eq!(referrers, vec![archived_source_id]);
965 assert_eq!(outcome.removed_incoming.len(), 1);
966 }
967
968 }