From e7ceb2c2807fabb2b0e68aeeeb0aab5948c582a5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 31 Aug 2026 21:28:00 +0200 Subject: [PATCH] Require exact fission custody assignments --- crates/ghostlight-dungeon/src/elaboration.rs | 50 +++++++++++++++++-- .../ghostlight-multiresolution-agency.md | 4 ++ .../Agents and model stages.md | 3 ++ notes/ghostlight-current-system-map.md | 6 ++- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/crates/ghostlight-dungeon/src/elaboration.rs b/crates/ghostlight-dungeon/src/elaboration.rs index 7fdb959..94dd2a7 100644 --- a/crates/ghostlight-dungeon/src/elaboration.rs +++ b/crates/ghostlight-dungeon/src/elaboration.rs @@ -1634,6 +1634,16 @@ impl crate::agent::ModelAgentTool for WorldComplexityTool<'_> { .ok_or_else(|| "complexity fission schema has no children".to_owned())?; children.insert("minItems".into(), serde_json::json!(2)); children.insert("maxItems".into(), serde_json::json!(8)); + let parent = &self.campaign.gestalts[self.parent_gestalt_id]; + selected["properties"]["resource_child_assignments"] = + exact_keyed_string_map_schema(parent.resources.iter().cloned()); + selected["properties"]["member_child_assignments"] = exact_keyed_string_map_schema( + self.campaign + .gestalt_members + .values() + .filter(|member| member.gestalt_id == self.parent_gestalt_id) + .map(|member| member.id.clone()), + ); } *variants = vec![selected]; if self.operation == WorldComplexityOperation::Individuate { @@ -1776,6 +1786,10 @@ impl ElaborationSubAgentPort for ModelWorldComplexityWo }, "assignment":assignment, "session_jurisdiction_id":jurisdiction_id, + "required_resource_assignment_ids":parent.resources, + "required_member_assignment_ids":self.campaign.gestalt_members.values() + .filter(|member|member.gestalt_id == parent_id) + .map(|member|member.id.clone()).collect::>(), "parent":parent, "parent_profile":profile, "parent_members":self.campaign.gestalt_members.values() @@ -2235,6 +2249,21 @@ fn exact_schema(value: impl Serialize) -> serde_json::Value { }) } +fn exact_keyed_string_map_schema(keys: impl IntoIterator) -> serde_json::Value { + let keys = keys.into_iter().collect::>(); + let properties = keys + .iter() + .cloned() + .map(|key| (key, serde_json::json!({"type":"string","minLength":1}))) + .collect::>(); + serde_json::json!({ + "type":"object", + "properties":properties, + "required":keys, + "additionalProperties":false, + }) +} + fn constrain_exact_object(schema: &mut serde_json::Value, exact: &serde_json::Value) -> Result<()> { let exact = exact .as_object() @@ -3454,7 +3483,7 @@ mod tests { home_location_id: "room".into(), shared_capabilities: BTreeSet::from(["river tending".into()]), shared_knowledge: BTreeSet::from(["seasonal ford marks".into()]), - resources: BTreeSet::new(), + resources: BTreeSet::from(["ferry charter".into()]), goals: vec!["keep the river habitable".into()], pressures: vec!["the banks are narrowing".into()], }, @@ -3484,6 +3513,8 @@ mod tests { goals: parent.goals.clone(), pressures: parent.pressures.clone(), }; + let mut licensed = child("licensed-river-households", "Licensed River Households"); + licensed.resources.insert("ferry charter".into()); crate::domain::GestaltFissionPreview { schema: "ghostlight.gestalt_fission_preview.v1".into(), campaign_id: campaign.id, @@ -3491,7 +3522,7 @@ mod tests { parent_gestalt_id: parent.id.clone(), partition_axis: crate::domain::AgencyAxis::Authority, children: vec![ - child("licensed-river-households", "Licensed River Households"), + licensed, child("unrecorded-river-households", "Unrecorded River Households"), ], child_partition_values: BTreeMap::from([ @@ -3503,7 +3534,10 @@ mod tests { ]), residual_child_id: "unrecorded-river-households".into(), member_child_assignments: BTreeMap::new(), - resource_child_assignments: BTreeMap::new(), + resource_child_assignments: BTreeMap::from([( + "ferry charter".into(), + "licensed-river-households".into(), + )]), evidence_receipt_ids: Vec::new(), gaps: Vec::new(), canon_candidates: Vec::new(), @@ -3540,6 +3574,16 @@ mod tests { 1 ); assert!(schema["$defs"].get("GestaltFissionPreview").is_none()); + assert_eq!( + schema["$defs"]["WorldComplexityMutationDraft"]["oneOf"][0]["properties"]["resource_child_assignments"] + ["required"], + serde_json::json!(["ferry charter"]) + ); + assert_eq!( + schema["$defs"]["WorldComplexityMutationDraft"]["oneOf"][0]["properties"]["member_child_assignments"] + ["required"], + serde_json::json!([]) + ); let draft = WorldComplexityMutationDraft::Fission { children: preview .children diff --git a/docs/architecture/ghostlight-multiresolution-agency.md b/docs/architecture/ghostlight-multiresolution-agency.md index ef1a7c3..57cd39d 100644 --- a/docs/architecture/ghostlight-multiresolution-agency.md +++ b/docs/architecture/ghostlight-multiresolution-agency.md @@ -102,6 +102,10 @@ therefore never paid for in model output and cannot drift while being copied. The shared fission validator reports the complete deterministic mismatch set in one observation, so an agent can repair the same compact draft without replaying inference or guessing which invariant failed first. +The lane-specific action schema requires one entry for every exact frozen +resource and dormant-member ID and rejects extra keys. The elaborator still +chooses child custody; it cannot silently omit a scarce thing the kernel must +conserve. Round effort is proportional to unresolved pressure rather than presumed model yield: `ceil(active_cell_budget * deficit / target)`, zero when the target is diff --git a/docs/public-architecture/Agents and model stages.md b/docs/public-architecture/Agents and model stages.md index 6aa82f0..23d0a46 100644 --- a/docs/public-architecture/Agents and model stages.md +++ b/docs/public-architecture/Agents and model stages.md @@ -85,6 +85,9 @@ while the tool owns parent version and location. Deterministic validation and the existing kernel commands remain the only admission owners. A rejected fission returns the complete deterministic mismatch set to the same agentic turn, rather than hiding several repairable defects behind one generic error. +Its action schema exposes the exact frozen resource and dormant-member IDs as +required assignment keys, while leaving their destination children to the +writer. After every productive round, `world-elaborator--session-compaction` distills that title's frontier in the assigned realm jurisdiction and its unresolved leads while the harness diff --git a/notes/ghostlight-current-system-map.md b/notes/ghostlight-current-system-map.md index a89cd98..b5f2bc4 100644 --- a/notes/ghostlight-current-system-map.md +++ b/notes/ghostlight-current-system-map.md @@ -1396,8 +1396,10 @@ outside this authority. validation proves active parent and location, exact version, unique bounded identity, valid relationships, and non-materialized member state. WorldKernel revalidates either lane before atomic mutation. The model schema exposes only - the assigned operation, bounds fission children to two through eight, and - fixes invariant member fields for individuation; tool-level construction + the assigned operation, bounds fission children to two through eight, + requires exactly the frozen parent resource and dormant-member IDs as + assignment-map keys while leaving child custody model-authored, and fixes + invariant member fields for individuation; tool-level construction additionally rejects duplicate child IDs/partitions or incomplete exact member assignment before shared semantic validation. The shared fission validator owns one complete deterministic mismatch set, including frozen