Begin is the one entry: world.create is gone, the route owns the defaults and how far Begin goes
world.create leaves the dispatch table and operation_schema. world.begin takes ghostlight.world_begin.v1 (title, brief, subject_label; unknown fields refused); begin_intent decides targets, roots, labels and lens weights, begin_plan decides the steps, and where a seed vault is configured Begin stops at the approved Draft so the seed lane stays reachable. The surface authors no constants. Tests that needed a Draft start from the kernel's own create; the bridge fixture is edited to match and still needs its Starfire drift run. May not build: first commit of the batch, unverified. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8912dde49f
commit
9ccc9fd4f2
5 changed files with 452 additions and 694 deletions
|
|
@ -52,8 +52,8 @@ const PLAY_FAULT_LINE: &str =
|
|||
"The storyteller lost the thread and your turn ended. Write again to start a new turn.";
|
||||
|
||||
/// What the Begin button captures from the owner: the three fields the form
|
||||
/// asks for. The rest of `world_create.v4` rides the button's own action as
|
||||
/// authored constants (`begin_action`).
|
||||
/// asks for, and all `world_begin.v1` carries. Every other choice a world is
|
||||
/// born with is the route's (`runtime::begin_intent`), not the surface's.
|
||||
const BEGIN_BINDINGS: [&str; 3] = ["title", "brief", "subject_label"];
|
||||
|
||||
const SEED_BINDINGS: [&str; 2] = ["vault_scope", "brief"];
|
||||
|
|
@ -201,20 +201,6 @@ fn jurisdiction_label(world: &WorldSnapshot, jurisdiction: JurisdictionKey) -> S
|
|||
}
|
||||
}
|
||||
|
||||
/// The `world_create.v4` fields the Begin form does not ask for, stated
|
||||
/// explicitly: no scale targets, no jurisdiction roots, no persona or agent
|
||||
/// label, and Dungeon's uniform lens policy (the library holds no default
|
||||
/// weights). They travel as the button's own action, which the lowering
|
||||
/// spreads into the payload beside the captured `bindings`.
|
||||
fn begin_action() -> anyhow::Result<Value> {
|
||||
Ok(json!({
|
||||
"targets":"{}",
|
||||
"jurisdictions":"[]",
|
||||
"lens_weights":serde_json::to_string(&crate::runtime::uniform_lens_weights())
|
||||
.context("the default lens weights encode")?
|
||||
}))
|
||||
}
|
||||
|
||||
/// `play` arrives beside the snapshot rather than inside it: `WorldSnapshot`
|
||||
/// carries no play-turn state of its own. The play card below reads `play`
|
||||
/// alone, never the operator's own unscoped log of every committed event:
|
||||
|
|
@ -281,13 +267,13 @@ pub(crate) fn authenticated_surface(
|
|||
"world.begin",
|
||||
"Begin",
|
||||
"world.begin",
|
||||
begin_action()?,
|
||||
json!({}),
|
||||
&BEGIN_BINDINGS,
|
||||
),
|
||||
]);
|
||||
commands.push(command_descriptor(
|
||||
"world.begin",
|
||||
"ghostlight.world_create.v4",
|
||||
"ghostlight.world_begin.v1",
|
||||
&BEGIN_BINDINGS,
|
||||
"WorldMailbox",
|
||||
));
|
||||
|
|
@ -560,7 +546,7 @@ pub(crate) fn authenticated_surface(
|
|||
json!({
|
||||
"id":"world.play.text",
|
||||
"kind":"control.input.textarea",
|
||||
// PA.f214: an authored `value`, as `world.create.brief` above:
|
||||
// PA.f214: an authored `value`, as `world.begin.brief` above:
|
||||
// the lowering only captures a `value`, so a box left
|
||||
// untouched must still submit `text: ""`, which continues
|
||||
// an existing Running turn (the running hint tells players
|
||||
|
|
@ -660,8 +646,7 @@ pub(crate) fn operation_schema(operation: &str) -> Option<&'static str> {
|
|||
"heimdall.auth.begin" => "heimdall.auth_begin_command.v1",
|
||||
"heimdall.auth.complete" => "heimdall.auth_complete_command.v1",
|
||||
"app.auth.logout" => "ghostlight.app_logout.v2",
|
||||
"world.create" => "ghostlight.world_create.v4",
|
||||
"world.begin" => "ghostlight.world_create.v4",
|
||||
"world.begin" => "ghostlight.world_begin.v1",
|
||||
"world.approve" => "ghostlight.world_approve.v0",
|
||||
"world.activate" => "ghostlight.world_activate.v0",
|
||||
"world.advance_time" => "ghostlight.world_advance_time.v0",
|
||||
|
|
@ -881,9 +866,10 @@ mod tests {
|
|||
}
|
||||
|
||||
/// R2 at the surface: the empty world asks for a title, a name and an
|
||||
/// optional brief, and offers one command, Begin. The fields the form no
|
||||
/// longer asks for are authored constants on the button, and the
|
||||
/// descriptor captures only what the form asks.
|
||||
/// optional brief, and offers one command, Begin. The button carries no
|
||||
/// authored choice of its own: what a world is born with beyond those
|
||||
/// three fields is the route's, and the descriptor captures only what the
|
||||
/// form asks.
|
||||
#[test]
|
||||
fn the_empty_world_surface_asks_for_a_title_a_name_and_offers_one_begin() {
|
||||
let surface = authenticated_surface("sha256:owner", None, None, true).unwrap();
|
||||
|
|
@ -893,6 +879,8 @@ mod tests {
|
|||
"narrative_persona",
|
||||
"operational_agent",
|
||||
"world.create",
|
||||
"lens_weights",
|
||||
"jurisdictions",
|
||||
"session_zero",
|
||||
"campaign",
|
||||
] {
|
||||
|
|
@ -926,21 +914,44 @@ mod tests {
|
|||
.collect();
|
||||
assert_eq!(commands.len(), 1, "{commands:?}");
|
||||
assert_eq!(commands[0]["command"], "world.begin");
|
||||
assert_eq!(commands[0]["payloadSchema"], "ghostlight.world_create.v4");
|
||||
assert_eq!(commands[0]["payloadSchema"], "ghostlight.world_begin.v1");
|
||||
assert_eq!(commands[0]["authority"], "WorldMailbox");
|
||||
assert_eq!(commands[0]["captureBindings"], json!(["title", "brief", "subject_label"]));
|
||||
|
||||
// The constants are explicit, and the lens weights are Dungeon's
|
||||
// uniform policy, every stock lens named.
|
||||
let button = find(root, "world.begin").expect("a Begin button");
|
||||
assert_eq!(button["props"]["label"], "Begin");
|
||||
let action = &button["props"]["action"];
|
||||
assert_eq!(action["targets"], "{}");
|
||||
assert_eq!(action["jurisdictions"], "[]");
|
||||
let decoded: ghostlight::LensWeights =
|
||||
serde_json::from_str(action["lens_weights"].as_str().expect("lens weights text")).unwrap();
|
||||
assert_eq!(decoded, crate::runtime::uniform_lens_weights());
|
||||
assert_eq!(decoded.iter().count(), ghostlight::Lens::ALL.len());
|
||||
assert!(decoded.iter().all(|(_, weight)| weight == 1));
|
||||
assert_eq!(
|
||||
button["props"]["action"],
|
||||
json!({"command":"world.begin"}),
|
||||
"the Begin button authors nothing beyond its command"
|
||||
);
|
||||
}
|
||||
|
||||
/// Each control feeds the field it is named for: typing into a control
|
||||
/// puts the text under the binding that control declares, and the Begin
|
||||
/// button captures exactly the bindings the three controls declare. A
|
||||
/// control bound to another control's field would submit no name (or
|
||||
/// overwrite the title) in a real client.
|
||||
#[test]
|
||||
fn each_begin_control_is_bound_to_its_own_field_and_the_button_captures_them_all() {
|
||||
let surface = authenticated_surface("sha256:owner", None, None, false).unwrap();
|
||||
let root = &surface["surface"]["root"];
|
||||
let bound = |id: &str| -> String {
|
||||
let bindings = find(root, id).unwrap()["stateBindings"].as_array().unwrap().clone();
|
||||
assert_eq!(bindings.len(), 1, "{id}");
|
||||
bindings[0]["bindingName"].as_str().unwrap().to_owned()
|
||||
};
|
||||
assert_eq!(bound("world.begin.title"), "title");
|
||||
assert_eq!(bound("world.begin.subject"), "subject_label");
|
||||
assert_eq!(bound("world.begin.brief"), "brief");
|
||||
let mut captured: Vec<String> = find(root, "world.begin").unwrap()["props"]["captureBindings"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|name| name.as_str().unwrap().to_owned())
|
||||
.collect();
|
||||
captured.sort();
|
||||
assert_eq!(captured, ["brief", "subject_label", "title"]);
|
||||
}
|
||||
|
||||
/// `world.play`'s own operation id resolves to its schema through
|
||||
|
|
|
|||
|
|
@ -13,17 +13,14 @@
|
|||
"sourceVersion": 0,
|
||||
"transport": "https-json"
|
||||
},
|
||||
"schemaId": "ghostlight.world_create.v4"
|
||||
"schemaId": "ghostlight.world_begin.v1"
|
||||
},
|
||||
"payload": {
|
||||
"bindings": {
|
||||
"brief": "A world captured for the committed Eve client bridge fixture.",
|
||||
"subject_label": "The Fixture Operator",
|
||||
"title": "Bridge Fixture World"
|
||||
},
|
||||
"jurisdictions": "[]",
|
||||
"lens_weights": "{\"patina\":1,\"charter\":1,\"ledger\":1,\"hearth\":1,\"tangle\":1,\"veil\":1,\"ember\":1,\"numen\":1}",
|
||||
"targets": "{}"
|
||||
}
|
||||
},
|
||||
"providerId": "gamecult.ghostlight.dungeon",
|
||||
"receiptSchema": "gamecult.eve.command_result.v1",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -624,10 +624,11 @@ lane still uses. The real-model gate that exists today is the Idunn recipe's
|
|||
runs `controllers::tests::real_local_model_cognition_modes_commit_speech`
|
||||
against a real model; exercising a whole play turn against one is not gated
|
||||
by this contract.
|
||||
`world.create`'s current payload is `world_create.v4`: title, brief, targets,
|
||||
jurisdiction roots and lens weights are required. Dungeon's Begin button states
|
||||
them as authored constants (no targets, no roots, uniform weights) beside the
|
||||
three fields the form asks for. `world.seed` runs one
|
||||
`world.begin` is the one route that makes a world. Its payload is
|
||||
`world_begin.v1`: title, brief and subject_label, nothing else; the route
|
||||
decides everything else a world is born with (no targets, no roots, no persona
|
||||
or agent label, uniform lens weights) and `begin_plan` decides how far Begin
|
||||
goes. `world.seed` runs one
|
||||
`SeedRunner` session per invocation against a `VaultEvidenceSource`.
|
||||
|
||||
Focused tests must prove:
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ and world phase (`anonymous_surface`, `authenticated_surface` in
|
|||
|---|---|---|
|
||||
| Anonymous | `heimdall.access_gate` | `heimdall.auth.begin`, `heimdall.auth.complete` |
|
||||
| Authenticated, any world state | a `Sign out` button on every authenticated surface | `app.auth.logout` (`ghostlight.app_logout.v2`) |
|
||||
| Authenticated, no world | title, your name, optional brief, and one Begin button | `world.begin` (`ghostlight.world_create.v4`): create, approve and activate as three kernel commands, stopping at the first refusal |
|
||||
| Authenticated, no world | title, your name, optional brief, and one Begin button | `world.begin` (`ghostlight.world_begin.v1`: title, brief, subject_label): create, then approve and activate as separate kernel commands, stopping at the first refusal; where a seed vault is configured it stops at the approved Draft so the owner can seed before Activate |
|
||||
| Draft world | world summary card; approve button for a required approver who has not approved; for the owner, the seed form and Seed card, and the activate button once every required approver has approved | `world.approve` (`ghostlight.world_approve.v0`), `world.seed` (`ghostlight.world_seed.v1`), `world.activate` (`ghostlight.world_activate.v0`) |
|
||||
| Active world, owner | advance-time control and the Play card: narration, the open question, the refusal of the player's own act, one free-text control | `world.advance_time` (`ghostlight.world_advance_time.v0`), `world.play` (`ghostlight.world_play.v0`) |
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue