Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions crates/lance-graph-contract/src/property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,42 +52,52 @@ pub struct PropertySpec {
pub nars_floor: Option<(u8, u8)>,
/// What kind of value this property holds (LF-21).
pub semantic_type: SemanticType,
/// GDPR / data-protection classification (LF-6 marking).
/// Default = `Marking::Internal` (GDPR-safe baseline).
/// Override per-predicate via `.with_marking(...)`.
pub marking: Marking,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid breaking PropertySpec struct literal initializers

Adding a new required public field to PropertySpec is a source-breaking API change for downstream crates that construct this type with struct literals (PropertySpec { ... }), which is likely in scope here because this crate is explicitly a shared contract for external consumers. Those callers will now fail to compile with a missing marking field, so this is not a zero-breaking change and should be gated by a compatibility strategy (e.g., avoid exposing required new fields directly in the public struct shape).

Useful? React with 👍 / 👎.

}

impl PropertySpec {
/// Create a Required property spec. Default codec: Passthrough (Index).
/// Default NARS floor: (128, 128) — moderate confidence required.
/// Default marking: `Marking::Internal` (GDPR-safe).
pub const fn required(predicate: &'static str) -> Self {
Self {
predicate,
kind: PropertyKind::Required,
codec_route: CodecRoute::Passthrough,
nars_floor: Some((128, 128)),
semantic_type: SemanticType::PlainText,
marking: Marking::Internal,
}
}

/// Create an Optional property spec. Caller must specify codec route.
/// No NARS floor by default (absence doesn't escalate).
/// Default marking: `Marking::Internal` (GDPR-safe).
pub const fn optional(predicate: &'static str, codec_route: CodecRoute) -> Self {
Self {
predicate,
kind: PropertyKind::Optional,
codec_route,
nars_floor: None,
semantic_type: SemanticType::PlainText,
marking: Marking::Internal,
}
}

/// Create a Free property spec. Default codec: CamPq (Argmax).
/// No NARS floor (schema-free, always accepted).
/// Default marking: `Marking::Internal` (GDPR-safe).
pub const fn free(predicate: &'static str) -> Self {
Self {
predicate,
kind: PropertyKind::Free,
codec_route: CodecRoute::CamPq,
nars_floor: None,
semantic_type: SemanticType::PlainText,
marking: Marking::Internal,
}
}

Expand All @@ -96,6 +106,14 @@ impl PropertySpec {
self
}

/// Override the GDPR / data-protection marking for this predicate (LF-6).
/// Default is `Marking::Internal`. SMB customer schema overrides:
/// `iban` → Financial, `geburtsdatum` → Pii, etc.
pub const fn with_marking(mut self, marking: Marking) -> Self {
self.marking = marking;
self
}

/// Override the NARS truth floor.
pub const fn with_nars_floor(mut self, frequency: u8, confidence: u8) -> Self {
self.nars_floor = Some((frequency, confidence));
Expand Down Expand Up @@ -456,6 +474,41 @@ mod tests {
assert!(p.nars_floor.is_none());
}

/// LF-6: every PropertySpec defaults to `Marking::Internal` (GDPR-safe).
#[test]
fn property_spec_marking_defaults_to_internal() {
assert_eq!(PropertySpec::required("kdnr").marking, Marking::Internal);
assert_eq!(PropertySpec::optional("note", CodecRoute::CamPq).marking, Marking::Internal);
assert_eq!(PropertySpec::free("free").marking, Marking::Internal);
}

/// SMB schema marking pattern: chain `with_marking` per predicate.
#[test]
fn property_spec_with_marking_overrides() {
let iban = PropertySpec::required("iban").with_marking(Marking::Financial);
let dob = PropertySpec::required("geburtsdatum").with_marking(Marking::Pii);
let note = PropertySpec::free("note"); // stays Internal

assert_eq!(iban.marking, Marking::Financial);
assert_eq!(dob.marking, Marking::Pii);
assert_eq!(note.marking, Marking::Internal);

// Per-row fold (W-2): `most_restrictive` over a row's markings.
let row_markings = [iban.marking, dob.marking, note.marking];
assert_eq!(Marking::most_restrictive(&row_markings), Marking::Financial);
}

/// `with_marking` is const and chains with `with_semantic_type` (LF-21).
#[test]
fn property_spec_with_marking_chains_with_semantic_type() {
const SPEC: PropertySpec = PropertySpec::required("iban")
.with_semantic_type(SemanticType::Iban)
.with_marking(Marking::Financial);
assert_eq!(SPEC.predicate, "iban");
assert_eq!(SPEC.semantic_type, SemanticType::Iban);
assert_eq!(SPEC.marking, Marking::Financial);
}

#[test]
fn below_floor_required() {
let p = PropertySpec::required("tax_id");
Expand Down
Loading