1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
//! House Rules — cross-workspace global memory tier decision cores
//! (bd-1n0np.10.2 / 10.3).
//!
//! 10.1 landed the [`crate::core::memory_scope::MemoryScope::Global`] lane and
//! the candidate-load union. This module is the pure, deterministic decision
//! logic on top of that substrate, kept free of CLI / DB / golden surfaces so it
//! is verifiable without RCH:
//!
//! - the **audited promotion gate** (10.2): a memory promotes to the global tier
//! only on explicit human marking or evidence from N distinct workspaces
//! (ADR-0006, "procedural memory requires evidence");
//! - the **capped house-rules quota** (10.3): global rules get a bounded share of
//! the pack budget so they never crowd out project context, with a
//! per-workspace opt-out.
//!
//! The CLI surfaces (`ee rule promote-global`, `ee remember --scope global`,
//! `ee insights --section houseRules`) and the audited curate transition wire
//! these decisions in; that wiring is the golden-gated follow-on.
use std::collections::BTreeSet;
use serde::Serialize;
/// Default number of distinct workspaces whose evidence justifies promoting a
/// memory to the global tier without explicit human marking (ADR-0006).
pub const DEFAULT_GLOBAL_PROMOTION_WORKSPACE_THRESHOLD: usize = 3;
/// Default share (basis points) of the pack budget reserved as the *cap* for the
/// house-rules section, so global rules never crowd out project context.
pub const DEFAULT_HOUSE_RULES_QUOTA_BASIS_POINTS: u32 = 2_000;
/// Why a memory is eligible for global-tier promotion.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", tag = "basis")]
pub enum GlobalPromotionBasis {
/// A human explicitly marked the memory for the global tier — always allowed.
ExplicitHumanMarking,
/// Evidence from `distinct_workspaces` (>= threshold) distinct workspaces.
CrossWorkspaceEvidence { distinct_workspaces: usize },
}
/// Audited promotion-gate decision for a candidate global-tier memory.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", tag = "action")]
pub enum GlobalPromotionDecision {
/// Promote to the global tier; `basis` records the justification for audit.
Promote { basis: GlobalPromotionBasis },
/// Deny promotion; the memory stays workspace-scoped.
Deny {
reason: &'static str,
distinct_workspaces: usize,
threshold: usize,
},
}
impl GlobalPromotionDecision {
/// Whether this decision promotes the memory to the global tier.
#[must_use]
pub const fn is_promote(&self) -> bool {
matches!(self, Self::Promote { .. })
}
}
/// Inputs to the promotion gate. `evidence_workspace_ids` is the set of distinct
/// workspaces that carry supporting evidence for the memory.
#[derive(Clone, Debug)]
pub struct GlobalPromotionGateInput<'a> {
pub evidence_workspace_ids: &'a BTreeSet<String>,
pub explicit_human_marking: bool,
pub threshold: usize,
}
/// Evaluate the audited global-promotion gate (bd-1n0np.10.2).
///
/// Explicit human marking always promotes (human authority). Otherwise the
/// memory must carry evidence from at least `threshold` distinct workspaces
/// (and the threshold must be non-zero). Pure and deterministic.
#[must_use]
pub fn evaluate_global_promotion_gate(
input: &GlobalPromotionGateInput<'_>,
) -> GlobalPromotionDecision {
if input.explicit_human_marking {
return GlobalPromotionDecision::Promote {
basis: GlobalPromotionBasis::ExplicitHumanMarking,
};
}
let distinct_workspaces = input.evidence_workspace_ids.len();
if input.threshold > 0 && distinct_workspaces >= input.threshold {
GlobalPromotionDecision::Promote {
basis: GlobalPromotionBasis::CrossWorkspaceEvidence {
distinct_workspaces,
},
}
} else {
GlobalPromotionDecision::Deny {
reason: "insufficient_cross_workspace_evidence",
distinct_workspaces,
threshold: input.threshold,
}
}
}
/// Inputs to the house-rules pack quota (bd-1n0np.10.3).
#[derive(Clone, Copy, Debug)]
pub struct HouseRulesQuotaInput {
pub total_budget_tokens: u64,
pub quota_basis_points: u32,
pub workspace_opted_out: bool,
}
/// The resolved house-rules section quota for one pack.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HouseRulesQuota {
/// Hard token cap for the house-rules section.
pub cap_tokens: u64,
/// `false` when the workspace opted out (cap is then zero).
pub enabled: bool,
}
/// Resolve the capped house-rules quota (bd-1n0np.10.3).
///
/// A per-workspace opt-out disables the section entirely (cap 0). Otherwise the
/// cap is `quota_basis_points` (clamped to 100%) of the total budget — a bounded
/// share so global rules never crowd out project context. Pure and deterministic.
#[must_use]
pub fn house_rules_quota(input: &HouseRulesQuotaInput) -> HouseRulesQuota {
if input.workspace_opted_out {
return HouseRulesQuota {
cap_tokens: 0,
enabled: false,
};
}
let basis_points = u64::from(input.quota_basis_points.min(10_000));
let cap_tokens = input.total_budget_tokens.saturating_mul(basis_points) / 10_000;
HouseRulesQuota {
cap_tokens,
enabled: true,
}
}
/// Greedily select house-rule items (in caller-supplied priority order) whose
/// cumulative token cost stays within `cap_tokens`. Deterministic: ties are
/// resolved by input order, and a single oversize item never blocks later
/// smaller items from filling the remaining cap (bd-1n0np.10.3 — global rules
/// never crowd out, and the section never overflows its quota).
#[must_use]
pub fn select_within_house_rules_quota(item_token_costs: &[u64], cap_tokens: u64) -> Vec<usize> {
let mut selected = Vec::new();
let mut used = 0_u64;
for (index, &cost) in item_token_costs.iter().enumerate() {
if used.saturating_add(cost) <= cap_tokens {
used = used.saturating_add(cost);
selected.push(index);
}
}
selected
}
#[cfg(test)]
mod tests {
use super::*;
fn workspaces(ids: &[&str]) -> BTreeSet<String> {
ids.iter().map(|id| (*id).to_owned()).collect()
}
#[test]
fn explicit_human_marking_always_promotes() {
let evidence = workspaces(&[]);
let decision = evaluate_global_promotion_gate(&GlobalPromotionGateInput {
evidence_workspace_ids: &evidence,
explicit_human_marking: true,
threshold: DEFAULT_GLOBAL_PROMOTION_WORKSPACE_THRESHOLD,
});
assert_eq!(
decision,
GlobalPromotionDecision::Promote {
basis: GlobalPromotionBasis::ExplicitHumanMarking
}
);
assert!(decision.is_promote());
}
#[test]
fn cross_workspace_evidence_promotes_at_or_above_threshold() {
let evidence = workspaces(&["ws_a", "ws_b", "ws_c"]);
let decision = evaluate_global_promotion_gate(&GlobalPromotionGateInput {
evidence_workspace_ids: &evidence,
explicit_human_marking: false,
threshold: 3,
});
assert_eq!(
decision,
GlobalPromotionDecision::Promote {
basis: GlobalPromotionBasis::CrossWorkspaceEvidence {
distinct_workspaces: 3
}
}
);
}
#[test]
fn insufficient_distinct_workspaces_is_denied() {
let evidence = workspaces(&["ws_a", "ws_b"]);
let decision = evaluate_global_promotion_gate(&GlobalPromotionGateInput {
evidence_workspace_ids: &evidence,
explicit_human_marking: false,
threshold: 3,
});
assert_eq!(
decision,
GlobalPromotionDecision::Deny {
reason: "insufficient_cross_workspace_evidence",
distinct_workspaces: 2,
threshold: 3,
}
);
assert!(!decision.is_promote());
}
#[test]
fn zero_threshold_without_marking_never_promotes() {
// A misconfigured zero threshold must not silently auto-promote everything.
let evidence = workspaces(&["ws_a"]);
let decision = evaluate_global_promotion_gate(&GlobalPromotionGateInput {
evidence_workspace_ids: &evidence,
explicit_human_marking: false,
threshold: 0,
});
assert!(!decision.is_promote());
}
#[test]
fn opt_out_disables_house_rules_section() {
let quota = house_rules_quota(&HouseRulesQuotaInput {
total_budget_tokens: 10_000,
quota_basis_points: DEFAULT_HOUSE_RULES_QUOTA_BASIS_POINTS,
workspace_opted_out: true,
});
assert_eq!(
quota,
HouseRulesQuota {
cap_tokens: 0,
enabled: false
}
);
}
#[test]
fn quota_is_a_bounded_share_of_the_budget() {
let quota = house_rules_quota(&HouseRulesQuotaInput {
total_budget_tokens: 10_000,
quota_basis_points: 2_000, // 20%
workspace_opted_out: false,
});
assert_eq!(
quota,
HouseRulesQuota {
cap_tokens: 2_000,
enabled: true
}
);
// Over-100% basis points clamp to the full budget, never above.
let clamped = house_rules_quota(&HouseRulesQuotaInput {
total_budget_tokens: 10_000,
quota_basis_points: 25_000,
workspace_opted_out: false,
});
assert_eq!(clamped.cap_tokens, 10_000);
}
#[test]
fn selection_stays_within_cap_and_does_not_let_one_item_block_others() {
// Costs: 30, 100 (too big), 40, 50. Cap 100 -> take 30, skip 100, take 40,
// skip 50 (30+40+50 > 100). A single oversize item never blocks later fits.
let selected = select_within_house_rules_quota(&[30, 100, 40, 50], 100);
assert_eq!(selected, vec![0, 2]);
let total: u64 = [30_u64, 100, 40, 50]
.iter()
.enumerate()
.filter(|(index, _)| selected.contains(index))
.map(|(_, cost)| *cost)
.sum();
assert!(total <= 100, "selection never exceeds the cap");
}
#[test]
fn empty_selection_when_cap_is_zero() {
assert!(select_within_house_rules_quota(&[10, 20], 0).is_empty());
}
}