vacant.reputation¶
P3 reputation — 5-dimensional Beta posterior per (vacant, substrate),
UCB exploration with cold-start §3.6 mechanism, STYLO drift discount,
portability factor, same-controller / same-substrate / same-stylo
detection (cost-raising, never preventing).
posterior
¶
Beta posterior + Beta5D -- five-dimensional reputation state.
Per P3 §3.1-§3.2 each reputation event updates a per-dimension Beta
posterior with time-decayed prior weight. The per-dimension half-life
is set in core.constants.DIM_HALF_LIFE_DAYS.
Update rule (§3.2):
gamma = exp(-ln(2) * Deltat / half_life_d)
alpha ← alpha0 + gamma * (alpha - alpha0) # decay accumulated evidence, keep prior
beta ← beta0 + gamma * (beta - beta0)
n_eff ← gamma * n_eff
alpha ← alpha + w * s
beta ← beta + w * (1 - s)
n_eff ← n_eff + w
s ∈ [0, 1] is the signal's positive rate; w is the source-weighted
evidence contribution.
Beta
¶
Bases: BaseModel
Single-dimension Beta posterior with time-decay state.
alpha0
class-attribute
instance-attribute
¶
Prior alpha0 -- kept across decays so the prior never erodes.
n_eff
class-attribute
instance-attribute
¶
Effective sample size = alpha + beta - alpha0 - beta0 (post-decay). Tracks how much evidence the posterior carries beyond the prior.
last_update_ts
class-attribute
instance-attribute
¶
Unix-epoch seconds. Used to compute decay against the current time.
decayed
¶
decayed(*, now_ts: float | datetime) -> Beta
Return a new Beta with alpha, beta, n_eff decayed to now_ts.
The prior never decays -- only the accumulated evidence above the
prior shrinks (P3 §3.2). Identity if now_ts <= last_update_ts.
Source code in src/vacant/reputation/posterior.py
update
¶
update(*, positive_weight: float, negative_weight: float, now_ts: float | datetime) -> Beta
Decay to now_ts then apply a weighted positive/negative pulse.
positive_weight and negative_weight are the w * s and
w * (1 - s) terms from §3.2. Both must be >= 0; the caller has
already factored s ∈ [0, 1] into them.
Source code in src/vacant/reputation/posterior.py
update_with_signal
¶
update_with_signal(*, signal: float, weight: float, now_ts: float | datetime) -> Beta
Convenience wrapper: split (signal, weight) into pos/neg pulses.
signal is the positive rate ∈ [0, 1]; weight >= 0 is the
source-weighted evidence contribution.
Source code in src/vacant/reputation/posterior.py
Beta5D
¶
Bases: BaseModel
Five-dimensional reputation state for one (vacant, substrate) pair.
decay_factor
¶
Exponential half-life decay factor: exp(-ln(2) * Deltat_days / half_life_d).
Source code in src/vacant/reputation/posterior.py
five_d_with_priors
¶
five_d_with_priors(*, now_ts: float | datetime = 0.0) -> Beta5D
Construct a Beta5D with the canonical base priors.
Cold-start adjustments (L1 attestation, stake, vouchers, sibling
inheritance) are applied separately in cold_start.py.
Source code in src/vacant/reputation/posterior.py
aggregator
¶
Reputation aggregator -- the public API surface that the registry queries.
Implements P4's vacant.registry.aggregation.ReputationOracle Protocol
(via score(vacant_id, dimensions)), plus the richer dispatch §7 API:
get_reputation(vid, substrate) -> Beta5Dget_ranked(capability_query, n) -> list[(VacantId, score)]record_review(reviewer, target, dimensions, substrate) -> None
record_review enforces the dispatch's reviewer-eligibility check:
reviews from SUNK / ARCHIVED / STALE vacants are rejected at the API
surface (P1 can_review), and reviews from suspected-collusion sets
are downweighted via the same-* detector signals.
VacantContext
dataclass
¶
VacantContext(vacant_id: VacantId, base_model_family: str = 'unknown', state: VacantState = ACTIVE, capability_text: str = '', attestation_level: str = 'L0', stake_amount: float = 0.0)
Per-vacant metadata the aggregator tracks alongside its posterior.
Sourced from P4's vacant table when wired in. Held here so unit
tests can run without a registry.
ReviewRecord
dataclass
¶
ReviewRecord(reviewer: VacantId, target: VacantId, dimensions: dict[str, float], substrate: str, source: str = 'peer_review', ts: float = (lambda: time())(), same_signals: tuple[SameDetectSignal, ...] = ())
One review event submitted to record_review.
Aggregator
¶
Aggregator(contexts: dict[VacantId, VacantContext] | None = None, *, review_limit_per_target_24h: int | None = None, logbooks: dict[VacantId, Logbook] | None = None, signing_keys: dict[VacantId, SigningKey] | None = None)
In-memory reputation aggregator. Persists state externally via P4.
Construction: pass a registry of VacantContext keyed by VacantId.
Tests typically build this fresh per-case; the demo dashboard
constructs one and seeds from the registry's vacant table.
Source code in src/vacant/reputation/aggregator.py
add_context
¶
add_context(ctx: VacantContext) -> None
register_audit
¶
Attach a Logbook + SigningKey for vid. Registering ANY
reviewer flips the aggregator into audit-aware mode for the rest
of its lifetime: subsequent record_review calls require the
reviewer to be registered, otherwise MissingAuditKeyError.
Source code in src/vacant/reputation/aggregator.py
get_reputation
async
¶
Return the per-substrate Beta5D, building a cold-start prior if absent.
Source code in src/vacant/reputation/aggregator.py
get_ranked
async
¶
get_ranked(capability_query: str, n: int, *, substrate: str = 'default', weights: Mapping[str, float] | None = None) -> list[tuple[VacantId, float]]
UCB-scored top-N candidates for a capability query.
Filters to is_runnable vacants whose capability_text contains
the query as a substring (cheap MVP search; P4's aggregation
layer does the real index lookup).
Source code in src/vacant/reputation/aggregator.py
record_review
async
¶
record_review(reviewer: VacantId, target: VacantId, dimensions: Mapping[str, float], substrate: str, *, source: str = 'peer_review', same_signals: Sequence[SameDetectSignal] = (), ts: float | None = None) -> None
Apply a review to the target's posterior. Raises
IneligibleReviewerError if reviewer's runtime state forbids
new reviews (P1 §4.1) and InvalidDimensionError for unknown
dims.
Source code in src/vacant/reputation/aggregator.py
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 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 | |
score
async
¶
Implements vacant.registry.aggregation.ReputationOracle.score.
vacant_id is the hex form (P4's storage convention). We map it
back to a VacantId against our context registry.
Source code in src/vacant/reputation/aggregator.py
apply_drift_discount
async
¶
apply_drift_discount(vid: VacantId, *, substrate: str, discount: float) -> None
Apply a STYLO-distance discount to a (vacant, substrate) posterior. Called by P1 / shadow-self when drift is detected.
Source code in src/vacant/reputation/aggregator.py
ucb
¶
UCB scoring for vacant selection.
P3 §3.7 extends the standard UCB1 (DRF arXiv:2509.05764) to multi-dim + uncertainty-aware:
mu_w = Sum w_d * mu_d
sigma_w = √(Sum w_d^2 * sigma_d^2) # treats dims as independent
n_w = harmonic_mean(n_eff_d for w_d > 0.05)
explore = c_explore * √(log N / max(n_w, 1))
score = mu_w + c * sigma_w + explore
Lineage and UCB scoring are decoupled (D015 §B). parent_id is caller-
side metadata — useful for filtering ("show me descendants of root R")
or sort tie-breaks — but the parent's posterior MUST NOT bleed into the
child's UCB score. CLAUDE.md §Load-bearing theory decisions makes the
lineage-not-individuals-evolve invariant load-bearing: individuals do
not inherit reputation from their parent; new lineage members reset
the clock.
lineage_prior_alpha is kept as a public helper for research callers
that explicitly want a lineage-weighted Beta prior outside the UCB path
(e.g. seeding a research probe). It is not used by ucb_score,
call_score, or ucb_with_lineage_prior.
ucb_score
¶
ucb_score(rep: Beta5D, *, weights: Mapping[str, float] | None = None, n_global: int = 1, c_base: float = UCB_C_BASE, c_explore: float = UCB_C_EXPLORE, significant_weight: float = 0.05) -> float
Multi-dim Bayesian UCB. P3 §3.7.
Source code in src/vacant/reputation/ucb.py
lineage_prior_alpha
¶
lineage_prior_alpha(*, base_alpha: float, base_beta: float, parent_alpha: float, parent_beta: float, depth: int, inherit_fraction: float = 0.25, decay_lambda: float = 0.5) -> tuple[float, float]
Lineage prior shaping (§4.3): blend parent posterior into child prior.
kappa(d) = inherit_fraction * exp(-decay_lambda * d) shrinks with
lineage depth so a long fork chain doesn't trivially inherit root.
Returns the blended (alpha, beta) for the child's prior.
Source code in src/vacant/reputation/ucb.py
ucb_with_lineage_prior
¶
ucb_with_lineage_prior(child_beta: Beta, parent_beta: Beta | None = None, *, n_global: int, depth: int = 0, c_explore: float = UCB_C_EXPLORE, inherit_fraction: float = 0.25, decay_lambda: float = 0.5) -> float
Single-dim UCB on a child posterior. Parent posterior is ignored.
D015 §B: lineage is caller-side metadata; individual vacants do not
inherit reputation from their parent (CLAUDE.md §Load-bearing theory
decisions). The parameters parent_beta, depth, inherit_fraction,
decay_lambda are accepted for back-compatible call sites and for
future caller-side filtering (depth may still be used to sort
descendants), but they have no effect on the score.
Use lineage_prior_alpha(...) directly if you genuinely need a
lineage-weighted Beta prior outside the UCB pipeline.
Source code in src/vacant/reputation/ucb.py
exploration_boost
¶
exploration_boost(*, n_eff: float, n_min: int = N_MIN_FOR_STABLE_SCORE, n_global: int, c_explore: float = UCB_C_EXPLORE) -> float
Cold-start exploration bonus (§3.8 stage 2): boost UCB explore term
while n_eff < n_min. Boost is 1 + (n_min - n_eff) / n_min,
multiplied into the explore term.
Source code in src/vacant/reputation/ucb.py
cold_start_floor
¶
call_score
¶
call_score(rep: Beta5D, *, weights: Mapping[str, float] | None = None, n_global: int, stake_amount: float = 0.0, attestation_level: str = 'L0', portability_bonus: float = 0.0, c_base: float = UCB_C_BASE, c_explore: float = UCB_C_EXPLORE) -> float
Full call scoring (§3.7 + §3.8):
score = ucb(...) + stake_bonus + att_floor + portability_bonus
stake_bonus = 0.1 * log(1 + stake / S_REF)-- affects exploration tolerance, not the mean (anti-Goodhart-against-stake §3.7).att_floorfromcold_start_floor(attestation_level).portability_bonusis supplied byportability.py(capped).
Source code in src/vacant/reputation/ucb.py
cold_start
¶
Cold-start mechanism (P3 §3.8 + dispatch §4).
Five components, per dispatch:
- UCB exploration -- implemented in
ucb.py(exploration_boost). - Birth-path startup signals --
birth_path_bonusenum table. - Niche uniqueness bonus --
niche_bonusfrom capability-supply count. - Low-stakes probes --
is_eligible_for_low_stakes_probepolicy hook. - Idle peer review --
should_idle_review_targetpolicy hook.
Stage 1 (initial prior, §3.8) is initial_prior(...): takes attestation
level / stake / vouchers / sibling and returns a Beta5D.
BirthPath
¶
Bases: StrEnum
Birth-path enumeration (THEORY_V5 §3.6).
- PATH_ZERO: human-built infrastructure (one-time).
- PATH_B: subagent graduation (legacy; permanent transitional path).
- PATH_C: client-mediated spawn (transitional; folds into D5 long-term).
- D1..D5: agent self-replication paths (THEORY_V5 §3.6 D-series).
ColdStartCaveats
dataclass
¶
Caveats accompanying a reputation read for new vacants.
InsufficientDataLabel
dataclass
¶
InsufficientDataLabel(show_scalar: bool, label: str, caveats: ColdStartCaveats)
Surface-level marker returned to UI clients when n_eff is low.
birth_path_bonus
¶
birth_path_bonus(path: BirthPath) -> tuple[float, float]
Return (alpha_boost for F/L/R each, alpha_boost for H) per birth path.
D-series paths return small boosts because lineage prior shaping
(see ucb.lineage_prior_alpha) carries the parent-reputation signal.
Source code in src/vacant/reputation/cold_start.py
initial_prior
¶
initial_prior(*, attestation_level: str = 'L0', stake_amount: float = 0.0, n_l1_plus_vouchers: int = 0, sibling: Beta5D | None = None, birth_path: BirthPath | None = None, now_ts: float = 0.0) -> Beta5D
Build a cold-start Beta5D per P3 §3.8 stage 1.
Composition order:
- Base priors (CONSTANTS.md §Reputation, see D008 §A).
- L1 attestation: +
L1_ATTESTATION_ALPHA_BOOSTto F/L/R alpha (skipped for L0). - Stake bonus:
min(2.0, log(1 + stake/S_REF))split half-each across F/L/R. - L3 vouches:
+L3_VOUCH_ALPHA_BOOST * n_l1_plus_vouchersto H alpha. - Sibling inheritance under same owner:
alpha/4,beta/4blended in (capped, decayed evidence). - Birth-path boost: per
birth_path_bonustable.
Source code in src/vacant/reputation/cold_start.py
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 | |
niche_bonus
¶
niche_bonus(*, capability_supply: int, saturation_supply: int = 10, max_bonus: float = 0.1) -> float
Niche uniqueness bonus: rarer capability → larger bonus.
capability_supply is the count of vacants currently offering this
capability. The bonus is max_bonus * (1 - supply / saturation),
floored at 0.
Intuition: a niche with 1 supplier should get the full max_bonus;
once 10+ vacants compete on the same capability, the bonus is 0.
Source code in src/vacant/reputation/cold_start.py
is_eligible_for_low_stakes_probe
¶
is_eligible_for_low_stakes_probe(rep: Beta5D, *, n_min: int = N_MIN_FOR_STABLE_SCORE) -> bool
Policy hook: caller-side proxy routes a small fraction of low-stakes
requests to vacants that have not yet crossed n_min. Reactivated by
P4 / P7 caller routing.
Source code in src/vacant/reputation/cold_start.py
should_idle_review_target
¶
should_idle_review_target(*, reviewer_idle_seconds: float, target_n_eff_min: float, n_min: int = N_MIN_FOR_STABLE_SCORE, idle_threshold_s: int = IDLE_REVIEW_THRESHOLD_S) -> bool
Policy hook: an idle reviewer should peer-review a target with
n_eff < n_min. P1 idle-time scheduler consumes this.
Source code in src/vacant/reputation/cold_start.py
show_label
¶
show_label(rep: Beta5D, *, n_show: int = N_SHOW_MIN_THRESHOLD) -> InsufficientDataLabel
Return whether to show a scalar reputation or INSUFFICIENT_DATA.
n_show=10 per CONSTANTS.md / D008 §A: if any dim is below this,
callers must not render a scalar score; show the caveat instead.
Source code in src/vacant/reputation/cold_start.py
discount
¶
STYLO-distance reputation discount (P3 §4.3 / dispatch §3).
When a vacant's behavioral fingerprint drifts substantially between epochs, the old evidence is less informative about the new behavior. The discount shrinks the effective sample size (alpha and beta scale together, preserving the mean while widening uncertainty).
This is the mechanism that bites self-evolution at the individual
vacant level. New lineage members get a clean posterior -- see
reputation/cold_start.py::initial_prior and §4.3 ("lineage as the
subject of evolution").
Discount curve:
distance = 0 → discount = 1.0 (no change)
distance ~ 1.5sigma → discount ~ 0.85
distance ~ STYLO_DRIFT_THRESHOLD (3.5) → discount ~ 0.40
distance → ∞ → discount → discount_floor (0.10)
compute_discount is shaped as a sigmoid centered just below the drift
threshold; apply_discount rescales alpha and beta toward their priors so the
posterior preserves its mean while shedding evidence.
CumulativeDriftTracker
¶
CumulativeDriftTracker(*, window: int = CUMULATIVE_DRIFT_WINDOW_EPOCHS, threshold: float = STYLO_DRIFT_THRESHOLD, threshold_multiplier: float = CUMULATIVE_DRIFT_THRESHOLD_MULTIPLIER)
Rolling-window sum of per-epoch STYLO drifts.
Single-shot compute_discount is fooled by an attacker who keeps
each epoch's drift just below STYLO_DRIFT_THRESHOLD while
accumulating change across many epochs (P3 §3.4 Padv-P3 §2). This
tracker keeps a rolling window of the last window per-epoch
drifts and trips when their sum exceeds
threshold * threshold_multiplier.
Tracker is per (vacant, substrate); the aggregator owns one per target it scores.
Source code in src/vacant/reputation/discount.py
compute_discount
¶
compute_discount(stylo_distance: float, *, threshold: float = STYLO_DRIFT_THRESHOLD) -> float
Map STYLO distance → discount multiplier in (discount_floor, 1.0].
Curve shape:
- distance == 0 → 1.0 (no change).
- Smooth sigmoid descent passing through ~0.85 at _MIDPOINT_OFFSET.
- Approaches _DISCOUNT_FLOOR as distance → ∞.
Source code in src/vacant/reputation/discount.py
apply_discount
¶
Shrink alpha and beta toward priors by discount ∈ (0, 1].
Preserves the mean (alpha / (alpha+beta)) but reduces effective sample size:
alpha' = alpha0 + discount * (alpha - alpha0)
beta' = beta0 + discount * (beta - beta0)
n_eff' = discount * n_eff
Source code in src/vacant/reputation/discount.py
apply_discount_5d
¶
Apply a single discount to all five dimensions.
Source code in src/vacant/reputation/discount.py
dimension_imbalance_alert
¶
dimension_imbalance_alert(rep: Beta5D, *, threshold: float | None = None) -> bool
Detect dimension imbalance (P3 §3.6 防線 4 / dispatch §"Dimension imbalance").
True iff at least one dimension's mean exceeds the threshold's gap above the lowest dimension's mean. This catches the attack pattern "pump only F while leaving A low":
- all-similar means → False (healthy distribution)
- one dim spiked while others stay near prior → True (imbalanced)
threshold defaults to DIMENSION_CORRELATION_ALERT_THRESHOLD = 0.6.
The implementation uses max - min as a quick proxy for the
correlation alert; a full pairwise correlation matrix is future work.
Source code in src/vacant/reputation/discount.py
portability
¶
Portability factor (dispatch §6 / P3 §"resilience-as-independent-metric").
THEORY_V5 §3.1 split portability out of raw reputation: it became an
independent resilience_score. The dispatch §6 still asks for a small
call_score bonus rewarding ecological contribution -- vacants that
serve across multiple substrates with high success.
Implementation:
raw = sum(success_rate_per_substrate)
diversity = log(1 + n_substrates)
portability = clip(raw * diversity_norm, 0, MAX_BONUS)
The bonus is capped at PORTABILITY_FACTOR_MAX_BONUS so it can't
dominate the UCB call_score (anti-Goodhart-against-portability).
compute_portability
¶
compute_portability(*, substrates_served: list[str], success_rate_per_substrate: Mapping[str, float], max_bonus: float = PORTABILITY_FACTOR_MAX_BONUS) -> float
Return a portability bonus in [0, max_bonus].
substrates_servedis the list of substrates this vacant has executed on.success_rate_per_substrateis the per-substrate success rate ∈ [0, 1].- Bonus is
max_bonus * diversity_factor * success_factor, where:diversity_factorsaturates at 1.0 oncen_substrates >= 4(log curve with reference 4).success_factor = mean(success_rates over served substrates).
Source code in src/vacant/reputation/portability.py
same_detect
¶
Same-* detection (P3 §3.4 / T5 / dispatch §5).
Three lines, each returning a SameDetectSignal:
same_controller: T5's three-layer pipeline (declared link → temporal correlation → behavioural similarity).same_substrate: shared base-model family.same_stylo: STYLO-Vec16 cosine similarity (consumes P1'sshadow_self.compute_embedding).
Framing (CLAUDE.md §Things to NOT do): these raise cost, they do not prevent. The signal output biases per-review weight in the aggregator; nothing here blocks writes.
SameDetectSignal
dataclass
¶
SameDetectSignal(strength: float, suspected_cluster: frozenset[VacantId], rationale: str)
Output of every same-* detector.
strength ∈ [0, 1] is monotone in suspicion. The aggregator uses
max(SAME_SIGNAL_DISCOUNT_FLOOR, 1 - max(strength)) as a per-review
cost-raising multiplier, so strength=0 → no penalty and strength=1
leaves at least the floor (CLAUDE.md «same-* is cost-raising not
preventing» — D015).
suspected_cluster includes both probe and target vacant ids when
the signal fires; empty when strength is 0.
cosine_similarity
¶
Cosine similarity in [-1, 1] (returns 0.0 for zero-norm inputs).
Source code in src/vacant/reputation/same_detect.py
cross_correlation
¶
Pearson correlation coefficient ∈ [-1, 1] for two equal-length series. Returns 0.0 if either has zero variance.
Source code in src/vacant/reputation/same_detect.py
same_controller
¶
same_controller(a: VacantId, b: VacantId, *, declared_same: bool = False, common_ancestor: bool = False, heartbeat_a: Sequence[float] | None = None, heartbeat_b: Sequence[float] | None = None, behavior_a: Sequence[float] | None = None, behavior_b: Sequence[float] | None = None, temporal_threshold: float = SAME_CONTROLLER_TEMPORAL_THRESHOLD, behavior_threshold: float = SAME_CONTROLLER_BEHAVIOR_THRESHOLD) -> SameDetectSignal
T5 §3.2 three-layer same-controller detection.
Layer 0 (declared): controller_id match or shared ancestor → 1.0.
Layer 1 (temporal): cross-correlation of heartbeat series; strength
proportional to (corr - threshold) / (1 - threshold).
Layer 2 (behaviour): cosine similarity over behavioural fingerprints
(capability text embedding or STYLO Vec16); strength proportional
to (sim - threshold) / (1 - threshold).
The detector takes the max of all layers that fire -- once any layer is positive, downstream weight discount applies.
Source code in src/vacant/reputation/same_detect.py
same_substrate
¶
same_substrate(a: VacantId, b: VacantId, *, family_a: str, family_b: str) -> SameDetectSignal
Strength = 1.0 iff family_a == family_b, else 0.
"Strength = 1" means full P3 §3.4.1 discount applies; the aggregator
halves weight (and quarter-weight if many recent reviews) at the
review-weighting step. The actual numeric discount lives in
aggregator.py; this detector just emits the binary signal.
Source code in src/vacant/reputation/same_detect.py
same_stylo
¶
same_stylo(a: VacantId, b: VacantId, *, embedding_a: Sequence[float], embedding_b: Sequence[float], threshold: float = SAME_CONTROLLER_BEHAVIOR_THRESHOLD) -> SameDetectSignal
STYLO-Vec16 similarity detector.
Consumes embeddings produced by P1's
vacant.runtime.shadow_self.compute_embedding (or the real STYLO
encoder once it lands). Strength is (sim - threshold) /
(1 - threshold) clipped to [0, 1].
Source code in src/vacant/reputation/same_detect.py
discount_from_signals
¶
discount_from_signals(signals: Sequence[SameDetectSignal]) -> float
Compose multiple SameDetectSignals into a single weight multiplier.
max(SAME_SIGNAL_DISCOUNT_FLOOR, 1 - max(strength)) is conservative:
any one detector firing reduces weight by its strength; the strongest
detector dominates so we don't compound penalties (the dispatch's
explicit framing — these are signals, not evidence to be summed).
The floor (D015) is load-bearing: same- detection is cost-raising,
not preventing* (CLAUDE.md §Load-bearing theory decisions). Even at
strength=1.0 we must not zero a reviewer's contribution — that
would convert a probabilistic suspicion into a unilateral mute.
Source code in src/vacant/reputation/same_detect.py
errors
¶
Error hierarchy for vacant.reputation.
IneligibleReviewerError
¶
Bases: ReputationError
A reviewer's runtime state forbids new reviews (P1 §4.1).
InvalidDimensionError
¶
Bases: ReputationError
An unknown reputation dimension was referenced.
InvalidSignalError
¶
Bases: ReputationError
A signal is malformed (e.g. score outside [0,1]).
ReviewRateLimitError
¶
Bases: ReputationError
A target's per-window review rate limit was exceeded (Padv-P3 finding D010 §1).
ChainTamperError
¶
Bases: ReputationError
A reviewer's logbook failed verify_chain during record_review
audit (D015 §D). Posterior update is rolled back and the review is
rejected — the auditable history of reputation events is the load-
bearing claim of the thesis (CLAUDE.md §load-bearing decisions).
MissingAuditKeyError
¶
Bases: ReputationError
The aggregator was constructed in audit mode but the reviewer's signing key / logbook is not registered (D015 §D).