Skip to content
Closed
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,9 @@
// 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 uses raw vector candidates and hydrated bodies,
// so it applies the default recall sensitivity ceiling itself. Drawers
// above `.elevated` are skipped before cue screening or snippet creation.
//
// Cost: O(probeLimit · K) index lookups + one batched body hydration
// for the surviving candidate pairs — never O(N²) over content.
Expand Down Expand Up @@ -168,6 +167,7 @@ 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 }
guard Self.isHuntRecallEligible(a), Self.isHuntRecallEligible(b) 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 Expand Up @@ -229,6 +229,12 @@ public extension GeniusLocusKit {
deduplicated: deduplicated)
}

/// Default MCP hunt disclosure ceiling, matching recall's default
/// `.sensitivityAtMost(.elevated)` posture.
internal static func isHuntRecallEligible(_ drawer: Drawer) -> Bool {
drawer.adjectiveSensitivity.rawValue <= AdjectiveSensitivity.elevated.rawValue

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 the raw sensitivity field, not the decoded enum

For drawers whose adjective bitmap contains a reserved/intermediate sensitivity raw value above elevated (for example a future tier at raw 24), drawer.adjectiveSensitivity falls back to .normal, so this check lets the pair reach ConflictCue and can still emit borderline snippets. Default recall's .sensitivityAtMost(.elevated) is enforced as a raw bitmap threshold, so this helper is not actually equivalent to the recall ceiling it is trying to mirror; compare the extracted raw field to 16 instead of comparing the lossy decoded enum.

Useful? React with 👍 / 👎.

}

/// Canonical unordered pair key — lexicographically smaller ID first.
internal static func pairKey(_ a: String, _ b: String) -> String {
a < b ? "\(a)||\(b)" : "\(b)||\(a)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,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 All @@ -55,9 +60,12 @@ struct ContradictionHuntTests {
engram: Fingerprint256,
kit: GeniusLocusKit,
handle: EstateHandle,
vectorStore: VectorStore
vectorStore: VectorStore,
sensitivity: AdjectiveSensitivity = .normal
) async throws -> Drawer {
let drawer = try await kit.capture(handle, captureFrame(content: content, room: "study"))
let drawer = try await kit.capture(
handle,
captureFrame(content: content, room: "study", sensitivity: sensitivity))
try await vectorStore.addVector(
itemID: drawer.id, engram: engram, modelID: Self.modelID,
modelVersion: "1.0", filedAt: Self.t0)
Expand Down Expand Up @@ -155,6 +163,25 @@ struct ContradictionHuntTests {
#expect(contradicts.isEmpty)
}

@Test("restricted and secret drawers are excluded before snippet disclosure")
func restrictedAndSecretDrawersAreExcluded() async throws {
let (kit, handle, vectorStore) = try await makeKit()
try await plant(
"Bob lives in Paris",
engram: near, kit: kit, handle: handle, vectorStore: vectorStore,
sensitivity: .restricted)
try await plant(
"Bob does not live in Paris",
engram: near, kit: kit, handle: handle, vectorStore: vectorStore,
sensitivity: .secret)

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
58 changes: 57 additions & 1 deletion packages/kits/GeniusLocusKit/rust/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2759,6 +2759,13 @@ impl EstateCoordinator {
if a.tombstoned_at.is_some() || b.tombstoned_at.is_some() {
continue;
}
let max_hunt_sensitivity =
locus_kit::adjectives::AdjectiveSensitivity::Elevated.raw_value();
if a.adjective_sensitivity().raw_value() > max_hunt_sensitivity
|| b.adjective_sensitivity().raw_value() > max_hunt_sensitivity
Comment on lines +2764 to +2765

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 the raw sensitivity field, not the decoded enum

When a drawer has a reserved/intermediate adjective-sensitivity raw value above Elevated, adjective_sensitivity() decodes it through from_raw and falls back to Normal, causing this gate to allow the pair even though the default recall ceiling uses a raw SensitivityAtMost(Elevated) threshold and would exclude it. This leaves a disclosure gap for newer/imported rows using reserved sensitivity values; the hunt gate should compare the raw bits from adjective_bitmap directly.

Useful? React with 👍 / 👎.

{
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 @@ -8910,7 +8917,27 @@ mod tests {
content: &str,
engram: &engram_lib::Engram,
) -> locus_kit::drawer::Drawer {
let drawer = coord.capture(h, cap_frame(content), NOW).expect("capture");
hunt_plant_with_sensitivity(
coord,
h,
vs,
content,
engram,
locus_kit::adjectives::AdjectiveSensitivity::Normal,
)
}

fn hunt_plant_with_sensitivity(
coord: &EstateCoordinator,
h: &EstateHandle,
vs: &VectorStore,
content: &str,
engram: &engram_lib::Engram,
sensitivity: locus_kit::adjectives::AdjectiveSensitivity,
) -> locus_kit::drawer::Drawer {
let mut frame = cap_frame(content);
frame.sensitivity = sensitivity;
let drawer = coord.capture(h, frame, NOW).expect("capture");
vs.add_vector(&drawer.id, engram, "minilm-v6", "1.0", NOW)
.expect("add_vector");
drawer
Expand Down Expand Up @@ -9007,6 +9034,35 @@ mod tests {
assert_eq!(contradicts, 0);
}

#[test]
fn hunt_restricted_and_secret_drawers_are_excluded() {
let (coord, h, vs) = open_one_with_vectors();
let near = hunt_near();
hunt_plant_with_sensitivity(
&coord,
&h,
&vs,
"Bob lives in Paris",
&near,
locus_kit::adjectives::AdjectiveSensitivity::Restricted,
);
hunt_plant_with_sensitivity(
&coord,
&h,
&vs,
"Bob does not live in Paris",
&near,
locus_kit::adjectives::AdjectiveSensitivity::Secret,
);

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