mingli_contract/ports.rs
1//! 两个端口本身。
2//!
3//! [`CastingEngine`] 是吃时刻的那一条,[`WordEngine`] 是吃字与笔画的那一条;
4//! 一片叶实现其一,编排层只认这两个 trait,双方都不认识对方。
5
6use crate::{DetItem, Family, Intent, Moment, Query, SchoolItem, Subject};
7use serde::Serialize;
8use serde_json::Value;
9
10
11///
12/// 「寻方位」这个意图([`crate::QueryKind::Locative`])要的是**结构**——哪个要素落在哪一宫、
13/// 那一宫朝哪个方向——至于所寻之事该取哪一宫为用,各家不同,属判读,不在本层。
14///
15/// 这是端口层的词汇而不是某片叶的:奇门读值符值使与门奇、六壬读三传之支、小六壬读所落之宫,
16/// 三者的**盘**毫无共同之处,但产出的**候选**是同一种东西。用例层只认这个形状,
17/// 不必知道候选是从哪种盘上怎么读出来的。
18/// 一个方位候选:盘上的某个要素落在哪一方。
19#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
20pub struct Bearing {
21 /// 来源叶的稳定 id。
22 pub leaf: &'static str,
23 /// 要素名(值符 / 值使 / 开门 / 乙奇 / 初传 …)。
24 pub element: String,
25 /// 落点的字面(奇门的「坎1」、六壬的「子」)。
26 pub at: String,
27 /// 方位。
28 pub direction: &'static str,
29 /// 附注:同宫的门 / 星 / 神 / 旺衰等结构事实,供判读。
30 pub note: String,
31}
32
33/// 一片叶的**主判据**:这套系统据以起论的那个低基数分类量。
34///
35/// 四柱取日支(12 值)、紫微取命宫支(12)、西洋占星取太阳所在星座(12)、
36/// 印度占星取月宿(27)、择日取建除(12)——每套系统都有这么一个「先看哪里」的量,
37/// 它是该系统自己的领域概念,不是为了给谁做统计才有的。
38///
39/// 跨叶做信息论比较时正好用得上它(见 `mingli-analysis`),但那只是一个消费者:
40/// 即使没有任何统计,「这套系统起论看哪一个量」仍然是这片叶该回答的问题。
41#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
42pub struct Principal {
43 /// 这个量叫什么(如「日支」「命宫支」「太阳星座」)。
44 pub label: &'static str,
45 /// 本次取值。取值域应当是低基数的有限集合。
46 pub value: String,
47}
48
49/// 一片叶:在共享上下文上排盘并产出统一 JSON,并声明自己答什么、据什么起论。
50pub trait CastingEngine: Send + Sync {
51 /// 稳定标识(作为输出 map 的 key)。
52 fn id(&self) -> &'static str;
53 /// 显示名。
54 fn name(&self) -> &'static str;
55 /// 计算家族。
56 fn family(&self) -> Family;
57 /// 在共享上下文 `m` 上排盘,输出统一 JSON。
58 fn cast(&self, m: &Moment, q: &Query) -> Value;
59 /// 确定性谱:本叶各方面是确定/随机/欠定。默认空,每叶覆盖以显式声明 DET/STO/UND 边界。
60 fn profile(&self) -> &'static [DetItem] {
61 &[]
62 }
63 /// 从本叶的盘上读出方位候选;本叶与方位无关则返回空(默认)。
64 ///
65 /// 用例层的「寻方位」靠这个方法而不是去解析各叶的输出 JSON——叶改个字段名,
66 /// 解析 JSON 的写法**不会编译报错**,只会静默少出候选;走这里则由类型系统盯着。
67 fn bearings(&self, _m: &Moment, _q: &Query) -> Vec<Bearing> {
68 Vec::new()
69 }
70 /// 本叶支持的流派集合(空=无流派分歧);每叶应恰有一个 `default=true`。
71 fn schools(&self) -> &'static [SchoolItem] {
72 &[]
73 }
74 /// 本叶答哪几类问局。缺省只答[`Intent::Natal`]——每片时刻叶都能给出生切片。
75 ///
76 /// 编排层据此路由(见 `mingli_engine::route`):加一片叶时,它答什么由它自己说,
77 /// 不需要回头改端口层或编排层的任何清单。
78 ///
79 /// **判定标准是「当下算得出这一类的 [`output_shape`](crate::IntentSpec::output_shape)」,
80 /// 不是「传统上该答」。** 这条声明直接决定运行时路由,声明了却产不出东西就是空跑,
81 /// 比少声明糟得多;而「算不算得出」看本叶的实现即可判定,「传统上该不该答」则要考据,
82 /// 按本项目的规矩需要 ≥2 个独立来源。
83 ///
84 /// 「算得出」要落到那个形态上,不是「沾边」:一片叶能给某个时刻的值,不等于它答得起
85 /// 「势(时间序列)」;能给某个神煞落宫,不等于它答得起「位(方位)」——后者要真的
86 /// 产出方位候选([`bearings`](CastingEngine::bearings))。
87 ///
88 /// 某类问局这套系统传统上确实用得着、只是本叶还没实现,那是 [`profile`](CastingEngine::profile)
89 /// 里一条 🟡 [`crate::Determinism::Und`] 该说的话——并且要按规矩分清是「查过定不下」还是「还没查」。
90 fn answers(&self) -> &'static [Intent] {
91 &[Intent::Natal]
92 }
93 /// 本叶盘面的读法:各字段是什么、传统上先看哪一处。没有则返回 `None`(默认)。
94 ///
95 /// 这是本叶的领域知识——「`strength.wuxing` 是五行力量分布、缺者宜补旺者宜泄」这种话,
96 /// 只有写这片叶的人说得准。释义层把它原样交给后端,自己不攒也不改。
97 ///
98 /// # 写法
99 ///
100 /// 读的人(语言模型)手上只有本叶输出的那份 JSON。它认得字,不认得这套系统,
101 /// 于是缺什么就补什么——**缺的是字段与领域概念之间的那一层**,不是结论。
102 ///
103 /// 1. **一条一个字段**,用反引号写出**真实的 JSON 路径**(`gates.zhi_shi_gate`,
104 /// 不是「值使门」)。路径写错,读的人会去找一个不存在的键
105 /// 2. **说取值域**:几个值、什么含义、有没有序(「旺相休囚死,前二为强、后三为弱」)
106 /// 3. **说结构事实,不下断语**。「三奇临吉门,传统视为得力」可以;「主大吉」不可以——
107 /// 吉凶归释义后端在护栏内自己判,读法只交事实
108 /// 4. **指明先看哪里**。每套系统都有起论之处(值符宫、命宫、法官位),
109 /// 没有这一句,模型会把九个宫平铺着讲一遍
110 /// 5. **点到 🟡**:本叶 [`profile`](CastingEngine::profile) 里标欠定的地方,
111 /// 读法里也要说一声,免得模型把留白当成漏算
112 /// 6. **篇幅与字段数相称**。字段少的叶两三句就够;把一片叶的读法写成一句话,
113 /// 等于没写(守卫会红)
114 ///
115 /// 已有的三条可作样板:四柱(字段最多、层次最深)、紫微(宫位制)、奇门(四盘叠加)。
116 fn reading_notes(&self) -> Option<&'static str> {
117 None
118 }
119 /// 同一套计算换个主体读时的象义重映射(公司 / 物 / 事)。默认无——多数叶不含
120 /// 宫位、十神、六亲这类随主体改变所指的概念,对它们 person 与其余主体等价。
121 fn subject_notes(&self, _subject: Subject) -> Option<&'static str> {
122 None
123 }
124 /// 本叶的[主判据][`Principal`];本叶没有这样一个量则返回 `None`(默认)。
125 ///
126 /// 实现应当从自己的**强类型盘面**取,不要去解自己输出的那份 JSON——
127 /// 改个字段名时,前者编译报错,后者只会静默失灵。
128 fn principal(&self, _m: &Moment, _q: &Query) -> Option<Principal> {
129 None
130 }
131}
132/// 一片叶的带元数据输出(承接层展示用:id / 显示名 / 家族 / 盘)。
133#[derive(Debug, Clone, Serialize)]
134pub struct LeafOutput {
135 /// 稳定标识。
136 pub id: &'static str,
137 /// 显示名。
138 pub name: &'static str,
139 /// 计算家族。
140 pub family: Family,
141 /// 家族中文标签。
142 pub family_label: &'static str,
143 /// 确定性谱(DET/STO/UND 边界)。
144 pub profile: &'static [DetItem],
145 /// 本叶支持的流派(空 = 无流派分歧)。
146 pub schools: &'static [SchoolItem],
147 /// 当前实际生效的流派 id(从 `q.schools` 取;若未指定,落到 default;无流派则空串)。
148 pub effective_school: String,
149 /// 排盘结果(统一 JSON)。
150 pub chart: Value,
151}
152
153/// 取某叶在本次查询下实际生效的流派 id(未指定则落到该叶的 default,无流派则空串)。
154#[must_use]
155pub fn effective_school_id(e: &dyn CastingEngine, q: &Query) -> String {
156 if let Some(sel) = q.schools.get(e.id()) {
157 return sel.clone();
158 }
159 e.schools()
160 .iter()
161 .find(|s| s.default)
162 .map_or_else(String::new, |s| s.id.to_string())
163}
164
165/// 字/词模态的一次查询(D 族:与出生时刻无关,吃文字或笔画)。
166#[derive(Debug, Clone, Default, Serialize, serde::Deserialize)]
167pub struct WordQuery {
168 /// 待取值的词(希伯来 gematria / 阿拉伯 abjad)。
169 pub text: Option<String>,
170 /// 姓各字笔画(五格用)。
171 pub surname: Option<Vec<u32>>,
172 /// 名各字笔画(五格用)。
173 pub given: Option<Vec<u32>>,
174}
175
176/// 一片**字词叶**:不吃共享时刻,只吃文字/笔画。
177///
178/// 与 [`CastingEngine`] 平行的第二个端口——D 族里 gematria / abjad / wuge 这三片叶
179/// 与时间无关,无法进入 moment fan-out,于是单列一条契约。
180pub trait WordEngine: Send + Sync {
181 /// 稳定标识(HTTP `system` 字段与输出 key)。
182 fn id(&self) -> &'static str;
183 /// 显示名。
184 fn name(&self) -> &'static str;
185 /// 取值。输入不足时返回 `Err` 并给出面向调用方的中文说明。
186 ///
187 /// # Errors
188 ///
189 /// 当查询缺少该叶必需的输入原子时返回错误说明(如五格缺姓或名的笔画)。
190 fn compute(&self, q: &WordQuery) -> Result<Value, String>;
191 /// 确定性谱。默认空,每叶覆盖。
192 fn profile(&self) -> &'static [DetItem] {
193 &[]
194 }
195}