Skip to content
Merged
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
// any lifecycle, including `.withdrawn` (a rejected review) and
// tombstoned rows — is never proposed again. Rejection is durable.
//
// Sensitivity: `DrawerStore.addTunnel` stamps the tunnel with the MAX
// of its endpoints' sensitivities (#57, both legs), so a proposed edge
// touching a restricted drawer is gated by the tunnel sensitivity
// ceiling automatically; the hunter adds nothing to disclose.
// Sensitivity: the hunt applies the same default ceiling as recall
// (`.sensitivityAtMost(.elevated)`) before content screening or
// snippet generation. Proposed tunnels are then additionally stamped
// by `DrawerStore.addTunnel` with the MAX endpoint sensitivity (#57).
//
// Cost: O(probeLimit · K) index lookups + one batched body hydration
// for the surviving candidate pairs — never O(N²) over content.
Expand Down Expand Up @@ -221,6 +221,13 @@ public extension GeniusLocusKit {
for pair in candidatePairs {
guard let a = drawersByID[pair.a], let b = drawersByID[pair.b],
a.tombstonedAt == nil, b.tombstonedAt == nil else { continue }
// Match BitmapEvaluator's default recall posture: callers
// without an explicit sensitivity grant may only mine the Normal
// tier (normal + elevated). Restricted/secret rows must not be
// screened, proposed, or echoed as borderline snippets.
guard a.adjectiveSensitivity.rawValue <= AdjectiveSensitivity.elevated.rawValue,
b.adjectiveSensitivity.rawValue <= AdjectiveSensitivity.elevated.rawValue
Comment on lines +228 to +229

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Compare raw sensitivity bits before screening

This guard decodes through Drawer.adjectiveSensitivity before comparing, but that accessor falls back to .normal for any unrecognized 6-bit sensitivity value, whereas BitmapEvaluator's default .sensitivityAtMost(.elevated) compares the raw field. If an estate contains a future-version/imported/corrupt row with sensitivity bits above 16 but not one of the current enum cases, it passes this check and can still be screened or echoed in borderline snippets, violating the intended default ceiling. Compare the raw extracted sensitivity bits instead, and apply the same fix to the Rust mirror.

Useful? React with 👍 / 👎.

else { continue }
// Incremental watermark: at least one side must be new enough.
if let watermark = filedAfter,
a.filedAt <= watermark, b.filedAt <= watermark { continue }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,19 @@ struct ContradictionHuntTests {
return (kit, handle, vectorStore)
}

private func captureFrame(content: String, room: String) -> CaptureFrame {
private func captureFrame(
content: String,
room: String,
sensitivity: AdjectiveSensitivity = .normal
) -> CaptureFrame {
CaptureFrame(
content: content,
channel: .typed,
room: room,
latticeAnchor: LatticeAnchor(udcCode: "004"),
addedBy: "hunt-tests",
embeddingModelID: Self.modelID
embeddingModelID: Self.modelID,
sensitivity: sensitivity
)
}

Expand Down Expand Up @@ -156,6 +161,32 @@ struct ContradictionHuntTests {
#expect(contradicts.isEmpty)
}

@Test("restricted and secret candidates are excluded by the default hunt sensitivity ceiling")
func protectedCandidatesAreExcludedByDefaultCeiling() async throws {
let (kit, handle, vectorStore) = try await makeKit()
let normal = try await kit.capture(
handle,
captureFrame(content: "Bob lives in Paris", room: "study"))
let secret = try await kit.capture(
handle,
captureFrame(
content: "Bob does not live in Paris SECRET-DO-NOT-ECHO",
room: "study",
sensitivity: .secret))

for drawer in [normal, secret] {
try await vectorStore.addVector(
itemID: drawer.id, engram: near, modelID: Self.modelID,
modelVersion: "1.0", filedAt: Self.t0)
}

let report = try await kit.huntContradictions(in: handle, now: Self.t0)

#expect(report.proposed.isEmpty)
#expect(report.borderline.isEmpty)
#expect(report.pairsScreened == 0)
}

@Test("unrelated content proposes nothing; missing vector store is reported")
func guards() async throws {
let (kit, handle, vectorStore) = try await makeKit()
Expand Down
36 changes: 36 additions & 0 deletions packages/kits/GeniusLocusKit/rust/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2839,6 +2839,17 @@ impl EstateCoordinator {
if a.tombstoned_at.is_some() || b.tombstoned_at.is_some() {
continue;
}
// Match BitmapEvaluator's default recall posture: callers
// without an explicit sensitivity grant may only mine the Normal
// tier (normal + elevated). Restricted/secret rows must not be
// screened, proposed, or echoed as borderline snippets.
if a.adjective_sensitivity().raw_value()
> locus_kit::adjectives::AdjectiveSensitivity::Elevated.raw_value()
|| b.adjective_sensitivity().raw_value()
> locus_kit::adjectives::AdjectiveSensitivity::Elevated.raw_value()
{
continue;
}
// Incremental watermark: at least one side must be new enough.
if let Some(watermark) = filed_after {
if a.filed_at <= watermark && b.filed_at <= watermark {
Expand Down Expand Up @@ -9087,6 +9098,31 @@ mod tests {
assert_eq!(contradicts, 0);
}

#[test]
fn hunt_protected_candidates_are_excluded_by_default_ceiling() {
let (coord, h, vs) = open_one_with_vectors();
let near = hunt_near();
let normal = coord
.capture(&h, cap_frame("Bob lives in Paris"), NOW)
.expect("capture normal");
let mut secret_frame = cap_frame("Bob does not live in Paris SECRET-DO-NOT-ECHO");
secret_frame.sensitivity = locus_kit::adjectives::AdjectiveSensitivity::Secret;
let secret = coord
.capture(&h, secret_frame, NOW)
.expect("capture secret");
for drawer in [&normal, &secret] {
vs.add_vector(&drawer.id, &near, "minilm-v6", "1.0", NOW)
.expect("add vector");
}

let report = coord
.hunt_contradictions(&h, "minilm-v6", 50, None, 64, NOW)
.expect("hunt");
assert!(report.proposed.is_empty());
assert!(report.borderline.is_empty());
assert_eq!(report.pairs_screened, 0);
}

#[test]
fn hunt_guards_and_missing_vector_store() {
let (coord, h, vs) = open_one_with_vectors();
Expand Down
Loading