Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion crates/utopia-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ struct ManifestDataDir {
/// not a side effect of a code change.
// 是迁移文件的**个数**,不是最大的编号(守卫 `schema_version_policy_compares_against_current`
// 按个数比):编号有空缺时两者不同——0071 由一个开放 PR 占着,0072 先落,个数是 71
const CURRENT_SCHEMA_VERSION: u32 = 88;
const CURRENT_SCHEMA_VERSION: u32 = 89;

fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
Expand Down
1 change: 1 addition & 0 deletions crates/utopia-extract/src/implication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ mod tests {
label: key,
description: "",
kind: "relation",
temporal: "state",
domains: vec![],
ranges: vec![],
via: vec![],
Expand Down
265 changes: 223 additions & 42 deletions crates/utopia-extract/src/phrase_align.rs

Large diffs are not rendered by default.

42 changes: 37 additions & 5 deletions crates/utopia-server/src/api/review_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,10 @@ pub struct DecideAlignmentPhraseReq {
/// forward | reverse;给了属性就得给
#[serde(default)]
pub direction: Option<String>,
/// start | end | none:状态属性下,带着某一刻的陈述那一刻标的是开始、结束,还是都不是
/// (#966)。不给 = 未知:这样的陈述不算类型化行,签名留在队列里等人补
#[serde(default)]
pub marks: Option<String>,
}

/// 人定一条短语签名绑到哪个属性(#725 对齐队列)。写成人的判定,代理此后不再改它;
Expand Down Expand Up @@ -1293,17 +1297,42 @@ pub async fn decide_alignment_phrase(
{
None => None,
Some(key) => {
let id = utopia_store::ontology::relation_type_views(&state.pool, kb_id)
let found = utopia_store::ontology::relation_type_views(&state.pool, kb_id)
.await?
.into_iter()
.find(|p| p.key == key)
.map(|p| p.id)
.map(|p| (p.id, p.temporal == "state"))
.ok_or_else(|| {
utopia_core::AppError::invalid("unknown_property", "no property with that key")
})?;
Some(id)
Some(found)
}
};
let marks = match (
property,
req.marks
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty()),
) {
(_, None) => None,
(Some((_, true)), Some(m @ ("start" | "end" | "none"))) => Some(m),
(Some((_, true)), Some(_)) => {
return Err(utopia_core::AppError::invalid(
"unknown_marks",
"marks is one of start, end or none",
)
.into())
}
(_, Some(_)) => {
return Err(utopia_core::AppError::invalid(
"marks_needs_state",
"only a state property says which end a moment marks",
)
.into())
}
};
let property = property.map(|(id, _)| id);
let direction = match (property, req.direction.as_deref()) {
(None, _) => None,
(Some(_), Some(d @ ("forward" | "reverse"))) => Some(d),
Expand All @@ -1315,7 +1344,8 @@ pub async fn decide_alignment_phrase(
.into())
}
};
let votes = json!({ "person": { "property": req.property, "direction": direction } });
let votes = json!({ "person": { "property": req.property, "direction": direction,
"marks": marks } });
// 判定和它的重算任务一次提交(0051)。这里**不再**同步重算:等物化锁占的是池里的
// 连接,而正在跑的那次对齐可能已经读完最后一遍,谁也不替这条判定投影。一个 job
// 只在判定提交后可见,worker 读的是当前绑定;屏幕上等的是 `review` / `graph` 事件
Expand All @@ -1330,6 +1360,8 @@ pub async fn decide_alignment_phrase(
votes: &votes,
decided_by: "person",
basis: None,
marks,
marks_asked: false,
},
)
.await?
Expand All @@ -1342,7 +1374,7 @@ pub async fn decide_alignment_phrase(
"phrase_binding",
Some(binding_id),
json!({ "phrase": sig.phrase, "property": req.property, "direction": direction,
"job_id": job_id }),
"marks": marks, "job_id": job_id }),
)
.await;
state.emit_review(kb_id);
Expand Down
95 changes: 95 additions & 0 deletions crates/utopia-server/src/api/review_routes_phrase_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ impl Fx {
votes: &json!({}),
decided_by: "agent",
basis: None,
marks: None,
marks_asked: false,
},
)
.await?;
Expand Down Expand Up @@ -246,6 +248,99 @@ async fn a_phrase_decision_is_accepted_with_its_job() -> anyhow::Result<()> {
run
}

/// 状态属性下,人定签名时说只带一个日期的陈述那个日期标什么(#966)。不说也收下,签名
/// 留在对齐队列里等人补,属性与方向是现在绑的;说了就写下、出队。认不出的值,或给了不是
/// 状态的属性(或没有属性),答 422,什么都不写
#[tokio::test]
async fn a_phrase_decision_under_a_state_says_what_a_single_date_marks() -> anyhow::Result<()> {
let Some(f) = Fx::new().await? else {
return Ok(());
};
let run = async {
let path = format!(
"/api/v1/kbs/{}/review/alignment/phrases/{}",
f.kb, f.binding
);
let queue = format!(
"/api/v1/kbs/{}/review?queue=alignment&limit=10&offset=0",
f.kb
);
let (status, body) = f
.call(
&f.editor,
"POST",
&path,
Some(json!({ "property": "based_in", "direction": "forward" })),
)
.await?;
assert_eq!(status, StatusCode::ACCEPTED, "{body}");
let (status, body) = f.call(&f.editor, "GET", &queue, None).await?;
assert_eq!(status, StatusCode::OK, "{body}");
assert_eq!(body["counts"]["alignment"], 1, "{body}");
assert_eq!(body["items"][0]["bound_to"], "based_in", "{body}");
assert_eq!(body["items"][0]["direction"], "forward", "{body}");

let (status, body) = f
.call(
&f.editor,
"POST",
&path,
Some(json!({ "property": "based_in", "direction": "forward", "marks": "start" })),
)
.await?;
assert_eq!(status, StatusCode::ACCEPTED, "{body}");
let b = phrase_bindings::bindings(&f.pool, f.kb).await?.remove(0);
assert_eq!(
(b.decided_by.as_str(), b.marks.as_deref()),
("person", Some("start"))
);
let votes: Value = sqlx::query_scalar("SELECT votes FROM phrase_bindings WHERE id=$1")
.bind(f.binding)
.fetch_one(&f.pool)
.await?;
assert_eq!(votes["person"]["marks"], "start", "{votes}");
let (_, body) = f.call(&f.editor, "GET", &queue, None).await?;
assert_eq!(body["counts"]["alignment"], 0, "{body}");

sqlx::query(
"INSERT INTO relation_types(id,kb_id,key,label,temporal)
VALUES ($1,$2,'acquired','acquired','event')",
)
.bind(Uuid::now_v7())
.bind(f.kb)
.execute(&f.pool)
.await?;
for (request, code) in [
(
json!({ "property": "based_in", "direction": "forward", "marks": "sometimes" }),
"unknown_marks",
),
(
json!({ "property": "acquired", "direction": "forward", "marks": "start" }),
"marks_needs_state",
),
(
json!({ "property": null, "marks": "start" }),
"marks_needs_state",
),
] {
let (status, body) = f.call(&f.editor, "POST", &path, Some(request)).await?;
assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY, "{body}");
assert_eq!(body["code"], code, "{body}");
}
let b = phrase_bindings::bindings(&f.pool, f.kb).await?.remove(0);
assert_eq!(
(b.relation_type_id.is_some(), b.marks.as_deref()),
(true, Some("start")),
"a refused request writes nothing"
);
anyhow::Ok(())
}
.await;
f.cleanup().await?;
run
}

#[tokio::test]
async fn a_rule_decision_is_accepted_with_its_job() -> anyhow::Result<()> {
let Some(f) = Fx::new().await? else {
Expand Down
1 change: 1 addition & 0 deletions crates/utopia-server/src/implication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pub async fn propose_rules(
label: &p.label,
description: &p.description,
kind: &p.kind,
temporal: &p.temporal,
domains: keys_of(&p.domains),
ranges: keys_of(&p.ranges),
via: Vec::new(),
Expand Down
2 changes: 2 additions & 0 deletions crates/utopia-server/src/ontology_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,8 @@ async fn adopt_map_to(
votes: &votes,
decided_by: "person",
basis: None,
marks: None,
marks_asked: false,
},
)
.await?;
Expand Down
2 changes: 2 additions & 0 deletions crates/utopia-server/src/ontology_agent_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ impl Fx {
votes: &json!({ "reason": "no_candidates" }),
decided_by: "agent",
basis: Some("test"),
marks: None,
marks_asked: false,
},
)
.await?;
Expand Down
Loading