Skip to main content

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}