diff --git a/crates/doc/src/foreign.rs b/crates/doc/src/foreign.rs
--- a/crates/doc/src/foreign.rs
+++ b/crates/doc/src/foreign.rs
@@ -89,8 +89,31 @@ impl Foreign {
             && self.data_store.is_empty()
     }
 
+    /// Whether a kept association (DIMASSOC object) links dimension `dim`: one CADCraft doesn't
+    /// model, written back as it was read.
+    pub fn has_dimassoc(&self, dim: Handle) -> bool {
+        self.objects.iter().any(|o| dimassoc_of(o) == Some(dim))
+    }
+
+    /// Drop the kept association of dimension `dim` (DIMDISASSOCIATE); whether it had one. Its
+    /// extension dictionary entry and the dimension's reactor to it go with it on save.
+    pub fn drop_dimassoc(&mut self, dim: Handle) -> bool {
+        let before = self.objects.len();
+        self.objects.retain(|o| dimassoc_of(o) != Some(dim));
+        self.objects.len() != before
+    }
+
     /// The entities the data store has records for (their 320 handles).
     pub fn data_store_entities(&self) -> std::collections::BTreeSet<Handle> {
         self.data_store.iter().filter(|t| t.code == 320).filter_map(|t| Handle::parse_hex(t.value.trim())).collect()
     }
 }
+
+/// The dimension a kept DIMASSOC object links: the first 330 after its AcDbDimAssoc marker.
+fn dimassoc_of(o: &ForeignObject) -> Option<Handle> {
+    if !o.kind.trim().eq_ignore_ascii_case("DIMASSOC") {
+        return None;
+    }
+    let mut body = o.tags.iter().skip_while(|t| !(t.code == 100 && t.value.trim() == "AcDbDimAssoc"));
+    body.find(|t| t.code == 330).and_then(|t| Handle::parse_hex(t.value.trim()))
+}
diff --git a/crates/engine/src/cmd/annotate.rs b/crates/engine/src/cmd/annotate.rs
--- a/crates/engine/src/cmd/annotate.rs
+++ b/crates/engine/src/cmd/annotate.rs
@@ -602,19 +602,26 @@ fn run_reassociate(s: &mut Session, p: &Value) -> Result<Value> {
     Ok(json!({ "dimensions": hs.len(), "associated": n, "points": points, "message": format!("{n} of {} dimension(s) associated.", hs.len()) }))
 }
 
+/// DIMDISASSOCIATE: clears the association of the given dimensions (the modelled one and a kept DIMASSOC read from the file); returns how many had one.
 fn run_disassociate(s: &mut Session, p: &Value) -> Result<Value> {
     let hs = dims_of(s, &targets(s, p)?);
     let d = s.doc_mut()?;
     let mut n = 0;
     for h in &hs {
+        let mut had = false;
         let _ = d.modify_entity(*h, |e| {
             if let EntityKind::Dimension(dm) = &mut e.kind
                 && !dm.assoc.is_empty()
             {
                 dm.assoc.clear();
-                n += 1;
+                had = true;
             }
         });
+        // An association read from the file that CADCraft doesn't model (kept as it was).
+        if d.foreign.has_dimassoc(*h) {
+            had |= std::sync::Arc::make_mut(&mut d.foreign).drop_dimassoc(*h);
+        }
+        n += usize::from(had);
     }
     Ok(json!({ "disassociated": n, "message": format!("{n} disassociated.") }))
 }
diff --git a/crates/engine/tests/dimdisassociate_kept.rs b/crates/engine/tests/dimdisassociate_kept.rs
new file mode 100644
--- /dev/null
+++ b/crates/engine/tests/dimdisassociate_kept.rs
@@ -0,0 +1,27 @@
+//! DIMDISASSOCIATE on dimensions whose association CADCraft keeps without modelling it (FF1): the
+//! kept DIMASSOC goes, so the saved file's dimension is no longer associative; UNDO brings it back.
+
+#[path = "../../io/tests/support/dimassoc.rs"]
+mod fixture;
+
+use cadcraft_doc::Handle;
+use cadcraft_engine::Session;
+use serde_json::json;
+
+/// DIMDISASSOCIATE drops only the chosen dimension's kept DIMASSOC; UNDO brings it back.
+#[test]
+fn disassociating_drops_the_kept_association() -> Result<(), Box<dyn std::error::Error>> {
+    let mut s = Session::empty();
+    s.open_drawing(cadcraft_io::read_dxf(fixture::source().as_bytes())?, "kept", None);
+    let kept = |s: &Session, h: u64| s.doc().map(|d| d.foreign.has_dimassoc(Handle(h)));
+    assert!(kept(&s, 0x51)? && kept(&s, 0x31)?);
+    let r = s.execute("dimdisassociate", &json!({ "handles": ["51"] }))?;
+    assert_eq!(r["disassociated"], 1);
+    assert!(!kept(&s, 0x51)? && kept(&s, 0x31)?, "only the chosen dimension's");
+    let text = cadcraft_io::write_dxf(s.doc()?);
+    let written = cadcraft_dxf::parse(text.as_bytes())?.iter().filter(|t| t.code == 0 && t.str() == "DIMASSOC").count();
+    assert_eq!(written, 1);
+    s.execute("undo", &json!({}))?;
+    assert!(kept(&s, 0x51)?);
+    Ok(())
+}
diff --git a/crates/io/src/dxf_raw.rs b/crates/io/src/dxf_raw.rs
--- a/crates/io/src/dxf_raw.rs
+++ b/crates/io/src/dxf_raw.rs
@@ -38,9 +38,22 @@ const MODELLED_KEYS: &[&str] = &[
 
 /// Extension dictionary entries not kept raw: dimension associativity (CADCraft reads and
 /// rewrites it), draw order and annotation scale contexts (written from their own models by the
-/// changes that model them; stale copies would contradict the drawing).
+/// changes that model them; stale copies would contradict the drawing). A dimension's association
+/// CADCraft didn't model (one in a block, or linked by object snaps it doesn't infer) is kept, see
+/// [`modelled_xdict_key`].
 const MODELLED_XDICT_KEYS: &[&str] = &["ACAD_DIMASSOC", "ACAD_SORTENTS", "ACDBCONTEXTDATAMANAGER"];
 
+/// The extension dictionary entry of a dimension's association (its DIMASSOC object).
+const DIMASSOC_KEY: &str = "ACAD_DIMASSOC";
+
+/// Whether an extension dictionary entry is written from CADCraft's model rather than kept: a
+/// dimension's DIMASSOC only when CADCraft read the association into the dimension (`owner` is
+/// the dictionary's owner; `own_assoc` the dimensions that have one), as it then writes its own.
+fn modelled_xdict_key(key: &str, owner: Option<&str>, own_assoc: &HashSet<String>) -> bool {
+    let k = up(key);
+    MODELLED_XDICT_KEYS.contains(&k.as_str()) && (k != DIMASSOC_KEY || owner.is_none_or(|o| own_assoc.contains(o)))
+}
+
 /// Applications whose xdata CADCraft reads and writes itself. `ACAD` holds dimension overrides
 /// (DSTYLE) on dimensions and dimension styles; elsewhere it is kept.
 const OWN_APPS: &[&str] = &["CADCRAFT", "ACADANNOTATIVE", "ACCMTRANSPARENCY", "ACAECLAYERSTANDARD"];
@@ -332,6 +345,9 @@ pub(crate) fn read(secs: &[Section], d: &mut Drawing) {
     for e in all_entities(d) {
         ents.entry(e.handle.hex()).or_insert(matches!(e.kind, EntityKind::Unknown(_)));
     }
+    // Dimensions whose association CADCraft read (and writes back from its model).
+    let own_assoc: HashSet<String> =
+        all_entities(d).filter(|e| matches!(&e.kind, EntityKind::Dimension(dm) if !dm.assoc.is_empty())).map(|e| e.handle.hex()).collect();
     let ids = identities(&tables, &blocks, &objects);
     // Extension dictionaries of entities and records: owner → dictionary.
     let mut xdict_of: HashMap<String, String> = HashMap::new();
@@ -361,7 +377,10 @@ pub(crate) fn read(secs: &[Section], d: &mut Drawing) {
         let (Some(h), Some(o)) = (hd.handle, hd.owner) else { return false };
         !by_handle.contains_key(&o) && xdict_of.get(&o) == Some(&h) && (ents.contains_key(&o) || ids.contains_key(&o))
     };
-    let modelled_entry = |t: &[Tag], h: &str| entries(t).iter().any(|(k, eh)| eh == h && MODELLED_XDICT_KEYS.contains(&up(k).as_str()));
+    let modelled_entry = |t: &[Tag], h: &str| {
+        let owner = head(t).owner;
+        entries(t).iter().any(|(k, eh)| eh == h && modelled_xdict_key(k, owner.as_deref(), &own_assoc))
+    };
     // Which objects to keep: those under a named object dictionary entry CADCraft doesn't model,
     // under the extension dictionary of an entity or record (minus modelled entries; one left
     // empty goes), or owned by an entity of a type CADCraft doesn't model. Memoized along each
@@ -395,7 +414,7 @@ pub(crate) fn read(secs: &[Section], d: &mut Drawing) {
                 continue;
             }
             if is_xdict(cur) {
-                break entries(t).iter().any(|(k, _)| !MODELLED_XDICT_KEYS.contains(&up(k).as_str()));
+                break entries(t).iter().any(|(k, _)| !modelled_xdict_key(k, Some(o.as_str()), &own_assoc));
             }
             break ents.get(&o) == Some(&true);
         };
@@ -412,7 +431,7 @@ pub(crate) fn read(secs: &[Section], d: &mut Drawing) {
         let Some(h) = head(t).handle.and_then(|h| handle(&h)) else { continue };
         let mut tags: Vec<RawTag> = t.iter().map(raw).collect();
         if is_xdict(i) {
-            strip_entries(&mut tags);
+            strip_entries(&mut tags, head(t).owner.as_deref(), &own_assoc);
         }
         if head(t).owner == root
             && let Some(key) = root_key(&h.hex())
@@ -588,12 +607,13 @@ pub(crate) fn data_store(tags: &mut Vec<Tag>, d: &Drawing, modern: bool) {
     tags.splice(at..at, out);
 }
 
-/// Drop a dictionary's modelled entries (key and handle) from its groups.
-fn strip_entries(tags: &mut Vec<RawTag>) {
+/// Drop a dictionary's modelled entries (key and handle) from its groups (`owner`: the
+/// dictionary's owner, for [`modelled_xdict_key`]).
+fn strip_entries(tags: &mut Vec<RawTag>, owner: Option<&str>, own_assoc: &HashSet<String>) {
     let mut out = Vec::with_capacity(tags.len());
     let mut i = 0;
     while let Some(t) = tags.get(i) {
-        if t.code == 3 && MODELLED_XDICT_KEYS.contains(&up(&t.value).as_str()) && tags.get(i + 1).is_some_and(|n| matches!(n.code, 350 | 360)) {
+        if t.code == 3 && modelled_xdict_key(&t.value, owner, own_assoc) && tags.get(i + 1).is_some_and(|n| matches!(n.code, 350 | 360)) {
             i += 2;
             continue;
         }
@@ -922,6 +942,14 @@ pub(crate) fn finish(tags: &mut Vec<Tag>, d: &Drawing) {
             alive_memo.insert(p, verdict);
         }
     }
+    // A kept association goes only with everything it links: a dimension whose geometry is gone
+    // is no longer associative (as when it is erased in AutoCAD), not linked to nothing.
+    for o in f.objects.iter().filter(|o| up(&o.kind) == "DIMASSOC") {
+        let linked = o.tags.iter().filter(|t| matches!(t.code, 331 | 332)).filter_map(|t| handle(&t.value)).all(|h| written.contains(&h.hex()));
+        if !linked {
+            alive_memo.insert(o.handle.hex(), false);
+        }
+    }
     // New handles for the written kept objects, above every written handle and the seed.
     let mut next = seed(tags).max(max_handle(tags).saturating_add(1));
     let mut alive: HashMap<String, String> = HashMap::new();
diff --git a/crates/io/tests/dimassoc_kept.rs b/crates/io/tests/dimassoc_kept.rs
new file mode 100644
--- /dev/null
+++ b/crates/io/tests/dimassoc_kept.rs
@@ -0,0 +1,112 @@
+//! Dimension associativity CADCraft doesn't model (FF1): a DIMASSOC of a dimension in a block, or
+//! linked by an object snap CADCraft doesn't infer, is kept and written back with its references
+//! moved, through DXF and DWG; erasing the geometry it links drops it (the dimension is then no
+//! longer associative, as in AutoCAD). Original synthetic DXF after the DXF Reference (DIMENSION,
+//! DIMASSOC, DICTIONARY). The DWG oracle shares acadrust 0.6.5; no AutoCAD validation is claimed.
+
+#[path = "support/dimassoc.rs"]
+mod fixture;
+
+use cadcraft_doc::{EntityKind, Handle};
+use cadcraft_dxf::Tag;
+use fixture::source;
+
+type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
+
+/// For each DIMASSOC written: (its dimension, the geometry it links), checked to be reachable the
+/// way AutoCAD reaches it (the dimension's extension dictionary, entry ACAD_DIMASSOC) and named in
+/// the dimension's reactors.
+fn written(text: &str) -> TestResult<Vec<(String, String)>> {
+    let tags = cadcraft_dxf::parse(text.as_bytes())?;
+    let recs = cadcraft_dxf::records(&tags);
+    let get = |r: &[Tag], c: i32| r.iter().find(|t| t.code == c).map(|t| t.str());
+    let group = |r: &[Tag], name: &str| -> Vec<String> {
+        r.iter().skip_while(|t| t.str() != format!("{{{name}")).skip(1).take_while(|t| t.code != 102).map(Tag::str).collect()
+    };
+    let by_handle = |h: &str| recs.iter().find(|(_, r)| get(r, 5).as_deref() == Some(h));
+    let mut out = Vec::new();
+    for (_, r) in recs.iter().filter(|(k, _)| k == "DIMASSOC") {
+        let me = get(r, 5).ok_or("handle")?;
+        let body: Vec<&Tag> = r.iter().skip_while(|t| t.str() != "AcDbDimAssoc").collect();
+        let dim = body.iter().find(|t| t.code == 330).map(|t| t.str()).ok_or("dimension")?;
+        let geometry = body.iter().find(|t| t.code == 331).map(|t| t.str()).ok_or("geometry")?;
+        let (kind, d) = by_handle(&dim).ok_or("dimension written")?;
+        assert_eq!(kind, "DIMENSION");
+        assert!(group(d, "ACAD_REACTORS").contains(&me), "{dim} names its DIMASSOC {me} as a reactor");
+        let xd = group(d, "ACAD_XDICTIONARY").into_iter().next().ok_or("extension dictionary")?;
+        let (_, x) = by_handle(&xd).ok_or("extension dictionary written")?;
+        assert!(x.windows(2).any(|w| w[0].code == 3 && w[0].str() == "ACAD_DIMASSOC" && w[1].str() == me));
+        assert!(by_handle(&geometry).is_some_and(|(k, _)| k == "LINE"), "{geometry} is written");
+        out.push((dim, geometry));
+    }
+    out.sort();
+    Ok(out)
+}
+
+/// Both fixture associations: (dimension, linked line).
+fn both() -> Vec<(String, String)> {
+    vec![("31".into(), "30".into()), ("51".into(), "50".into())]
+}
+
+/// Associations CADCraft does not model are written back, through two DXF saves.
+#[test]
+fn associations_cadcraft_doesnt_model_are_written_back_with_their_references() -> TestResult {
+    let d = cadcraft_io::read_dxf(source().as_bytes())?;
+    // Neither is in CADCraft's model: one is in a block, the other's snap isn't inferred.
+    let dims = d.model.iter().chain(d.blocks.values().flat_map(|b| b.entities.iter()));
+    assert!(dims.filter_map(|e| if let EntityKind::Dimension(dm) = &e.kind { Some(dm) } else { None }).all(|dm| dm.assoc.is_empty()));
+    let text = cadcraft_io::write_dxf(&d);
+    assert_eq!(written(&text)?, both());
+    // Read back and saved again, they stay.
+    let back = cadcraft_io::read_dxf(text.as_bytes())?;
+    assert_eq!(written(&cadcraft_io::write_dxf(&back))?, both());
+    Ok(())
+}
+
+/// Erasing the linked line drops its association and leaves no dangling reference.
+#[test]
+fn erasing_the_linked_geometry_drops_the_association() -> TestResult {
+    let mut d = cadcraft_io::read_dxf(source().as_bytes())?;
+    assert!(d.model.remove(Handle(0x50)).is_some());
+    let text = cadcraft_io::write_dxf(&d);
+    assert_eq!(written(&text)?, vec![("31".to_string(), "30".to_string())]);
+    // No reference to the dropped object is left behind (the dimension's reactors among them).
+    let tags = cadcraft_dxf::parse(text.as_bytes())?;
+    let handles: std::collections::HashSet<String> = tags.iter().filter(|t| t.code == 5).map(Tag::str).collect();
+    for t in tags.iter().filter(|t| matches!(t.code, 330 | 331 | 350 | 360)) {
+        assert!(t.str() == "0" || handles.contains(&t.str()), "dangling {} {}", t.code, t.str());
+    }
+    Ok(())
+}
+
+/// Both associations go into the DWG and survive a second DWG save.
+#[cfg(not(target_arch = "wasm32"))]
+#[test]
+fn associations_go_into_the_dwg_and_back() -> TestResult {
+    use acadrust::objects::ObjectType;
+    let d = cadcraft_io::read_dxf(source().as_bytes())?;
+    let mut bytes = cadcraft_io::write_with(&d, "out.dwg", &cadcraft_io::SaveOptions::default())?;
+    for _ in 0..2 {
+        let out = acadrust::DwgReader::from_stream(std::io::Cursor::new(&bytes)).read_with_stats()?;
+        assert_eq!((out.stats.skipped_source_records, out.stats.recovered_errors), (0, 0));
+        let mut found: Vec<(u64, bool)> = out
+            .document
+            .objects
+            .values()
+            .filter_map(|o| match o {
+                ObjectType::Associative(a) if a.dxf_name.eq_ignore_ascii_case("DIMASSOC") => {
+                    let dim = [0x31u64, 0x51].into_iter().find(|h| a.references_handle(acadrust::Handle::new(*h)))?;
+                    let geometry = acadrust::Handle::new(if dim == 0x31 { 0x30 } else { 0x50 });
+                    Some((dim, a.references_handle(geometry)))
+                }
+                _ => None,
+            })
+            .collect();
+        found.sort_unstable();
+        assert_eq!(found, [(0x31, true), (0x51, true)]);
+        // The reopened DWG saves them again.
+        let back = cadcraft_io::read(&bytes, "out.dwg")?;
+        bytes = cadcraft_io::write_with(&back, "out.dwg", &cadcraft_io::SaveOptions::default())?;
+    }
+    Ok(())
+}
diff --git a/crates/io/tests/support/dimassoc.rs b/crates/io/tests/support/dimassoc.rs
new file mode 100644
--- /dev/null
+++ b/crates/io/tests/support/dimassoc.rs
@@ -0,0 +1,51 @@
+//! An original synthetic DXF after the DXF Reference (DIMENSION, DIMASSOC, DICTIONARY): dimension
+//! associativity CADCraft doesn't model. Shared by the I/O and engine tests (by file path).
+
+/// A LINE record with handle `h` in the space or block `owner`, at height `y`.
+fn line(h: &str, owner: &str, y: f64) -> String {
+    format!("0\nLINE\n5\n{h}\n330\n{owner}\n100\nAcDbEntity\n8\n0\n100\nAcDbLine\n10\n0\n20\n{y}\n30\n0\n11\n10\n21\n{y}\n31\n0\n")
+}
+
+/// An aligned dimension along the line at `y`, with its extension dictionary and its reactor to
+/// the DIMASSOC.
+fn dimension(h: &str, owner: &str, xdict: &str, assoc: &str, y: f64) -> String {
+    format!(
+        "0\nDIMENSION\n5\n{h}\n102\n{{ACAD_REACTORS\n330\n{assoc}\n102\n}}\n102\n{{ACAD_XDICTIONARY\n360\n{xdict}\n102\n}}\n330\n{owner}\n\
+         100\nAcDbEntity\n8\n0\n100\nAcDbDimension\n10\n10\n20\n{d}\n30\n0\n11\n5\n21\n{d}\n31\n0\n70\n33\n3\nStandard\n\
+         100\nAcDbAlignedDimension\n13\n0\n23\n{y}\n33\n0\n14\n10\n24\n{y}\n34\n0\n",
+        d = y + 2.
+    )
+}
+
+/// The extension dictionary and the DIMASSOC: the dimension's first point on `geometry` by
+/// object snap `osnap` (10 nearest, 8 perpendicular: neither inferred by CADCraft for a line).
+fn assoc(xdict: &str, h: &str, dim: &str, geometry: &str, osnap: u8, y: f64) -> String {
+    format!(
+        "0\nDICTIONARY\n5\n{xdict}\n330\n{dim}\n100\nAcDbDictionary\n280\n1\n281\n1\n3\nACAD_DIMASSOC\n360\n{h}\n\
+         0\nDIMASSOC\n5\n{h}\n102\n{{ACAD_REACTORS\n330\n{xdict}\n102\n}}\n330\n{xdict}\n100\nAcDbDimAssoc\n330\n{dim}\n90\n1\n70\n0\n71\n0\n\
+         1\nAcDbOsnapPointRef\n72\n{osnap}\n331\n{geometry}\n73\n0\n91\n0\n40\n0.0\n10\n0\n20\n{y}\n30\n0\n75\n0\n"
+    )
+}
+
+/// A block holding a line and a dimension on it (nearest snap), and a model space line with a
+/// dimension (perpendicular snap).
+pub fn source() -> String {
+    let mut s = String::from("0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1015\n9\n$HANDSEED\n5\n100\n0\nENDSEC\n");
+    s.push_str("0\nSECTION\n2\nCLASSES\n0\nCLASS\n1\nDIMASSOC\n2\nAcDbDimAssoc\n3\nObjectDBX Classes\n90\n0\n280\n0\n281\n0\n0\nENDSEC\n");
+    s.push_str("0\nSECTION\n2\nTABLES\n0\nTABLE\n2\nBLOCK_RECORD\n5\n1\n70\n2\n");
+    s.push_str("0\nBLOCK_RECORD\n5\n1F\n330\n1\n100\nAcDbSymbolTableRecord\n100\nAcDbBlockTableRecord\n2\n*Model_Space\n");
+    s.push_str("0\nBLOCK_RECORD\n5\n20\n330\n1\n100\nAcDbSymbolTableRecord\n100\nAcDbBlockTableRecord\n2\nWALL\n");
+    s.push_str("0\nENDTAB\n0\nENDSEC\n0\nSECTION\n2\nBLOCKS\n");
+    s.push_str("0\nBLOCK\n5\n21\n330\n20\n100\nAcDbEntity\n8\n0\n100\nAcDbBlockBegin\n2\nWALL\n70\n0\n10\n0\n20\n0\n30\n0\n3\nWALL\n1\n\n");
+    s.push_str(&line("30", "20", 0.));
+    s.push_str(&dimension("31", "20", "40", "41", 0.));
+    s.push_str("0\nENDBLK\n5\n22\n330\n20\n100\nAcDbEntity\n8\n0\n100\nAcDbBlockEnd\n0\nENDSEC\n");
+    s.push_str("0\nSECTION\n2\nENTITIES\n");
+    s.push_str(&line("50", "1F", 20.));
+    s.push_str(&dimension("51", "1F", "60", "61", 20.));
+    s.push_str("0\nENDSEC\n0\nSECTION\n2\nOBJECTS\n0\nDICTIONARY\n5\nC\n330\n0\n100\nAcDbDictionary\n281\n1\n");
+    s.push_str(&assoc("40", "41", "31", "30", 10, 0.));
+    s.push_str(&assoc("60", "61", "51", "50", 8, 20.));
+    s.push_str("0\nENDSEC\n0\nEOF\n");
+    s
+}
diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md
--- a/docs/file-format-parity.md
+++ b/docs/file-format-parity.md
@@ -1,6 +1,6 @@
 # File-format parity
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (DIMASSOC objects for angular, diametric and radial dimensions) · **Target:** Autodesk AutoCAD 2027
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (dimension associativity CADCraft doesn't model kept through DXF and DWG) · **Target:** Autodesk AutoCAD 2027
 
 Every format AutoCAD 2027 reads or writes, with CADCraft's support, fidelity and tests. Work
 items are in [gaps.md](gaps.md#file-formats); the weighted score feeds
@@ -101,6 +101,15 @@ on read with a note, so such files no longer exceed the DXF limit (#1482). **DWG
 | Objects: dynamic block data (ACAD_EVALUATION_GRAPH, BLOCK…PARAMETER, BLOCK…ACTION, grips) | read into a model (`dynblock`, `blocks.dynamic`): parameters, visibility states, value sets, stretch and lookup actions; references: their dynamic block, values and custom properties (`blocks.reference`, Properties ▸ Custom, read-only); kept raw (#528) | written back as read; a changed visibility state is written as AutoCAD writes it (a new anonymous block with its `AcDbBlockRepBTag`, the objects' `AcDbBlockRepETag`, the reference's representation data) | visibility states (and lookups that set only them) can be changed (`blocks.reference.set`, Properties ▸ Custom); distances, positions and the actions they drive not yet; DWG open and save keep it too (checked with an AutoCAD dynamic block) |
 | Handles and owners | entity handles kept; others regenerated | consistent | stable across CADCraft re-saves (#495); other programs' table and object handles renumbered once |
 
+**Dimension associativity CADCraft doesn't model.** CADCraft models the association of model and
+paper space dimensions whose points it can infer and writes those DIMASSOC objects itself. Any
+other DIMASSOC (a dimension in a block, or points linked by snaps CADCraft doesn't infer, such as
+nearest, perpendicular or through a block insert) is kept as read and written back with its
+references moved, through DXF and DWG (`dimassoc_kept`). A kept association whose linked geometry
+was erased is dropped, so the dimension is no longer associative, as when the geometry is erased
+in AutoCAD; DIMDISASSOCIATE drops it too. Moving the geometry in CADCraft doesn't move such a
+dimension (AutoCAD re-evaluates it on its next edit). Before, every one of them was lost on save.
+
 **DXF ≈ 50% read, ≈ 30% write** (estimated; weights: common 2D entities 50%, annotation objects
 20%, tables/objects 15%, foreign data preservation 15%).
 
@@ -140,6 +149,7 @@ our output in AutoCAD black-box.
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-24 | minor | Dimension associativity CADCraft doesn't model (dimensions in blocks, uninferred snaps) kept and written back through DXF and DWG; dropped with erased geometry or by DIMDISASSOCIATE |
 | 2026-10-22 | minor | DXF/DWG save: angular, diametric and radial dimensions get standard DIMASSOC objects (one centre reference to the circle for radial/diametric, the definition points for angular), so AutoCAD follows their geometry; the standard objects of those kinds are read back too, without CADCraft's xdata (#1651) |
 | 2026-10-19 | minor | DXF/DWG save: angular dimension block text honors DIMDSEP, including per-dimension overrides (#1646) |
 | 2026-10-19 | minor | A file's own arrowhead blocks with standard names (AutoCAD's `_Dot`, `_DotSmall`), named only by a dimension style, are kept and saved as they were instead of being replaced by CADCraft's solids; PURGE keeps arrowhead blocks dimension styles name |
