1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//! A root comment and flat replies sharing one immutable original anchor and current resolution state.
use sea_orm::entity::prelude::*;
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "threads")]
/// A root comment and flat replies sharing one immutable original anchor and current resolution state.
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
/// Internal UUID primary key.
pub id: uuid::Uuid,
/// UUID of the conversation that owns this record.
pub context_id: uuid::Uuid,
/// UUID of the session that created this record; absent for externally imported records.
pub origin_session_id: Option<uuid::Uuid>,
/// UUID of the logical human or agent author, independent of the posting account.
pub author_id: uuid::Uuid,
/// Original anchor shape: general, file or line. Anchors are immutable after insertion.
pub anchor_kind: String,
/// Original repository-relative anchor path; absent for general comments.
pub file_path: Option<String>,
/// Original diff side, old or new, for a line anchor.
pub side: Option<String>,
/// Original inclusive starting line, using one-based indexing.
pub start_line: Option<i64>,
/// Original inclusive ending line, using one-based indexing.
pub end_line: Option<i64>,
/// Current lifecycle discriminator for this record.
pub state: String,
/// UUID of the last local resolver; absent when open or resolution was observed remotely.
pub resolved_by_id: Option<uuid::Uuid>,
/// UTC time of the last known local resolution.
pub resolved_at: Option<String>,
/// UUID of the identity responsible for the last local thread state change, if known.
pub updated_by_id: Option<uuid::Uuid>,
/// UTC time of the latest content or state update.
pub updated_at: String,
/// Opaque remote discussion/thread ID, scoped by context; NULL until linked.
pub forge_thread_id: Option<String>,
/// Immutable typed source identity and bounded relocation evidence; absent for provider-only coordinates.
pub source_evidence: Option<Json>,
/// Disposable provider location observation; rebuilt from GitHub during refresh.
pub forge_observation: Option<Json>,
/// Confirmed repository-relative path or absolute saved-file association. The original file_path never changes.
pub current_path: Option<String>,
/// Immutable absolute saved-file location for local notes. Git and forge anchors remain repository-relative.
pub source_path: Option<String>,
/// Confirmed file deletion or changed ephemeral working source. Absence is not proof of a valid line.
pub orphan_reason: Option<crate::backend::database::entities::common::OrphanReason>,
#[sea_orm(belongs_to, relation_enum = "Author", from = "author_id", to = "id")]
/// Loaded relationship; not an additional stored column.
pub author: BelongsTo<super::identities::Entity>,
#[sea_orm(
belongs_to,
relation_enum = "ResolvedBy",
from = "resolved_by_id",
to = "id"
)]
/// Loaded relationship; not an additional stored column.
pub resolved_by: BelongsTo<Option<super::identities::Entity>>,
#[sea_orm(
belongs_to,
relation_enum = "UpdatedBy",
from = "updated_by_id",
to = "id"
)]
/// Loaded relationship; not an additional stored column.
pub updated_by: BelongsTo<Option<super::identities::Entity>>,
#[sea_orm(belongs_to, relation_enum = "Context", from = "context_id", to = "id")]
/// Loaded relationship; not an additional stored column.
pub context: BelongsTo<super::review_contexts::Entity>,
#[sea_orm(
belongs_to,
relation_enum = "OriginSession",
from = "origin_session_id",
to = "id"
)]
/// Loaded relationship; not an additional stored column.
pub origin_session: BelongsTo<Option<super::sessions::Entity>>,
}
impl Model {
/// Validate immutable evidence at insertion. Its fields are stored and replaced as one value.
pub(crate) fn validate_source(&self) -> crate::backend::database::error::Result<()> {
use crate::backend::database::error::invalid;
let Some(evidence) = &self.source_evidence else {
return Ok(());
};
let source = &evidence["source"];
let hash = source["content_hash"].as_str().unwrap_or_default();
let kind = source["kind"].as_str().unwrap_or_default();
let object = |field: &str| {
source[field].as_str().is_some_and(|value| {
[40, 64].contains(&value.len())
&& value.bytes().all(|byte| byte.is_ascii_hexdigit())
})
};
let valid_source = match kind {
"commit" => object("sha"),
"index_file" => object("blob_oid"),
"working_file" | "local_file" => true,
_ => false,
};
if !["file", "line"].contains(&self.anchor_kind.as_str())
|| !valid_source
|| !hash.strip_prefix("sha256:").is_some_and(|value| {
value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
})
|| ["selected_text", "context_before", "context_after"]
.iter()
.any(|field| !evidence[field].is_string())
|| self.source_path.is_some() && kind != "local_file"
|| self.anchor_kind == "file"
&& ["selected_text", "context_before", "context_after"]
.iter()
.any(|field| evidence[field] != "")
{
return Err(invalid("Incomplete or incompatible anchor evidence"));
}
Ok(())
}
pub(crate) fn validate_observation(&self) -> crate::backend::database::error::Result<()> {
use crate::backend::database::error::invalid;
let Some(observation) = &self.forge_observation else {
return Ok(());
};
if ["base", "head"]
.iter()
.any(|field| observation[field].as_str().is_none_or(str::is_empty))
|| !observation["outdated"].is_boolean()
{
return Err(invalid("Incomplete forge observation"));
}
let current = &observation["current"];
if current.is_null() {
return Ok(());
}
if current["path"].as_str().is_none_or(str::is_empty)
|| match current["kind"].as_str() {
Some("file") => false,
Some("line") => {
current["start"].as_i64().is_none_or(|start| start < 1)
|| current["end"]
.as_i64()
.is_none_or(|end| end < current["start"].as_i64().unwrap_or(1))
|| ![Some("old"), Some("new")].contains(¤t["side"].as_str())
}
_ => true,
}
{
return Err(invalid("Incomplete forge location"));
}
Ok(())
}
}
impl ActiveModelBehavior for ActiveModel {}