diff --git a/crates/tepp_api/src/export.rs b/crates/tepp_api/src/export.rs index b00f0d0d6..9c3d8ac53 100644 --- a/crates/tepp_api/src/export.rs +++ b/crates/tepp_api/src/export.rs @@ -35,8 +35,9 @@ pub struct JsonLdExport { pub contract_version: u16, /// `JSON-LD` context URI or term map serialized as string for v1. pub context: String, - /// Primary node identifier. - pub id: String, + /// Primary node identifier. The v1 wire key remains `id` for compatibility. + #[serde(rename = "id")] + pub node_id: String, /// Node type label. pub type_name: String, /// Opaque payload digest the consumer must verify separately. @@ -120,14 +121,14 @@ impl JsonLdExport { /// Returns field-validation errors for empty values. pub fn new( context: impl Into, - id: impl Into, + node_id: impl Into, type_name: impl Into, artifact_digest_sha256: impl Into, ) -> Result { let export = Self { contract_version: EXPORT_CONTRACT_VERSION, context: context.into(), - id: id.into(), + node_id: node_id.into(), type_name: type_name.into(), artifact_digest_sha256: artifact_digest_sha256.into(), }; @@ -159,7 +160,7 @@ impl JsonLdExport { fn validate(&self) -> Result<(), ApiError> { require_contract_version(self.contract_version, EXPORT_CONTRACT_VERSION)?; require_nonempty(&self.context)?; - require_nonempty(&self.id)?; + require_nonempty(&self.node_id)?; require_nonempty(&self.type_name)?; require_nonempty(&self.artifact_digest_sha256)?; Ok(()) diff --git a/crates/tepp_api/tests/jsonld_semantic_identifier.rs b/crates/tepp_api/tests/jsonld_semantic_identifier.rs new file mode 100644 index 000000000..e7ec8a928 --- /dev/null +++ b/crates/tepp_api/tests/jsonld_semantic_identifier.rs @@ -0,0 +1,23 @@ +//! Contract regression for semantic Rust identifiers on the JSON-LD export seam. + +use tepp_api::JsonLdExport; + +#[test] +fn jsonld_export_uses_node_id_in_rust_and_preserves_v1_wire_id() { + let export = JsonLdExport::new( + "https://example.org/tepp/context.jsonld", + "urn:tepp:artifact:1", + "ValidationReport", + "abc123", + ) + .expect("valid JSON-LD export"); + + assert_eq!(export.node_id, "urn:tepp:artifact:1"); + + let wire_json = export.to_json().expect("serialize JSON-LD export"); + assert!(wire_json.contains(r#""id":"urn:tepp:artifact:1""#)); + assert!(!wire_json.contains("node_id")); + + let decoded = JsonLdExport::from_json(&wire_json).expect("deserialize v1 JSON-LD export"); + assert_eq!(decoded.node_id, export.node_id); +} diff --git a/docs/connectors/naruon-artifact-consumer.md b/docs/connectors/naruon-artifact-consumer.md index 9c6f6d185..b1a2771be 100644 --- a/docs/connectors/naruon-artifact-consumer.md +++ b/docs/connectors/naruon-artifact-consumer.md @@ -31,6 +31,8 @@ TEPP remains the scientific authority for estimation, recovery metrics, temporal Committed examples live under `examples/`. Schemas for analysis-run requests and corpus-split manifests live under `schemas/`. +For the `JsonLdExport` v1 boundary, Rust source code uses the semantic field name `node_id`; Serde deliberately preserves the existing serialized key `id`. Consumers must treat the versioned wire contract—not an internal Rust field spelling—as the interoperability boundary. A future change from the v1 `id` term to another JSON-LD representation such as `@id` requires an explicit versioned contract decision rather than an implicit source rename. + ## Purpose-bound disclosure When naruon requests an export, TEPP evaluates `AnalyticalPurpose::ModularServiceConsumer`. Free-text source bodies remain optional and purpose-gated; opaque analytical identifiers and membership/relation structure must not be blanket-masked when required for multilevel measurement (ADR 0009). @@ -59,3 +61,5 @@ Klyne, G., & Newman, C. (2002). *Date and time on the Internet: Timestamps* (RFC ISO/IEC. (2019). *ISO/IEC 27701:2019 Security techniques — Extension to ISO/IEC 27001 and ISO/IEC 27002 for privacy information management — Requirements and guidelines*. International Organization for Standardization. National Institute of Standards and Technology. (2020). *NIST Privacy Framework: A tool for improving privacy through enterprise risk management* (Version 1.0). U.S. Department of Commerce. https://doi.org/10.6028/NIST.CSWP.01162020 + +World Wide Web Consortium. (2020, July 16). *JSON-LD 1.1: A JSON-based serialization for linked data* (W3C Recommendation). https://www.w3.org/TR/json-ld11/ diff --git a/docs/doctoring/jsonld-node-semantic-identifier.md b/docs/doctoring/jsonld-node-semantic-identifier.md new file mode 100644 index 000000000..adad18c5f --- /dev/null +++ b/docs/doctoring/jsonld-node-semantic-identifier.md @@ -0,0 +1,39 @@ +# JSON-LD node semantic identifier + +## Decision + +The `tepp_api::JsonLdExport` Rust source contract names the primary node identity `node_id` rather than the underspecified bare field `id`. The existing version-1 JSON wire remains byte-key compatible at the field-name level: Serde maps the Rust `node_id` field to the serialized/deserialized key `"id"`. + +This is a source-contract naming repair, not a v1 wire-format migration. Organization search found no ContextualWisdomLab source consumer accessing `JsonLdExport.id`; the published consumer boundary is the versioned serialized artifact. The crate is also marked `publish = false`, so the repository remains the source owner for this API surface. + +## DDD boundary + +**Bounded context:** Analytical Artifact Export. + +**Aggregate:** `JsonLdExport` is a versioned export envelope owned by `tepp_api`. + +**Value object:** `node_id` identifies the primary node represented by the envelope. `type_name` describes the node type and `artifact_digest_sha256` binds the envelope to the artifact payload. + +**Invariant:** internal Rust names must identify the domain role without relying on a generic `id`; the existing v1 serialized key remains stable so naruon and other modular consumers do not require a coordinated wire migration. + +## JSON-LD compatibility + +JSON-LD 1.1 defines node identifiers through the `@id` keyword and permits terms to be defined through a context. TEPP v1 already has a deployed envelope field named `id`; this change does not claim that bare `id` is itself the normative JSON-LD keyword and does not silently rewrite the wire to `@id`. The existing contract is preserved until a separately versioned wire decision is made. + +The new regression test proves both sides of the anti-corruption seam: Rust code reads `node_id`, while serialized v1 JSON contains `"id"` and not `"node_id"`; deserializing the same v1 payload restores `node_id`. + +## Security, privacy, and scientific scope + +No estimator, psychometric arithmetic, temporal semantics, evidence cutoff, PII authority, database schema, network endpoint, or export authorization rule changes. The node identifier remains an opaque string and passes the same non-empty validation. `deny_unknown_fields` remains enabled. + +## Verification + +The RED commit introduced an external crate test that accessed `JsonLdExport.node_id` while production still exposed only `id`, producing a compile-time contract failure. The production repair then renamed the Rust field and constructor parameter and added `#[serde(rename = "id")]` to preserve the wire contract. Fresh exact-head repository and central checks remain the merge authority. + +## References + +Feitelson, D. G., Mizrahi, A., Noy, N., Ben Shabat, A., Eliyahu, O., & Sheffer, R. (2022). How developers choose names. *IEEE Transactions on Software Engineering, 48*(1), 37–52. https://doi.org/10.1109/TSE.2020.2976920 + +Schankin, A., Berger, A., Holt, D. V., Hofmeister, J. C., Riedel, T., & Beigl, M. (2018). Descriptive compound identifier names improve source code comprehension. In *Proceedings of the 26th Conference on Program Comprehension* (pp. 31–40). Association for Computing Machinery. https://doi.org/10.1145/3196321.3196332 + +World Wide Web Consortium. (2020, July 16). *JSON-LD 1.1: A JSON-based serialization for linked data* (W3C Recommendation). https://www.w3.org/TR/json-ld11/