mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): add relation resolver + inbound-30177 canonical-linkage rule
Phase 2 (revised) of the cross-workspace agent library: the read side of the definition-instance relation resolver and the inbound kind:30177 canonical-linkage rule, with the interim fail-closed new-link posture. No instance-removal/insertion coordinator (that is Phase 4b). Relation resolver: MutationRoute::for_linked_definition resolves an instance's persona_id against the raw keyless definition store to classify its linkage (persona_id IS the linked definition's slug). This is the one canonical join every library mechanism uses; nothing re-derives the persona_id join ad hoc. Inbound 30177 canonical linkage: apply_inbound_managed_agent now consults the resolver and freezes two linkage-authorship cases, applying only safe per-instance fields (name, parallelism, respond_to, respond_to_allowlist): - OwnedByLibrary: the matched instance is linked to a projected definition and the event would clear or re-point persona_id. - InadmissibleNewLink: the event would newly link a plain instance to a projected definition (only the Phase-4b coordinator may admit). A frozen linkage re-retains the local record at a monotonically newer created_at (via retain_agent_record) so the relay head converges back to the library-authoritative linkage, mirroring the 30175 rule. Plain-to-plain relinks and no-op events apply exactly as at head. The round-2 projected persona preflights stay intact in front of this. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
@@ -187,12 +187,40 @@ fn reconcile_inbound_persona_event_blocking(
|
||||
}
|
||||
KIND_MANAGED_AGENT => {
|
||||
let mut agents = load_managed_agents(&app)?;
|
||||
apply_inbound_managed_agent(
|
||||
// §2.8 canonical linkage is resolved against the raw keyless
|
||||
// definition store (`library_ref` is not wire-carried).
|
||||
let definitions = load_agent_definitions(&app)?;
|
||||
let linkage = apply_inbound_managed_agent(
|
||||
&mut agents,
|
||||
&definitions,
|
||||
&d_tag,
|
||||
managed_agent_content_from_event(&event)?,
|
||||
);
|
||||
save_managed_agents(&app, &agents)?;
|
||||
|
||||
// §2.8 convergence: a frozen linkage means the retained head (the
|
||||
// inbound event, retained above) authored a library-owned or
|
||||
// inadmissible linkage change. Re-retain the LOCAL record's
|
||||
// projection at a monotonically newer `created_at` so the relay head
|
||||
// converges back to the library-authoritative linkage — the same
|
||||
// mechanism §2.7's 30175 rule uses. The frozen record still exists
|
||||
// (only its linkage was left intact), so it always has a projection
|
||||
// to reassert.
|
||||
if let InboundAgentLinkage::Frozen(reason) = linkage {
|
||||
if let Some(local) = agents.iter().find(|record| record.pubkey == d_tag) {
|
||||
if let Err(e) = crate::managed_agents::reconcile::retain_agent_record(
|
||||
&conn,
|
||||
&scope.owner_keys,
|
||||
local,
|
||||
) {
|
||||
eprintln!(
|
||||
"buzz-desktop: inbound 30177 convergence re-retain failed for \
|
||||
{d_tag}: {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
eprintln!("buzz-desktop: {}", reason.note(&d_tag));
|
||||
}
|
||||
}
|
||||
_ => unreachable!("kind gated above"),
|
||||
}
|
||||
@@ -410,7 +438,60 @@ fn apply_inbound_persona(personas: &mut Vec<AgentDefinition>, inbound: AgentDefi
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge an inbound kind:30177 managed-agent projection into the local set.
|
||||
/// The outcome of applying an inbound kind:30177 event under the §2.8
|
||||
/// canonical-linkage rule.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum InboundAgentLinkage {
|
||||
/// No local match, or any linkage change the event carried was admissible:
|
||||
/// the event applied exactly as at head.
|
||||
Applied,
|
||||
/// The event tried to author or clear a library-owned linkage, or to admit
|
||||
/// a projected link that only the Phase-4b coordinator may admit. The
|
||||
/// linkage change (and the definition quad it derives) was IGNORED and only
|
||||
/// safe per-instance fields were applied. The caller must re-retain the
|
||||
/// local record at a newer `created_at` so the relay head converges back
|
||||
/// (§2.8, mirroring §2.7's 30175 rule) and surface the typed reason.
|
||||
Frozen(LinkageFreezeReason),
|
||||
}
|
||||
|
||||
/// Why an inbound kind:30177 event's linkage authorship was rejected (§2.8).
|
||||
/// Typed so the convergence + degradation surfacing at the call site is not a
|
||||
/// bare string.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum LinkageFreezeReason {
|
||||
/// The matched instance is currently linked to a library-projected
|
||||
/// definition and the inbound event would clear or re-point its
|
||||
/// `persona_id`. Linkage is owned by the library machinery — the same
|
||||
/// OUTPUT-ONLY discipline §2.7 applies to projected definition metadata.
|
||||
OwnedByLibrary,
|
||||
/// The inbound event would newly link a plain instance to a projected
|
||||
/// definition. Admitting a projected link requires the coordinator's
|
||||
/// `admit_instance_link()` step (Phase 4b); until it lands the interim
|
||||
/// posture fails the new link closed.
|
||||
InadmissibleNewLink,
|
||||
}
|
||||
|
||||
impl LinkageFreezeReason {
|
||||
/// A human-readable degradation note for logging (§2.8 surfacing). The
|
||||
/// coordinator (Phase 4b) will replace this interim posture with an
|
||||
/// admission decision.
|
||||
fn note(self, agent_pubkey: &str) -> String {
|
||||
match self {
|
||||
Self::OwnedByLibrary => format!(
|
||||
"inbound kind:30177 for {agent_pubkey}: refused to re-point/clear a \
|
||||
library-owned linkage; safe fields applied, re-retaining local head (§2.8)"
|
||||
),
|
||||
Self::InadmissibleNewLink => format!(
|
||||
"inbound kind:30177 for {agent_pubkey}: refused to newly link a plain \
|
||||
instance to a library-projected definition (needs the Phase-4b \
|
||||
coordinator); safe fields applied, re-retaining local head (§2.8)"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge an inbound kind:30177 managed-agent projection into the local set
|
||||
/// under the §2.8 canonical-linkage rule.
|
||||
///
|
||||
/// Matches the local record whose `pubkey` equals the event's d-tag (the d-tag
|
||||
/// IS the agent pubkey — see `build_agent_event`). On match, overwrite ONLY the
|
||||
@@ -420,35 +501,86 @@ fn apply_inbound_persona(personas: &mut Vec<AgentDefinition>, inbound: AgentDefi
|
||||
/// untouched. The projection type carries none of them, so they cannot be
|
||||
/// reached here even if a foreign event tried to inject them.
|
||||
///
|
||||
/// # §2.8 canonical linkage
|
||||
///
|
||||
/// A library-linked instance's `persona_id` is owned by the library machinery,
|
||||
/// not the relay. `definitions` is the raw keyless definition store, resolved
|
||||
/// through the [`MutationRoute::for_linked_definition`] read-side resolver so
|
||||
/// this arm and every other library mechanism discover the instance↔definition
|
||||
/// relationship one way. Two linkage changes fail closed (returning
|
||||
/// [`InboundAgentLinkage::Frozen`], applying only safe per-instance fields):
|
||||
/// - the matched instance is currently linked to a projected definition and the
|
||||
/// event would clear or re-point `persona_id` ([`OwnedByLibrary`]);
|
||||
/// - the event would newly link a currently-plain instance to a projected
|
||||
/// definition ([`InadmissibleNewLink`]) — a Phase-4b coordinator admission.
|
||||
///
|
||||
/// Every other case (no local match, no linkage change, or a plain↔plain
|
||||
/// relink) applies exactly as at head.
|
||||
///
|
||||
/// No match is a no-op: managed agents carry device-local secrets and are never
|
||||
/// minted from a relay event — an agent that does not already exist locally has
|
||||
/// no secret key to run with, so inserting a secretless shell would be useless
|
||||
/// and misleading. This diverges from the persona path, which DOES insert on no
|
||||
/// match (personas are secretless definitions). Flagged in the reconcile docs.
|
||||
///
|
||||
/// [`OwnedByLibrary`]: LinkageFreezeReason::OwnedByLibrary
|
||||
/// [`InadmissibleNewLink`]: LinkageFreezeReason::InadmissibleNewLink
|
||||
fn apply_inbound_managed_agent(
|
||||
agents: &mut [ManagedAgentRecord],
|
||||
definitions: &[ManagedAgentRecord],
|
||||
d_tag: &str,
|
||||
inbound: ManagedAgentEventContent,
|
||||
) {
|
||||
if let Some(local) = agents.iter_mut().find(|record| record.pubkey == d_tag) {
|
||||
local.name = inbound.name;
|
||||
// Mirror of the slimmed writer (agent_event_content): a
|
||||
// definition-linked event omits the definition quad because those
|
||||
// fields resolve through the kind:30175 definition — absent means
|
||||
// "not carried", never "clear". Definition-less events still carry
|
||||
// the quad and apply it unconditionally (including clears).
|
||||
let definition_linked = inbound.persona_id.is_some();
|
||||
local.persona_id = inbound.persona_id;
|
||||
if !definition_linked {
|
||||
local.system_prompt = inbound.system_prompt;
|
||||
local.model = inbound.model;
|
||||
local.provider = inbound.provider;
|
||||
local.persona_source_version = inbound.persona_source_version;
|
||||
}
|
||||
local.parallelism = inbound.parallelism;
|
||||
local.respond_to = inbound.respond_to;
|
||||
local.respond_to_allowlist = inbound.respond_to_allowlist;
|
||||
) -> InboundAgentLinkage {
|
||||
let Some(local) = agents.iter_mut().find(|record| record.pubkey == d_tag) else {
|
||||
return InboundAgentLinkage::Applied;
|
||||
};
|
||||
|
||||
// Safe per-instance fields apply as at head regardless of the linkage
|
||||
// decision — they are genuinely instance-local and never library-authored.
|
||||
local.name = inbound.name;
|
||||
local.parallelism = inbound.parallelism;
|
||||
local.respond_to = inbound.respond_to;
|
||||
local.respond_to_allowlist = inbound.respond_to_allowlist;
|
||||
|
||||
// §2.8 canonical-linkage classification, resolved through the read-side
|
||||
// resolver against the raw keyless definition store. `library_ref` is not
|
||||
// wire-carried; the linkage's library status can only be read from the
|
||||
// definition the instance's `persona_id` resolves to.
|
||||
let linkage_changes = local.persona_id.as_deref() != inbound.persona_id.as_deref();
|
||||
let freeze = if !linkage_changes {
|
||||
None
|
||||
} else if MutationRoute::for_linked_definition(definitions, local.persona_id.as_deref())
|
||||
== MutationRoute::LibraryProjected
|
||||
{
|
||||
Some(LinkageFreezeReason::OwnedByLibrary)
|
||||
} else if MutationRoute::for_linked_definition(definitions, inbound.persona_id.as_deref())
|
||||
== MutationRoute::LibraryProjected
|
||||
{
|
||||
Some(LinkageFreezeReason::InadmissibleNewLink)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(reason) = freeze {
|
||||
// Linkage authorship rejected: leave `persona_id` and the
|
||||
// definition-resolved quad exactly as the local record holds them. The
|
||||
// caller re-retains this record so the relay head converges back.
|
||||
return InboundAgentLinkage::Frozen(reason);
|
||||
}
|
||||
|
||||
// Admissible: head behavior. A definition-linked event omits the definition
|
||||
// quad because those fields resolve through the kind:30175 definition —
|
||||
// absent means "not carried", never "clear". Definition-less events still
|
||||
// carry the quad and apply it unconditionally (including clears).
|
||||
let definition_linked = inbound.persona_id.is_some();
|
||||
local.persona_id = inbound.persona_id;
|
||||
if !definition_linked {
|
||||
local.system_prompt = inbound.system_prompt;
|
||||
local.model = inbound.model;
|
||||
local.provider = inbound.provider;
|
||||
local.persona_source_version = inbound.persona_source_version;
|
||||
}
|
||||
InboundAgentLinkage::Applied
|
||||
}
|
||||
|
||||
/// Merge an inbound kind:30176 team projection into the local set.
|
||||
|
||||
@@ -265,7 +265,7 @@ fn inbound_managed_agent_drops_injected_secrets_and_harness() {
|
||||
let content =
|
||||
crate::managed_agents::agent_events::managed_agent_content_from_event(&event).unwrap();
|
||||
let mut agents = vec![local_agent()];
|
||||
apply_inbound_managed_agent(&mut agents, AGENT_PUBKEY, content);
|
||||
apply_inbound_managed_agent(&mut agents, &[], AGENT_PUBKEY, content);
|
||||
|
||||
let a = &agents[0];
|
||||
// Secrets / harness / runtime — every one preserved from the local record.
|
||||
@@ -356,7 +356,7 @@ fn inbound_definition_less_agent_applies_quad() {
|
||||
let content =
|
||||
crate::managed_agents::agent_events::managed_agent_content_from_event(&event).unwrap();
|
||||
let mut agents = vec![local_agent()];
|
||||
apply_inbound_managed_agent(&mut agents, AGENT_PUBKEY, content);
|
||||
apply_inbound_managed_agent(&mut agents, &[], AGENT_PUBKEY, content);
|
||||
|
||||
let a = &agents[0];
|
||||
assert_eq!(a.persona_id, None);
|
||||
@@ -376,7 +376,7 @@ fn inbound_managed_agent_no_match_is_noop() {
|
||||
let content =
|
||||
crate::managed_agents::agent_events::managed_agent_content_from_event(&event).unwrap();
|
||||
let mut agents = vec![local_agent()];
|
||||
apply_inbound_managed_agent(&mut agents, "someotheragentpubkey", content);
|
||||
apply_inbound_managed_agent(&mut agents, &[], "someotheragentpubkey", content);
|
||||
|
||||
// No agent minted from a relay event — it would have no secret key.
|
||||
assert_eq!(agents.len(), 1);
|
||||
@@ -386,6 +386,177 @@ fn inbound_managed_agent_no_match_is_noop() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── §2.8 canonical-linkage rule (kind:30177) ─────────────────────────────
|
||||
|
||||
/// A keyless definition (former persona) for the linkage resolver: `into_
|
||||
/// agent_record` sets `slug = id`, and a projected one carries `library_ref`.
|
||||
fn definition(slug: &str, projected: bool) -> ManagedAgentRecord {
|
||||
let mut record = inbound_for(slug, "Definition").into_agent_record();
|
||||
if projected {
|
||||
record.library_ref = Some(format!("lib-{slug}"));
|
||||
record.library_applied_revision = Some(1);
|
||||
}
|
||||
record
|
||||
}
|
||||
|
||||
/// Inbound kind:30177 content carrying an explicit `persona_id` (or `None`).
|
||||
/// Mirrors the wire shape `managed_agent_content_from_event` produces.
|
||||
fn agent_content(name: &str, persona_id: Option<&str>) -> ManagedAgentEventContent {
|
||||
ManagedAgentEventContent {
|
||||
name: name.to_string(),
|
||||
persona_id: persona_id.map(str::to_string),
|
||||
system_prompt: None,
|
||||
model: None,
|
||||
provider: None,
|
||||
persona_source_version: None,
|
||||
parallelism: 4,
|
||||
respond_to: crate::managed_agents::RespondTo::OwnerOnly,
|
||||
respond_to_allowlist: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// A local instance linked to `persona_id`, keyed by `AGENT_PUBKEY`.
|
||||
fn linked_agent(persona_id: &str) -> ManagedAgentRecord {
|
||||
let mut agent = local_agent();
|
||||
agent.persona_id = Some(persona_id.to_string());
|
||||
agent
|
||||
}
|
||||
|
||||
/// An inbound event that would re-point a library-owned linkage is frozen:
|
||||
/// `persona_id` stays on the local library-projected definition, safe fields
|
||||
/// still apply, and the caller is told to converge the relay head back (§2.8).
|
||||
#[test]
|
||||
fn inbound_30177_freezes_repoint_of_library_owned_linkage() {
|
||||
let definitions = vec![definition("shared-def", true)];
|
||||
let mut agents = vec![linked_agent("shared-def")];
|
||||
|
||||
// Inbound tries to re-point the linkage to a different definition.
|
||||
let outcome = apply_inbound_managed_agent(
|
||||
&mut agents,
|
||||
&definitions,
|
||||
AGENT_PUBKEY,
|
||||
agent_content("Renamed", Some("other-def")),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
outcome,
|
||||
InboundAgentLinkage::Frozen(LinkageFreezeReason::OwnedByLibrary),
|
||||
"re-pointing a library-owned linkage must freeze",
|
||||
);
|
||||
let a = &agents[0];
|
||||
assert_eq!(
|
||||
a.persona_id,
|
||||
Some("shared-def".to_string()),
|
||||
"linkage must stay on the local library-owned definition",
|
||||
);
|
||||
assert_eq!(a.name, "Renamed", "safe per-instance fields still apply");
|
||||
assert_eq!(a.parallelism, 4, "safe per-instance fields still apply");
|
||||
}
|
||||
|
||||
/// An inbound event that would CLEAR a library-owned linkage
|
||||
/// (`persona_id: None`) is frozen the same way — clearing is authorship too.
|
||||
#[test]
|
||||
fn inbound_30177_freezes_clear_of_library_owned_linkage() {
|
||||
let definitions = vec![definition("shared-def", true)];
|
||||
let mut agents = vec![linked_agent("shared-def")];
|
||||
|
||||
let outcome = apply_inbound_managed_agent(
|
||||
&mut agents,
|
||||
&definitions,
|
||||
AGENT_PUBKEY,
|
||||
agent_content("Renamed", None),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
outcome,
|
||||
InboundAgentLinkage::Frozen(LinkageFreezeReason::OwnedByLibrary),
|
||||
);
|
||||
assert_eq!(
|
||||
agents[0].persona_id,
|
||||
Some("shared-def".to_string()),
|
||||
"a definition-less inbound must not clear a library-owned linkage",
|
||||
);
|
||||
}
|
||||
|
||||
/// An inbound event that would newly link a currently-plain instance to a
|
||||
/// library-projected definition is frozen as an inadmissible new link — only
|
||||
/// the Phase-4b coordinator may admit a projected link (§2.8, P6-C1 interim).
|
||||
#[test]
|
||||
fn inbound_30177_freezes_inadmissible_new_link_to_projected_definition() {
|
||||
let definitions = vec![definition("shared-def", true)];
|
||||
// Local instance is definition-less (plain).
|
||||
let mut agents = vec![linked_agent("")];
|
||||
agents[0].persona_id = None;
|
||||
|
||||
let outcome = apply_inbound_managed_agent(
|
||||
&mut agents,
|
||||
&definitions,
|
||||
AGENT_PUBKEY,
|
||||
agent_content("Renamed", Some("shared-def")),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
outcome,
|
||||
InboundAgentLinkage::Frozen(LinkageFreezeReason::InadmissibleNewLink),
|
||||
"a new link to a projected definition must fail closed",
|
||||
);
|
||||
assert_eq!(
|
||||
agents[0].persona_id, None,
|
||||
"the inadmissible new link must not be authored",
|
||||
);
|
||||
}
|
||||
|
||||
/// A linkage change that touches only plain definitions applies as at head —
|
||||
/// the §2.8 rule freezes ONLY library-owned or projected-target changes, never
|
||||
/// an ordinary plain relink.
|
||||
#[test]
|
||||
fn inbound_30177_applies_plain_relink_unchanged() {
|
||||
let definitions = vec![definition("plain-a", false), definition("plain-b", false)];
|
||||
let mut agents = vec![linked_agent("plain-a")];
|
||||
|
||||
let outcome = apply_inbound_managed_agent(
|
||||
&mut agents,
|
||||
&definitions,
|
||||
AGENT_PUBKEY,
|
||||
agent_content("Renamed", Some("plain-b")),
|
||||
);
|
||||
|
||||
assert_eq!(outcome, InboundAgentLinkage::Applied);
|
||||
assert_eq!(
|
||||
agents[0].persona_id,
|
||||
Some("plain-b".to_string()),
|
||||
"a plain→plain relink applies exactly as at head",
|
||||
);
|
||||
}
|
||||
|
||||
/// An inbound event that leaves the linkage unchanged is never frozen even when
|
||||
/// the linked definition is library-projected — the definition quad is still
|
||||
/// correctly omitted (linked), and safe fields apply. Freezing keys on a
|
||||
/// linkage CHANGE, not on the linked definition's library status alone.
|
||||
#[test]
|
||||
fn inbound_30177_no_linkage_change_applies_even_when_projected() {
|
||||
let definitions = vec![definition("shared-def", true)];
|
||||
let mut agents = vec![linked_agent("shared-def")];
|
||||
agents[0].system_prompt = Some("local prompt".to_string());
|
||||
|
||||
let outcome = apply_inbound_managed_agent(
|
||||
&mut agents,
|
||||
&definitions,
|
||||
AGENT_PUBKEY,
|
||||
agent_content("Renamed", Some("shared-def")),
|
||||
);
|
||||
|
||||
assert_eq!(outcome, InboundAgentLinkage::Applied);
|
||||
let a = &agents[0];
|
||||
assert_eq!(a.persona_id, Some("shared-def".to_string()));
|
||||
assert_eq!(a.name, "Renamed");
|
||||
assert_eq!(
|
||||
a.system_prompt,
|
||||
Some("local prompt".to_string()),
|
||||
"a linked inbound omits the definition quad — the local snapshot survives",
|
||||
);
|
||||
}
|
||||
|
||||
// ── Team (30176) inbound ─────────────────────────────────────────────────
|
||||
|
||||
const TEAM_ID: &str = "team-local-id";
|
||||
|
||||
@@ -561,6 +561,31 @@ impl MutationRoute {
|
||||
}))
|
||||
}
|
||||
|
||||
/// The route for the DEFINITION a keyed instance links to — the read side
|
||||
/// of the §2.8 definition–instance relation resolver. An instance's
|
||||
/// `persona_id` IS the linked definition's slug (they are assigned in
|
||||
/// lockstep: `into_agent_record` sets `slug = id`, and every linked
|
||||
/// instance carries `persona_id == definition.slug`). This is the ONE
|
||||
/// canonical join every library mechanism uses to discover an
|
||||
/// instance↔definition relationship — the inbound kind:30177 canonical-
|
||||
/// linkage rule consults it to decide whether an instance's linkage is
|
||||
/// library-owned; nothing re-derives the `persona_id` join ad hoc.
|
||||
///
|
||||
/// A definition-less instance (`persona_id == None`) links to no definition
|
||||
/// and is always [`Plain`](Self::Plain), as is a `persona_id` that resolves
|
||||
/// to a plain definition or to no definition at all (a create, or a stale
|
||||
/// link). Only a `persona_id` resolving to a `library_ref`-carrying
|
||||
/// definition is [`LibraryProjected`](Self::LibraryProjected).
|
||||
pub(crate) fn for_linked_definition(
|
||||
definitions: &[ManagedAgentRecord],
|
||||
persona_id: Option<&str>,
|
||||
) -> Self {
|
||||
match persona_id {
|
||||
Some(slug) => Self::for_slug(definitions, slug),
|
||||
None => Self::Plain,
|
||||
}
|
||||
}
|
||||
|
||||
/// Refuse a plain-path mutation whose target `slug` resolves to a
|
||||
/// library-projected record (§2.7). The command boundaries that key by slug
|
||||
/// — `delete_persona` and snapshot/team import — call this on the RAW
|
||||
|
||||
@@ -649,6 +649,67 @@ fn mutation_route_for_persona_d_tag_catches_projected_team_slug_that_for_slug_mi
|
||||
);
|
||||
}
|
||||
|
||||
/// §2.8 relation-resolver read side: `for_linked_definition` classifies an
|
||||
/// instance's linkage by resolving its `persona_id` against the raw keyless
|
||||
/// definition store. A `persona_id` pointing at a projected definition is
|
||||
/// `LibraryProjected`; one pointing at a plain definition, at no definition (a
|
||||
/// stale link), or `None` (a definition-less instance) is `Plain`. This is the
|
||||
/// canonical join the inbound kind:30177 rule consults — the same `persona_id →
|
||||
/// slug` derivation every library mechanism uses, resolved one way.
|
||||
#[test]
|
||||
fn for_linked_definition_classifies_projected_plain_absent_and_none() {
|
||||
let definitions = vec![
|
||||
projected_record("shared", 3),
|
||||
custom_persona("custom:plain", "Plain").into_agent_record(),
|
||||
];
|
||||
|
||||
// Instance linked to a projected definition → LibraryProjected.
|
||||
assert_eq!(
|
||||
super::MutationRoute::for_linked_definition(&definitions, Some("shared")),
|
||||
super::MutationRoute::LibraryProjected,
|
||||
);
|
||||
// Instance linked to a plain definition → Plain.
|
||||
assert_eq!(
|
||||
super::MutationRoute::for_linked_definition(&definitions, Some("custom:plain")),
|
||||
super::MutationRoute::Plain,
|
||||
);
|
||||
// Instance whose persona_id resolves to no definition (a stale link) → Plain.
|
||||
assert_eq!(
|
||||
super::MutationRoute::for_linked_definition(&definitions, Some("does-not-exist")),
|
||||
super::MutationRoute::Plain,
|
||||
);
|
||||
// Definition-less instance (`persona_id == None`) → Plain.
|
||||
assert_eq!(
|
||||
super::MutationRoute::for_linked_definition(&definitions, None),
|
||||
super::MutationRoute::Plain,
|
||||
);
|
||||
}
|
||||
|
||||
/// §2.8 resolver vs. `persona_d_tag`: the instance→definition join keys on the
|
||||
/// definition's UUID `slug` (== the instance `persona_id`), NOT the team-
|
||||
/// derived d-tag. A team-sourced projected definition whose `persona_id`/`slug`
|
||||
/// is a UUID but whose d-tag is the pack slug must still resolve as projected
|
||||
/// through `for_linked_definition` — the resolver and the inbound d-tag
|
||||
/// preflight key on different fields for different callers, and this pins that
|
||||
/// the linkage join uses the slug.
|
||||
#[test]
|
||||
fn for_linked_definition_keys_on_slug_not_persona_d_tag() {
|
||||
let mut definition = projected_record("uuid-shared", 5);
|
||||
definition.source_team_persona_slug = Some("codereviewer".to_string());
|
||||
let definitions = vec![definition];
|
||||
|
||||
// The instance's `persona_id` is the definition's UUID slug — resolves.
|
||||
assert_eq!(
|
||||
super::MutationRoute::for_linked_definition(&definitions, Some("uuid-shared")),
|
||||
super::MutationRoute::LibraryProjected,
|
||||
);
|
||||
// The team d-tag is NOT the slug — it does not resolve the linkage join.
|
||||
assert_eq!(
|
||||
super::MutationRoute::for_linked_definition(&definitions, Some("codereviewer")),
|
||||
super::MutationRoute::Plain,
|
||||
);
|
||||
}
|
||||
|
||||
/// The fingerprint narrowing (F3): a plain persona save may freely change a
|
||||
/// projected record's SCOPE-LOCAL fields (`is_active`, `env_vars`) — these are
|
||||
/// not library-authoritative, so the shared fingerprint is unchanged and the
|
||||
|
||||
Reference in New Issue
Block a user