host/taint: match peerspeak's second ownership carrier
The consumer half of phase 1 (plan §5.1, impl plan §3). The engine's tag root becomes a union: `peerspeak.owned` truthy OR `node.name` starting with `peerspeak_owned_`. Round 8 added the second carrier because a node property is invisible to the registry `global` event and recoverable only by binding the node — which is exactly how the phase-5 gate failed — while `node.name` is announced directly. The union lives in `local_root_reason`, not in the adapter. Folding both into the one `peerspeak_owned` bool at the observation boundary would make each carrier untestable alone, which is the phase-3r lesson: a gate asserting a value two sources can satisfy gates neither. The existing `peerspeak_tagged_nodes_…` fixture now carries both carriers, so it would keep passing if either were deleted; two new tests pin them individually, and a third pins that the prefix matches only at the start of a name. Both literals are now named constants — they are a cross-repo wire contract with peerspeak, not local naming — and asserted against tests/fixtures/ownership-tag-contract.txt, committed byte-identical in both repos. That test also runs the fixture's own worked example name through the engine, so the shared file cannot document a value this side does not actually exclude. Five mutations verified: drop either carrier, loosen `starts_with` to `contains`, or rename either constant, and exactly the intended test fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+34
-1
@@ -123,6 +123,29 @@ pub const CAPTURE_SINK_PREFIX: &str = "pixelpass_capture_";
|
||||
/// what `pulse.module.id` is for (v3.4 §5.2 correction 4).
|
||||
pub const ECHO_CANCEL_GROUP_PREFIX: &str = "echo-cancel-";
|
||||
|
||||
/// Ownership carrier 1: the node property peerspeak sets on everything it
|
||||
/// plays (v3.5 §5.1). Read at the observer boundary, which is the only place
|
||||
/// that touches raw property names — see [`super::observer`].
|
||||
///
|
||||
/// ⚠️ **Cross-repo wire contract.** peerspeak emits this; it does not depend
|
||||
/// on this crate, nor this crate on it. The values are pinned in
|
||||
/// `tests/fixtures/ownership-tag-contract.txt`, committed byte-identical in
|
||||
/// both repos, and asserted by [`tests::ownership_carriers_match_the_cross_repo_fixture`].
|
||||
/// The producer's matching constants live in peerspeak
|
||||
/// `src/audio/ownership.rs`. Changing either is a both-repos-same-session
|
||||
/// change that invalidates the phase 5 matrix.
|
||||
pub const PEERSPEAK_OWNED_PROP: &str = "peerspeak.owned";
|
||||
|
||||
/// Ownership carrier 2: a `node.name` prefix (v3.5 §5.1, round 8).
|
||||
///
|
||||
/// Matched as a **union** with [`PEERSPEAK_OWNED_PROP`] — either one makes a
|
||||
/// node peerspeak-owned. Two carriers because a property is invisible to the
|
||||
/// registry `global` event and recoverable only by binding the node (v3.5
|
||||
/// §6.7), which is precisely how the phase-5 gate failed; this one is
|
||||
/// announced directly. A union is also the fail-closed direction: a missed
|
||||
/// tag leaks call audio into the share, a spurious one only over-excludes.
|
||||
pub const PEERSPEAK_OWNED_NODE_PREFIX: &str = "peerspeak_owned_";
|
||||
|
||||
/// Why a node is tainted or excluded. Stable machine-readable codes: this
|
||||
/// value is the phase 5 audit output, the phase 6 status event, and the
|
||||
/// eventual answer to "why isn't this app being shared?".
|
||||
@@ -535,7 +558,17 @@ fn ambiguous_id_nodes(snapshot: &GraphSnapshot) -> BTreeSet<Serial> {
|
||||
}
|
||||
|
||||
fn local_root_reason(node: &NodeSnapshot, ctx: &ExclusionCtx) -> Option<Reason> {
|
||||
if node.props.peerspeak_owned {
|
||||
// The two ownership carriers, as a union (v3.5 §5.1). Kept here rather
|
||||
// than folded together at the observer boundary so that the union is a
|
||||
// pure, directly-testable rule: an adapter that collapsed both into the
|
||||
// one `peerspeak_owned` bool would make each carrier untestable alone,
|
||||
// which is exactly how phase 3r's row 1 nearly gated nothing.
|
||||
if node.props.peerspeak_owned
|
||||
|| node
|
||||
.name
|
||||
.as_deref()
|
||||
.is_some_and(|name| name.starts_with(PEERSPEAK_OWNED_NODE_PREFIX))
|
||||
{
|
||||
return Some(Reason::PeerspeakOwned);
|
||||
}
|
||||
if let (Some(module), Some(aec)) = (node.props.pulse_module_id, ctx.aec_module_id)
|
||||
|
||||
Reference in New Issue
Block a user