From 76599ab10b25bd2fc81355d95288256b5555c46b Mon Sep 17 00:00:00 2001 From: Greg von Nessi Date: Mon, 16 Feb 2026 22:06:27 +0000 Subject: [PATCH] =?UTF-8?q?Fix=20glossary=20termId=20=E2=86=92=20ref=20to?= =?UTF-8?q?=20match=20spec=20glossaryMark=20schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same pattern as the citation ref → refs fix: update the glossary mark constructor to emit "ref" instead of "termId", add backward-compat helpers (get_glossary_ref, normalize_glossary_attrs), and update GlossaryRef serde to serialize as "ref" while accepting both forms. Bump version to 0.7.0 (wire format change). --- CHANGELOG.md | 17 ++++++- Cargo.toml | 2 +- SECURITY.md | 4 +- cdx-cli/Cargo.toml | 2 +- cdx-core/Cargo.toml | 2 +- cdx-core/src/content/text.rs | 51 +++++++++++++++++++- cdx-core/src/extensions/semantic/glossary.rs | 2 +- cdx-core/tests/extension_schema.rs | 7 +-- 8 files changed, 75 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 875b9ec..716748f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.7.0] - 2026-02-16 + +### Changed + +- **Breaking:** `ExtensionMark::glossary()` now emits `"ref"` instead of `"termId"` to match the spec's `glossaryMark` schema +- **Breaking:** `GlossaryRef.term_id` serializes as `"ref"` instead of `"termId"` +- Deserialization accepts both old `"termId"` and new `"ref"` for backward compatibility + +### Added + +- `ExtensionMark::get_glossary_ref()` helper supporting both `"ref"` and legacy `"termId"` keys +- `ExtensionMark::normalize_glossary_attrs()` to migrate `"termId"` → `"ref"` in-place +- Backward-compatibility tests for glossary `"termId"` deserialization + ## [0.6.0] - 2026-02-16 ### Changed @@ -296,7 +310,8 @@ Initial release implementing Codex Document Format Specification v0.1. - `sign_document` - Sign a document with ES256 - `extract_content` - Extract text content from blocks -[Unreleased]: https://github.com/Entrolution/cdx-core/compare/v0.6.0...HEAD +[Unreleased]: https://github.com/Entrolution/cdx-core/compare/v0.7.0...HEAD +[0.7.0]: https://github.com/Entrolution/cdx-core/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/Entrolution/cdx-core/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/Entrolution/cdx-core/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/Entrolution/cdx-core/compare/v0.3.0...v0.4.0 diff --git a/Cargo.toml b/Cargo.toml index f16eefe..eaf08d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,4 @@ thiserror = "2.0" chrono = { version = "0.4", features = ["serde", "now"], default-features = false } # Internal crates -cdx-core = { path = "cdx-core", version = "0.6.0" } +cdx-core = { path = "cdx-core", version = "0.7.0" } diff --git a/SECURITY.md b/SECURITY.md index 47a6a8e..f2aae61 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Version | Supported | |---------|-----------| -| 0.6.x | Yes | -| < 0.6 | No | +| 0.7.x | Yes | +| < 0.7 | No | Only the latest minor release receives security updates. Earlier versions are not supported. diff --git a/cdx-cli/Cargo.toml b/cdx-cli/Cargo.toml index 272129e..a99a10b 100644 --- a/cdx-cli/Cargo.toml +++ b/cdx-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cdx-cli" -version = "0.6.0" +version = "0.7.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/cdx-core/Cargo.toml b/cdx-core/Cargo.toml index d3d8342..213e072 100644 --- a/cdx-core/Cargo.toml +++ b/cdx-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cdx-core" -version = "0.6.0" +version = "0.7.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/cdx-core/src/content/text.rs b/cdx-core/src/content/text.rs index fd092d3..b55d212 100644 --- a/cdx-core/src/content/text.rs +++ b/cdx-core/src/content/text.rs @@ -299,6 +299,29 @@ impl ExtensionMark { } } + /// Get glossary term ref, supporting both `"ref"` and legacy `"termId"`. + /// + /// Returns `None` if neither key is present. + #[must_use] + pub fn get_glossary_ref(&self) -> Option<&str> { + self.get_string_attribute("ref") + .or_else(|| self.get_string_attribute("termId")) + } + + /// Rewrite legacy `"termId"` → `"ref"` in the attributes map. + /// + /// No-op if `"ref"` already exists or `"termId"` is absent. + pub fn normalize_glossary_attrs(&mut self) { + if let Some(obj) = self.attributes.as_object_mut() { + if obj.contains_key("ref") { + return; + } + if let Some(val) = obj.remove("termId") { + obj.insert("ref".to_string(), val); + } + } + } + // ===== Convenience constructors for common extension marks ===== /// Create a citation mark (semantic extension). @@ -340,7 +363,7 @@ impl ExtensionMark { #[must_use] pub fn glossary(term_id: impl Into) -> Self { Self::new("semantic", "glossary").with_attributes(serde_json::json!({ - "termId": term_id.into() + "ref": term_id.into() })) } @@ -1176,7 +1199,31 @@ mod tests { fn test_glossary_convenience() { let ext = ExtensionMark::glossary("api-term"); assert!(ext.is_type("semantic", "glossary")); - assert_eq!(ext.get_string_attribute("termId"), Some("api-term")); + assert_eq!(ext.get_string_attribute("ref"), Some("api-term")); + assert_eq!(ext.get_glossary_ref(), Some("api-term")); + } + + #[test] + fn test_get_glossary_ref_legacy() { + let ext = ExtensionMark::new("semantic", "glossary") + .with_attributes(serde_json::json!({"termId": "api-term"})); + assert_eq!(ext.get_glossary_ref(), Some("api-term")); + } + + #[test] + fn test_normalize_glossary_attrs() { + let mut ext = ExtensionMark::new("semantic", "glossary") + .with_attributes(serde_json::json!({"termId": "api-term"})); + ext.normalize_glossary_attrs(); + assert_eq!(ext.get_string_attribute("ref"), Some("api-term")); + assert!(ext.get_string_attribute("termId").is_none()); + } + + #[test] + fn test_normalize_glossary_attrs_noop_when_ref_exists() { + let mut ext = ExtensionMark::glossary("api-term"); + ext.normalize_glossary_attrs(); + assert_eq!(ext.get_string_attribute("ref"), Some("api-term")); } #[test] diff --git a/cdx-core/src/extensions/semantic/glossary.rs b/cdx-core/src/extensions/semantic/glossary.rs index 5a12e02..550c014 100644 --- a/cdx-core/src/extensions/semantic/glossary.rs +++ b/cdx-core/src/extensions/semantic/glossary.rs @@ -146,9 +146,9 @@ impl GlossaryTerm { /// A reference to a glossary term in the document. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] pub struct GlossaryRef { /// ID of the glossary term. + #[serde(rename = "ref", alias = "termId")] pub term_id: String, /// Display text (if different from term). diff --git a/cdx-core/tests/extension_schema.rs b/cdx-core/tests/extension_schema.rs index 44cd24d..2b49245 100644 --- a/cdx-core/tests/extension_schema.rs +++ b/cdx-core/tests/extension_schema.rs @@ -117,13 +117,14 @@ fn schema_entity_emits_uri_and_entity_type() { // ===== Glossary ===== #[test] -fn schema_glossary_emits_term_id() { +fn schema_glossary_emits_ref() { let mark = ExtensionMark::glossary("ai"); let json = mark_to_json(&mark); assert_eq!(json["type"], "semantic:glossary"); - assert_string_field(&json, "termId", "glossary"); - assert_eq!(json["termId"], "ai"); + assert_string_field(&json, "ref", "glossary"); + assert_eq!(json["ref"], "ai"); + assert_field_absent(&json, "termId", "glossary must not emit legacy 'termId'"); } // ===== Index =====