Skip to content

Commit a356926

Browse files
committed
feat: PropertySpec.marking field + with_marking builder (LF-6 extension)
Per SMB session REQUEST at bus 5c8543a (16:30). Additive — every PropertySpec gains a `marking: Marking` field initialised to Marking::Internal (GDPR-safe baseline) by all 3 const constructors. New const builder method `.with_marking(m)` chains with the existing .with_semantic_type/.with_nars_floor/.with_codec_route pattern. Why: Before: SMB carried a side-table of (entity, predicate, Marking) tuples in smb-ontology::markings::SMB_MARKINGS. Two sources of truth (schema + side-table) could drift; typos silently fell back to Marking::Internal. After: marking lives directly on PropertySpec. SMB ontology dissolves the side-table; per-row fold becomes `schema.properties.iter().map(|p| p.marking).collect()`. Pattern matches with_semantic_type from PR #263 (commit 76a7237). Zero breaking change: existing PropertySpec construction sites inherit Marking::Internal default. New tests verify the default, override per-predicate, chained construction with_semantic_type + with_marking, and the W-2 most_restrictive fold over a row's markings. 243 contract lib tests pass (3 new). Clippy clean on new code. https://claude.ai/code/session_01SbYsmmbPf9YQuYbHZN52Zh
1 parent 90c8a2d commit a356926

1 file changed

Lines changed: 53 additions & 0 deletions

File tree

crates/lance-graph-contract/src/property.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,42 +52,52 @@ pub struct PropertySpec {
5252
pub nars_floor: Option<(u8, u8)>,
5353
/// What kind of value this property holds (LF-21).
5454
pub semantic_type: SemanticType,
55+
/// GDPR / data-protection classification (LF-6 marking).
56+
/// Default = `Marking::Internal` (GDPR-safe baseline).
57+
/// Override per-predicate via `.with_marking(...)`.
58+
pub marking: Marking,
5559
}
5660

5761
impl PropertySpec {
5862
/// Create a Required property spec. Default codec: Passthrough (Index).
5963
/// Default NARS floor: (128, 128) — moderate confidence required.
64+
/// Default marking: `Marking::Internal` (GDPR-safe).
6065
pub const fn required(predicate: &'static str) -> Self {
6166
Self {
6267
predicate,
6368
kind: PropertyKind::Required,
6469
codec_route: CodecRoute::Passthrough,
6570
nars_floor: Some((128, 128)),
6671
semantic_type: SemanticType::PlainText,
72+
marking: Marking::Internal,
6773
}
6874
}
6975

7076
/// Create an Optional property spec. Caller must specify codec route.
7177
/// No NARS floor by default (absence doesn't escalate).
78+
/// Default marking: `Marking::Internal` (GDPR-safe).
7279
pub const fn optional(predicate: &'static str, codec_route: CodecRoute) -> Self {
7380
Self {
7481
predicate,
7582
kind: PropertyKind::Optional,
7683
codec_route,
7784
nars_floor: None,
7885
semantic_type: SemanticType::PlainText,
86+
marking: Marking::Internal,
7987
}
8088
}
8189

8290
/// Create a Free property spec. Default codec: CamPq (Argmax).
8391
/// No NARS floor (schema-free, always accepted).
92+
/// Default marking: `Marking::Internal` (GDPR-safe).
8493
pub const fn free(predicate: &'static str) -> Self {
8594
Self {
8695
predicate,
8796
kind: PropertyKind::Free,
8897
codec_route: CodecRoute::CamPq,
8998
nars_floor: None,
9099
semantic_type: SemanticType::PlainText,
100+
marking: Marking::Internal,
91101
}
92102
}
93103

@@ -96,6 +106,14 @@ impl PropertySpec {
96106
self
97107
}
98108

109+
/// Override the GDPR / data-protection marking for this predicate (LF-6).
110+
/// Default is `Marking::Internal`. SMB customer schema overrides:
111+
/// `iban` → Financial, `geburtsdatum` → Pii, etc.
112+
pub const fn with_marking(mut self, marking: Marking) -> Self {
113+
self.marking = marking;
114+
self
115+
}
116+
99117
/// Override the NARS truth floor.
100118
pub const fn with_nars_floor(mut self, frequency: u8, confidence: u8) -> Self {
101119
self.nars_floor = Some((frequency, confidence));
@@ -456,6 +474,41 @@ mod tests {
456474
assert!(p.nars_floor.is_none());
457475
}
458476

477+
/// LF-6: every PropertySpec defaults to `Marking::Internal` (GDPR-safe).
478+
#[test]
479+
fn property_spec_marking_defaults_to_internal() {
480+
assert_eq!(PropertySpec::required("kdnr").marking, Marking::Internal);
481+
assert_eq!(PropertySpec::optional("note", CodecRoute::CamPq).marking, Marking::Internal);
482+
assert_eq!(PropertySpec::free("free").marking, Marking::Internal);
483+
}
484+
485+
/// SMB schema marking pattern: chain `with_marking` per predicate.
486+
#[test]
487+
fn property_spec_with_marking_overrides() {
488+
let iban = PropertySpec::required("iban").with_marking(Marking::Financial);
489+
let dob = PropertySpec::required("geburtsdatum").with_marking(Marking::Pii);
490+
let note = PropertySpec::free("note"); // stays Internal
491+
492+
assert_eq!(iban.marking, Marking::Financial);
493+
assert_eq!(dob.marking, Marking::Pii);
494+
assert_eq!(note.marking, Marking::Internal);
495+
496+
// Per-row fold (W-2): `most_restrictive` over a row's markings.
497+
let row_markings = [iban.marking, dob.marking, note.marking];
498+
assert_eq!(Marking::most_restrictive(&row_markings), Marking::Financial);
499+
}
500+
501+
/// `with_marking` is const and chains with `with_semantic_type` (LF-21).
502+
#[test]
503+
fn property_spec_with_marking_chains_with_semantic_type() {
504+
const SPEC: PropertySpec = PropertySpec::required("iban")
505+
.with_semantic_type(SemanticType::Iban)
506+
.with_marking(Marking::Financial);
507+
assert_eq!(SPEC.predicate, "iban");
508+
assert_eq!(SPEC.semantic_type, SemanticType::Iban);
509+
assert_eq!(SPEC.marking, Marking::Financial);
510+
}
511+
459512
#[test]
460513
fn below_floor_required() {
461514
let p = PropertySpec::required("tax_id");

0 commit comments

Comments
 (0)