Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
a5daf6c
Merge pull request #479 from deeplethe/main
WaylandYang Sep 8, 2026
b99bcef
An event holds at the moment it names (#487)
WaylandYang Sep 8, 2026
0fffff4
Classes, properties, attributes and knowledge edit in dialogs; the pa…
WaylandYang Sep 8, 2026
84f45ec
A decision records why (#473)
WaylandYang Sep 8, 2026
9f36f1f
Record 0028 becomes 0031 (#489)
WaylandYang Sep 8, 2026
3e1cb40
An automatic merge is gated by what it can undo (#474)
WaylandYang Sep 8, 2026
0c98e7d
The adjudicator looks before it asks (#475)
WaylandYang Sep 8, 2026
fcb0f4f
The import history shows what each import actually did (#481)
WaylandYang Sep 8, 2026
4f6afb5
Rules read as a table, and a rule is written in a dialog (#482)
WaylandYang Sep 8, 2026
3a2f3ef
A rule is an edge on the schema diagram (#483)
WaylandYang Sep 8, 2026
cae4291
A rule may say or, once (#484)
WaylandYang Sep 8, 2026
cb2bab7
A rule reads what a rule concluded (#485)
WaylandYang Sep 8, 2026
017896b
A Chinese name does not stop the queue, and a stated figure is writte…
WaylandYang Sep 9, 2026
db1d37d
A rule that says not one of is not silently skipped (#494)
WaylandYang Sep 9, 2026
f97eecd
KB members read like the deployment's user list (#492)
WaylandYang Sep 9, 2026
7312fed
Re-running a document is an icon, not three different words (#491)
WaylandYang Sep 9, 2026
28ae808
A rule's criterion is a column, not a sentence (#490)
WaylandYang Sep 9, 2026
010a663
A rule computes what it concludes, written down (#493)
WaylandYang Sep 9, 2026
3c33a52
A rule computes what it concludes (#495)
WaylandYang Sep 9, 2026
3601089
One canvas, drawn once (#499)
WaylandYang Sep 9, 2026
47335ab
The schema diagram draws the biggest classes, and reads lighter (#511)
WaylandYang Sep 9, 2026
c1c657e
The ontology reads as three tables (#524)
WaylandYang Sep 9, 2026
cfccaa1
A fact row leads with the fact (#506)
WaylandYang Sep 9, 2026
a4d210f
A floating panel does not borrow the colour behind it (#525)
WaylandYang Sep 9, 2026
90f7c15
Scope RSS hydration summaries to each source (#462)
Floating-Y Sep 9, 2026
131aefa
Wake idle workers when jobs are queued (#521)
rksharma-owg Sep 9, 2026
acc4afd
Returning to a page does not refetch what the stream keeps fresh (#533)
WaylandYang Sep 9, 2026
362ec01
A glass panel does not flash white on its way in (#532)
WaylandYang Sep 9, 2026
789f551
A page renders before its data arrives (#529)
WaylandYang Sep 9, 2026
b50df8f
Selecting a class goes straight to its highlight (#534)
WaylandYang Sep 9, 2026
cb996aa
A search does not hold a runtime worker, and its two channels run sid…
WaylandYang Sep 9, 2026
dd71624
A predicate is named one way (#536)
WaylandYang Sep 9, 2026
f3a3847
A purge judges its blobs in one question (#537)
WaylandYang Sep 9, 2026
43d68d7
Ingest embeds four batches at a time, and both ingests share the loop…
WaylandYang Sep 9, 2026
357d4ac
LLM errors preserve their response bodies (#538)
ousamabenyounes Sep 9, 2026
1029a14
The LLM client's literals read at the point of use, and its test serv…
WaylandYang Sep 9, 2026
dc64bd3
A promise to search is not an answer (#543)
WaylandYang Sep 9, 2026
932c3ba
Model probes report reachability and authentication (#541)
ousamabenyounes Sep 9, 2026
2abdabc
A derivation rewinds with the second clock (#557)
WaylandYang Sep 9, 2026
2bc9036
The ontology tables read like a table (#540)
WaylandYang Sep 9, 2026
b26b571
The docked panel and the table say where you are (#545)
WaylandYang Sep 9, 2026
72019ca
A vector index is built by a job (#566)
WaylandYang Sep 9, 2026
fcefd76
A conversation reads at reading size (#551)
WaylandYang Sep 9, 2026
ad8517f
fix(ontology): show class hierarchy links (#544)
ousamabenyounes Sep 9, 2026
526fc91
Add bulk mapping decisions (#505) (#552)
ousamabenyounes Sep 9, 2026
a5a94e0
An unset variable stays unset on the way into the container (#572)
lml2468 Sep 9, 2026
0f6ffb2
An undeclared name does not become a node (#568)
WaylandYang Sep 9, 2026
f92c276
One relation, one predicate (#569)
WaylandYang Sep 9, 2026
0c95565
The one variable that keeps the old form says why (#576)
WaylandYang Sep 9, 2026
eb27c4a
Picking an engine before typing does not blank the page (#577)
WaylandYang Sep 9, 2026
711949f
An action is a declared call, written down (#531)
WaylandYang Sep 9, 2026
f7712c9
The graph answers with paths, neighbours and a timeline (#565)
WaylandYang Sep 9, 2026
e049cf3
The schema document is searched, not extracted (#561)
WaylandYang Sep 9, 2026
8c6be84
A definition can be written by hand (#563)
WaylandYang Sep 9, 2026
8c88776
The semantic layer gets measured, and exploration learns to align (#567)
WaylandYang Sep 9, 2026
630b1b6
Exploration describes the data and asks what it cannot tell (#571)
WaylandYang Sep 9, 2026
0fc44a7
A question picks its definitions instead of taking the first thirty (…
WaylandYang Sep 9, 2026
833fc30
A group described by its relation to X is not X (#579)
WaylandYang Sep 10, 2026
cebda7b
The highlight is drawn last (#584)
WaylandYang Sep 10, 2026
86b4ae7
A new base starts with no ontology pack (#585)
WaylandYang Sep 10, 2026
9aaf8ca
A written quantity is a value, not a node (#586)
WaylandYang Sep 10, 2026
44d16fa
A fact quotes the words that name each side (#583)
WaylandYang Sep 10, 2026
ffbb24b
A quantity is never an entity (#587)
WaylandYang Sep 10, 2026
2838296
A relation says whether it happens or holds (#593)
WaylandYang Sep 10, 2026
e0d9383
A script whose usage line says ./ is executable (#591)
lml2468 Sep 10, 2026
5c28a56
0031 records that a pack relation is a state by construction (#597)
WaylandYang Sep 10, 2026
0e3123b
A relation carries its own attributes (#598)
WaylandYang Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,7 @@ jobs:
cache-dependency-path: web/pnpm-lock.yaml
- name: Install
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm test
- name: Build (含类型检查)
run: pnpm build
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,14 @@ jobs:
done
# 故意不给 UTOPIA_JWT_SECRET:让这一跑覆盖自动生成那条路径。
# 下面的注册请求会签发并携带 token,密钥没生成好就过不了。
#
# UTOPIA_MIGRATION_URL 显式传空串,而不是不传:compose 的
# `${VAR:-}` 在变量没设时发出去的就是空串,而这条冒烟测试原本直接
# docker run、从不传它,于是 #343 全绿发布、用户照 README 起就退出
# ("relative URL without a base")。空串必须等同于未设,在这里钉住。
docker run -d --name app --network smoke -p 1516:1516 \
-e UTOPIA_DATABASE_URL=postgres://utopia:utopia@pg:5432/utopia \
-e UTOPIA_MIGRATION_URL= \
utopia:smoke

# 必须打 /api/v1/health 并核对 JSON:健康检查挂在 nest("/api/v1") 里,
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

134 changes: 131 additions & 3 deletions crates/utopia-core/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,61 @@ pub struct Source {
pub created_at: DateTime<Utc>,
}

impl Source {
/// 这个来源下的文档要不要进抽取。
///
/// **缺省是要。** 只有 config 里 `{"extract": false}` 明说了才不抽——schema
/// 文档就是这样(0035 决定 7):它是给问数检索表结构的语料,不是事实的来源,
/// 进抽取的结果是每个列名变成一个实体。开关记在来源上而不是文档上,因为
/// 「只检索、不学习」是这一整个来源的性质;也没有拿来源的名字当规则,那是
/// 命名约定冒充类型保证(0009)。
///
/// 值不是布尔的按没写处理:一个手滑不该让一整个来源静默停抽。
/// `documents::queue_extraction` 里的 SQL 判的是同一件事,改一处要改两处。
pub fn extracts(&self) -> bool {
self.config
.get("extract")
.and_then(serde_json::Value::as_bool)
.unwrap_or(true)
}
}

#[cfg(test)]
mod source_extracts_tests {
use super::Source;

fn with(config: serde_json::Value) -> Source {
Source {
id: uuid::Uuid::nil(),
kb_id: uuid::Uuid::nil(),
kind: "folder".into(),
name: "x".into(),
config,
icon: None,
sync_interval_minutes: None,
sync_cron: None,
last_sync_at: None,
last_sync_status: "never".into(),
last_sync_error: None,
last_sync_added: 0,
ingest_token: None,
created_at: chrono::Utc::now(),
}
}

#[test]
fn only_an_explicit_false_turns_extraction_off() {
// 老来源的 config 是 `{}`,watch_folder 的是 `{"path": …}`:都照旧抽取
assert!(with(serde_json::json!({})).extracts());
assert!(with(serde_json::json!({ "path": "/x" })).extracts());
assert!(with(serde_json::json!({ "extract": true })).extracts());
// 不是布尔的按没写处理,而不是按 false
assert!(with(serde_json::json!({ "extract": "no" })).extracts());
assert!(with(serde_json::json!({ "extract": 0 })).extracts());
assert!(!with(serde_json::json!({ "extract": false })).extracts());
}
}

/// 来源配置里**用来鉴权**的那几个键。凭据只进不出:列表与创建 / 更新的响应都剔掉,
/// 更新时客户端没传或传空串就保留库里的原值,审计里也不落。
///
Expand Down Expand Up @@ -453,6 +508,19 @@ pub struct RelationTypeView {
pub usage: i64,
}

/// 一条边上挂的一个属性值(0037)。`value` 与 `entity` 二选一:
/// 金额、比例、日期是字面值;「经 C 撮合」里的 C 是实体(这一格这一刀还不写,位置留着)
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FactQualifier {
pub qualifier_type_id: Uuid,
pub key: String,
pub label: String,
/// 形状与 `facts.object_value` 一致:{"value": …, "unit": …}
pub value: Option<serde_json::Value>,
pub entity_id: Option<Uuid>,
pub entity_name: Option<String>,
}

/// 抽取未匹配统计(本体扩展建议的信号源)。
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
pub struct OntologyMiss {
Expand Down Expand Up @@ -550,6 +618,10 @@ pub struct RelationType {
/// attribute 专用:text | number | date | bool
pub datatype: Option<String>,
pub unit: Option<String>,
/// **这条关系的边能带哪些属性**(0037):指向 kind='attribute' 的行。
/// `A invested B` 上的「金额」是边自己的属性,不是第二个宾语;金额的
/// datatype / unit / 换算全复用属性定义,只是它的 domain 是一条关系而不是一个类
pub qualifiers: Vec<Uuid>,
}

#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
Expand Down Expand Up @@ -600,6 +672,9 @@ pub struct GraphEdge {
/// **与 `inferred` 不是一回事**,尽管两个词很近:那一位说的是「名字来自原文
/// 而不是本体」,这一位说的是「这条边根本不是谁说的,是引擎推的」
pub derived: bool,
/// 边上的属性(0037):画布把金额写到边的标签上要靠它
#[sqlx(skip)]
pub qualifiers: Vec<FactQualifier>,
/// 推它出来的那条规则(`transitive` / `symmetric` / `inverse` / `sub_property`);
/// 断言的边为 None。
///
Expand Down Expand Up @@ -645,8 +720,13 @@ pub struct EntityFact {
pub temporal: Option<String>,
pub other_id: Option<Uuid>,
pub other_name: Option<String>,
/// 对端实体的类型标签;属性事实没有对端时为 None
pub other_type: Option<String>,
/// 字面值宾语(属性事实/问数映射):{"value":…,"unit":…} 或 {"summary":…}
pub object_value: Option<serde_json::Value>,
/// 边上的属性(0037)。不在行里——`fact_qualifiers` 另一张表,加载后按事实 id 补
#[sqlx(skip)]
pub qualifiers: Vec<FactQualifier>,
pub valid_from: Option<DateTime<Utc>>,
pub valid_to: Option<DateTime<Utc>>,
/// 精度描述的是这条事实**有的那些日期**的粒度。两端都没有日期时为 None——
Expand Down Expand Up @@ -926,6 +1006,14 @@ pub struct KnowledgeBase {
/// 上次推完的时间。**答的是「上次看过没有」,不是「上次改过没有」**
pub last_inference_at: Option<DateTime<Utc>>,
pub ontology_lang: String,
/// 探索从 schema 写的数据描述:一行是什么、键、单位、码值、时间轴、相似列。
/// 只写 schema 说了的;每次探索重写
pub data_description: Option<String>,
/// 探索拿不准、需要库的主人答的问题(JSON 字符串数组)
pub data_questions: serde_json::Value,
/// 人写的约定:「测试单不算数」「有效订单是 2/3/4」这类 schema 里没有的规则。
/// 探索不碰它——量过:宽表语料上问数没有约定 2/18,有 14/18(#520)
pub data_conventions: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
Expand Down Expand Up @@ -1004,6 +1092,35 @@ pub struct MappingRevision {
pub changed_at: DateTime<Utc>,
}

/// 一轮映射探索扫了什么、丢了什么、剩下什么(#503)。
///
/// **它回答的是覆盖率**:十一条提议对着一张八十列的宽表,与十一条刚好覆盖完
/// 一个小库,从 `concept_mappings` 里看长得一模一样。分子是 `tables_covered`,
/// 分母是 `tables_scanned`,而 `schema_truncated` 说明覆盖不全是「没看见」
/// 还是「看见了没提」。
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
pub struct ExplorationRun {
pub id: Uuid,
pub started_at: DateTime<Utc>,
pub finished_at: Option<DateTime<Utc>>,
pub sources: Vec<String>,
pub tables_scanned: i32,
pub columns_scanned: i32,
/// schema 文本撞了上限:提示词里没有的表,模型没有机会提
pub schema_truncated: bool,
/// 这一轮允许提几条(按表数放大)
pub cap: i32,
/// 模型回了几条 / 落库几条。两者之差是被丢掉的,明细在 `dropped`
pub returned: i32,
pub accepted: i32,
/// `{"source": {"n": 12, "example": "…"}, …}`,键见
/// `utopia_store::exploration_runs::drop_reason`
pub dropped: serde_json::Value,
pub tables_covered: Vec<String>,
/// 跑挂了的那一轮也留一行——失败与「跑了但什么都没提」不是一回事
pub error: Option<String>,
}

/// 语义层的一条映射:业务概念 → 数据资产定义(见 `docs/decisions/0011`)。
///
/// **字段是列,不是 JSON 里的键。** 从前它是一条 `mapped_to` 事实,
Expand All @@ -1030,6 +1147,9 @@ pub struct ConceptMapping {
/// **状态而不是置信度。** 从前借事实的 confidence 表达「提议 0.6 / 确认 1.0」,
/// 那是把二值状态编码成浮点数,还顺带让它落进「低置信事实」那一档
pub status: String,
/// 人从零写的口径记写的人;探索提的为空(#562)。`decided_by` 分不出这件事——
/// 探索提的经人确认之后同样有 decided_by
pub written_by: Option<Uuid>,
}

/// 一处公理违规,配好展示所需的三元组文本(见 `axiom_violations`)。
Expand Down Expand Up @@ -1139,14 +1259,19 @@ pub struct BlockedDerivation {
pub premises: Vec<Uuid>,
}

/// 证明的一步:一条断言前提,连同它的证据(0002 R2)。
/// 证明的一步:一条前提,连同它的证据(0002 R2)。
///
/// 前提一律是断言(`fact_derivations` 不记派生),所以证明是一条链而不是一棵树:
/// 派生 → 按 `seq` 排好的断言 → 每条断言的原句。叶子就是 chunk。
/// 前提要么是断言——叶子就是它的原句(chunk)——要么是**另一条派生**(0030),
/// 那一步的证据是它自己的前提,在 `premises` 里再往下一层。所以证明是一棵树,
/// 深度与推理同一条上限。
#[derive(Debug, Clone, Serialize)]
pub struct ProofStep {
pub seq: i32,
/// 断言时是 `facts.id`,派生时是 `derived_facts.id`——看 `derived`
pub fact_id: Uuid,
/// 这一步自己是推出来的(0030)。**界面要分得出**:一条推出来的前提与
/// 一条读来的前提在句子上长得一样,而它们能不能追到原文完全不同
pub derived: bool,
pub subject_id: Uuid,
pub subject: String,
pub predicate_id: Option<Uuid>,
Expand All @@ -1161,6 +1286,9 @@ pub struct ProofStep {
/// 这条前提后来被撤了。派生随之失效,但证明还要读得出「当时靠的是什么」
pub retracted: bool,
pub evidence: Vec<EvidenceView>,
/// 这一步自己的前提,按 `seq`(0030)。断言那一步是空的——它的叶子是
/// `evidence` 里的原句,不必再往下问
pub premises: Vec<ProofStep>,
}

/// 一条派生事实的完整证明:它本身,加上按顺序展开到原句的前提。
Expand Down
29 changes: 25 additions & 4 deletions crates/utopia-extract/src/governor.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! 治理的第二层(0025 第二刀):攒批判不定的对,逐条带工具再看一遍。
//!
//! 模型能看的东西:一侧的全部事实、一侧的原文片段、台账里人对某个名字的决定、
//! 库里名字相近的其他实体。结束只有两种:`decide`(same / different + 置信度 +
//! 库里名字相近的其他实体、合并会牵动什么(0028:一致性检查会开出的矛盾、靠着
//! 一边的派生、点过名的回答、两边的类型是不是一个大类)。结束只有两种:`decide`(same / different + 置信度 +
//! 一句理由)或 `defer`(留给人一个具体的问题)。工具定义、提示词与回合的解析
//! 在这里;跑循环、查库的在 server 的 governance 任务里——这里不碰库也不碰模型。

Expand All @@ -23,7 +24,7 @@ pub struct EarlierLook<'a> {
/// 模型在一个回合里要的事
#[derive(Debug, PartialEq)]
pub enum Step {
/// 查一样东西:facts / quotes / ledger / namesakes
/// 查一样东西:facts / quotes / ledger / namesakes / consequences
Lookup {
tool: String,
args: Value,
Expand Down Expand Up @@ -69,6 +70,10 @@ pub fn tools() -> Value {
"name": "namesakes",
"description": "Other entities in this base whose name contains the query, with their type and how many facts they carry.",
"parameters": query }},
{ "type": "function", "function": {
"name": "consequences",
"description": "What merging A and B would touch: relations that allow one value where the two sides hold different ones, derived facts resting on either side, chat answers that named either side, and whether the two types belong to one family. A merge that would touch any of these is held for a person whatever your confidence.",
"parameters": { "type": "object", "properties": {} } }},
{ "type": "function", "function": {
"name": "decide",
"description": "Give the verdict for this pair.",
Expand Down Expand Up @@ -105,7 +110,11 @@ pub fn messages(pair: &AdjudicationPair, earlier: &EarlierLook) -> Vec<Value> {
\n\
You may look things up before answering, at most {MAX_STEPS} lookups: the facts of a \
side, the source passages that mention a side, what people in this base decided about \
a name, and other entities with a similar name. People's earlier decisions are how the \
a name, other entities with a similar name, and what merging the two would touch (a \
relation that allows one value where the sides hold different ones is a contradiction, \
and a merge that would touch anything outside the graph is held for a person whatever \
your confidence: prefer to defer with the question that would settle it). People's \
earlier decisions are how the \
owners of this base want such cases judged; follow them unless the facts of this pair \
clearly differ, and never let one override a contradiction in the facts. Look only for \
what would change your answer.\n\
Expand Down Expand Up @@ -174,6 +183,10 @@ pub fn read_step(name: &str, arguments: &str) -> Step {
},
_ => Step::Unknown(format!("{name} needs a query")),
},
"consequences" => Step::Lookup {
tool: name.into(),
args: json!({}),
},
"decide" => {
let same = match args["verdict"].as_str() {
Some("same") => true,
Expand Down Expand Up @@ -299,7 +312,15 @@ mod tests {
.collect();
assert_eq!(
names,
["facts", "quotes", "ledger", "namesakes", "decide", "defer"]
[
"facts",
"quotes",
"ledger",
"namesakes",
"consequences",
"decide",
"defer"
]
);
}
}
Loading
Loading