Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/src/wasm/social.rs

7.3 KiB, 1 run

created by r2519314175:995, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The Social panel's edge — a thin binding to the JS driver `window.DaimondSocial`.
2//!
3//! WHY THIS MODULE EXISTS, WHICH IS NOT WHAT IT DOES. On 2026-08-24 two real daimons, on two
4//! accounts and two different models, were each asked to do one side of the Social panel's work.
5//! Neither could reach it, and neither said so: one told its user to go and find "Daimond's
6//! feedback/issue reporting interface (typically accessible from a menu in the app)", which is
7//! the panel it had just failed to find, and the other spent eighteen calls and finished with
8//! "The gateway isn't running." The gateway was running. The rule they broke is
9//! [`crate::tools::Tool::FileShow`]'s, and it is stated there as a defect class rather than a
10//! feature: a working surface the model cannot reach is a surface the model will deny.
11//!
12//! **THE PANEL COMPOSES THE PROSE ABOUT RECORDS AND THIS SIDE COMPOSES THE POLICY.** Which
13//! proposals exist, what state each is in, what its tally is and what a note's sealed build
14//! identifier is are all the panel's answers, drawn on a screen the user is looking at; a second
15//! renderer here would be a second opinion in a second language, disagreeing with that screen the
16//! first time either changed. It is the same argument [`crate::tools::Shown`] is written under.
17//! What Rust keeps is every refusal and the consent question, because those are the sentences
18//! that decide something.
19//!
20//! **CONSENT IS BOUND TO BYTES AND NOT TO ARGUMENTS**, which is why publishing is two calls and
21//! not one. [`compose`] asks the panel what would actually leave and hands back both the
22//! characters the user is shown and an opaque token standing for them; [`commit`] sends what that
23//! token holds. Composing once and sending the arguments again afterwards would mean the user
24//! approved a rendering and the app sent a rebuild of it, and the two part company at exactly the
25//! field a person would want to have seen -- the build identifier that travels with a note, the
26//! title of the proposal a vote lands on.
27
28use crate::llm::extract_json_string;
29use crate::wasm::js_str;
30
31use oxedyne_fe2o3_core::prelude::*;
32
33use wasm_bindgen::prelude::wasm_bindgen;
34use wasm_bindgen::{JsCast, JsValue};
35use wasm_bindgen_futures::JsFuture;
36
37
38#[wasm_bindgen]
39extern "C" {
40
41 /// The driver object `www/js/improve.js` installs at `window.DaimondSocial`.
42 #[wasm_bindgen(js_name = DaimondSocial)]
43 type Panel;
44
45 /// Read one view of the panel, and answer with the text a model reads.
46 #[wasm_bindgen(method)]
47 fn read(this: &Panel, req: &str) -> js_sys::Promise;
48
49 /// Work out exactly what one act would put on the wire, without sending any of it.
50 #[wasm_bindgen(method)]
51 fn compose(this: &Panel, req: &str) -> js_sys::Promise;
52
53 /// Send what a `compose` composed, named by the token it minted for it.
54 #[wasm_bindgen(method)]
55 fn commit(this: &Panel, token: &str) -> js_sys::Promise;
56}
57
58
59/// What came back from asking the panel what one act would publish.
60///
61/// A refusal is an ANSWER and not an error. Every reason a compose can fail -- no voice on the
62/// forge, a proposal number that is not there, an act this build does not have -- is something
63/// the model can act on and must be told in its own language, so it comes back as a sentence to
64/// hand over rather than as an `Err` the dispatcher would dress in a failure line.
65#[derive(Clone, Debug)]
66pub enum Composed {
67 // Ready to put to the user. `token` is opaque on this side on purpose: Rust must not be
68 // able to assemble one, or the binding between what was seen and what is sent is a
69 // convention rather than a mechanism.
70 Draft {
71 shown: String, // exactly what the user is shown
72 token: String, // the panel's handle on the payload those characters describe
73 },
74 Refused(String), // nothing was composed, and this is what the model reads instead
75}
76
77/// Reach the driver object on `window`, or refuse in the model's language.
78fn panel() -> Outcome<Panel> {
79 let win = res!(web_sys::window()
80 .ok_or_else(|| err!("Reaching the Social panel needs a browser window."; System, Missing)));
81 let obj = res!(js_sys::Reflect::get(&win, &JsValue::from_str("DaimondSocial"))
82 .map_err(|e| err!("Reading window.DaimondSocial failed: {}.", js_str(&e); System, Missing)));
83 if obj.is_undefined() || obj.is_null() {
84 return Err(err!(
85 "Daimond's Social panel is not loaded in this page. Tell the user what you wanted to \
86 read or publish there, and carry on without it."; System, Missing));
87 }
88 Ok(obj.unchecked_into::<Panel>())
89}
90
91/// The `message` of a rejected JS `Error`, verbatim. A refusal from the driver is written for
92/// the model to read and act on, so nothing here rewords it.
93fn refusal(e: &JsValue) -> String {
94 match js_sys::Reflect::get(e, &JsValue::from_str("message")) {
95 Ok(m) => m.as_string().unwrap_or_else(|| js_str(e)),
96 Err(_) => js_str(e),
97 }
98}
99
100/// Settle a driver promise that answers with a string.
101async fn text(p: js_sys::Promise) -> Outcome<String> {
102 let v = match JsFuture::from(p).await {
103 Ok(v) => v,
104 Err(e) => return Err(err!("{}", refusal(&e); IO, Invalid)),
105 };
106 match v.as_string() {
107 Some(s) => Ok(s),
108 None => Err(err!(
109 "The Social panel answered with something that cannot be read, so what it did is \
110 unknown. Do not tell the user it worked."; Invalid, Data)),
111 }
112}
113
114/// Read one view, and answer with what the panel says is on it.
115///
116/// # Arguments
117/// * `req` - The request [`crate::tools::social_read_step`] composed.
118pub async fn read(req: &str) -> Outcome<String> {
119 let p = res!(panel());
120 text(p.read(req)).await
121}
122
123/// Ask what one act would publish, without publishing any of it.
124///
125/// # Arguments
126/// * `req` - The request [`crate::tools::social_send_step`] composed.
127pub async fn compose(req: &str) -> Outcome<Composed> {
128 let p = res!(panel());
129 let json = res!(text(p.compose(req)).await);
130 if let Some(no) = extract_json_string(&json, "refusal") {
131 if !no.trim().is_empty() {
132 return Ok(Composed::Refused(no));
133 }
134 }
135 // Neither field may be empty and neither is optional. A draft with nothing to show would
136 // put an empty dialog in front of the user, who would then be approving a blank -- and a
137 // draft with no token would be a yes with nothing to spend it on.
138 let shown = match extract_json_string(&json, "shown") {
139 Some(s) if !s.trim().is_empty() => s,
140 _ => return Err(err!(
141 "The Social panel did not say what it would publish, so there is nothing to put to \
142 the user. Nothing was sent."; Invalid, Data)),
143 };
144 let token = match extract_json_string(&json, "token") {
145 Some(t) if !t.trim().is_empty() => t,
146 _ => return Err(err!(
147 "The Social panel composed something and would not name it, so there is no way to \
148 send exactly what the user would have approved. Nothing was sent."; Invalid, Data)),
149 };
150 Ok(Composed::Draft { shown, token })
151}
152
153/// Publish what a [`compose`] composed.
154///
155/// # Arguments
156/// * `token` - The handle the panel minted for that draft, and no other.
157pub async fn commit(token: &str) -> Outcome<String> {
158 let p = res!(panel());
159 text(p.commit(token)).await
160}