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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
//! Composition primitives — `Auditable` and `SoftDeletable`.
//!
//! These are the runtime trait surfaces a model picks up when adopters
//! opt in via `#[model(auditable)]` (Phase 8 §T2.4 — supersedes T2.2's
//! `#[derive(Auditable)]` per spec line 1037, locked 2026-05-03) or
//! `#[model(soft_deletable)]` (Phase 8 §T2.6 — supersedes T2.3's
//! `#[derive(SoftDeletable)]` for symmetry with the auditable surface
//! and to de-risk 8γ T6's automatic default-filter composition).
//! Phase 8 §T2.1 landed the trait shapes only; T2.4 / T2.6 ship the
//! macro emissions and are the source of truth for behavior.
//! Downstream code that only needs to *bound a generic* on "models with
//! audit fields" or "models with soft-delete semantics" can import
//! these traits today.
//!
//! # Why two traits, no methods beyond the field accessors?
//!
//! Phase 8 §D6 (lines 149–157 of the v3 plan) settles the audit-field
//! shape: `created_by: Option<String>` populated from
//! `AuthContext.user_id` at create time when `ctx.auth().is_some()`, and
//! left as `None` otherwise (no warn-on-null). The trait exposes the
//! getter as `Option<&str>` so callers do not pay a `String` clone to
//! observe the audit user.
//!
//! `SoftDeletable` mirrors the same pattern for `deleted_at:
//! Option<DateTime>`. Models implementing the trait acquire a default
//! filter that excludes rows where `deleted_at IS NOT NULL`; adopter-side
//! bypass goes through the `_insecurely()` audit-warning shape — same
//! Phase 5 `set_tenant` precedent already in [`crate::DjogiContext`].
//! That filter and bypass live in the Phase 8 query layer (T2.4 / T2.5);
//! this module is intentionally bound surface only.
//!
//! # Sealing model — convention-sealed, not compile-enforced
//!
//! Phase 8 v3 spec line 758 directs T2.1 to "convention-seal per
//! `decisions.md` row 78 pattern". The intent — per the explicit
//! [CHECK] callout on v3 line 809: "convention-sealed = doc comment
//! plus trait visibility, no compile-enforced seal" — is doc-only seal.
//! No `private::Sealed` supertrait, no `__seal::Sealed` re-export. The
//! reasons:
//!
//! 1. The traits are *user-implementable in shape* — adopter macros
//! (`#[model(auditable)]` T2.4 / `#[model(soft_deletable)]` T2.6)
//! emit `impl Auditable for UserModel` / `impl SoftDeletable for
//! UserModel` directly. If we sealed them via a supertrait, the
//! macro emission would need to route through
//! `::djogi::__private::compose::Sealed` (the [`crate::hooks`]
//! precedent). T2.1 explicitly defers macro work, and threading a
//! seal across two follow-up commits adds churn for no protection
//! benefit at this stage.
//! 2. The framework's harder seals (`Model` via [`crate::model::__sealed`],
//! `HasHooks` via [`crate::hooks`], `App` via the apps-seal token,
//! `PrimaryKey` via `PkSealToken`) defend an SQL-injection or
//! correctness boundary — a hand-rolled `impl Model` could smuggle
//! `table_name()` strings into the emitter. `Auditable` and
//! `SoftDeletable` carry no such boundary: the only methods are
//! field getters that return `Option<&str>` and `Option<DateTime>`.
//! A hostile hand-rolled impl can lie about its audit user, but
//! every other read of the column already routes through the same
//! `FromPgRow` decode the macros emit, so the lie never leaves the
//! in-memory copy.
//! 3. `decisions.md` row "Apps seal enforcement" already documents that
//! "True hard-sealing of a proc-macro-emitted trait is not achievable
//! in stable Rust" — every public path the macro emission needs is
//! downstream-reachable too. The supertrait approach buys a cosmetic
//! barrier, not a real one. We document the convention here and move
//! on.
//!
//! If a future phase decides `Auditable` / `SoftDeletable` need
//! compile-enforced seals (e.g. because a security review surfaces a
//! threat the field-getter shape cannot mitigate), the upgrade path is
//! straightforward: add a `pub(crate) mod private { pub trait Sealed
//! {} }` supertrait, re-export through `crate::__private::compose` for
//! macro emission, and follow the [`crate::hooks::HasHooks`] precedent
//! at `djogi/src/hooks.rs:171`. Until then, the convention seal is
//! load-bearing through doc comments alone.
//!
//! # Phase / spec anchors
//!
//! - Phase 8 v3 §T2 line 221 — "`djogi/src/compose.rs` — runtime helpers
//! `Auditable` / `SoftDeletable` traits (sealed; convention-sealed per
//! `decisions.md` row 78 pattern)."
//! - Phase 8 v3 §D6 lines 149–157 — `created_by` nullable, AuthContext-
//! driven population, no warn on null.
//! - `feedback_macro_path_routing.md` — runtime trait module routes
//! directly through `crate::types::DateTime`, **not** via
//! `crate::__private::time` (the `__private` re-export exists for
//! macro-emitted code, not for hand-written framework modules).
use crateModel;
use crateDateTime;
/// Marker trait emitted by `#[model(auditable)]` (Phase 8 §T2.4 —
/// supersedes T2.2's `#[derive(Auditable)]` per spec line 1037).
///
/// A model carrying this bound declares `created_by: Option<String>`
/// itself (Path B per Phase 8 v3 line 866) and the
/// `#[model(auditable)]` attribute emits the trait impl plus an
/// inherent `__djogi_auditable_populate` helper invoked from
/// [`Model::create`](crate::model::Model::create) before the user
/// `before_create` hook. When [`ctx.auth()`](crate::context::DjogiContext::auth)
/// is `Some`, the helper captures `format!("{}", auth.user_id)`
/// (Display, not Debug — Debug shape is unstable per spec line 1064)
/// into the field unless the user already set a value; otherwise the
/// field stays `None`. No warn-on-null per Phase 8 §D6.
///
/// The single accessor returns a borrowed `&str` to keep audit reads
/// allocation-free in hot paths (request-side rendering, audit-log
/// emission).
///
/// # Example bound
///
/// ```ignore
/// fn render_audit_line<M: djogi::Auditable>(m: &M) -> String {
/// match m.created_by() {
/// Some(user) => format!("created by {user}"),
/// None => "created by system".to_string(),
/// }
/// }
/// ```
/// Marker trait emitted by `#[model(soft_deletable)]` (Phase 8 §T2.6 —
/// supersedes T2.3's `#[derive(SoftDeletable)]` for the same
/// proc-macros-cannot-observe-sibling-derives constraint that drove
/// the T2.4 Auditable pivot).
///
/// A model carrying this bound declares `deleted_at: Option<DateTime>`
/// itself (Path B per Phase 8 v3 line 866) and the
/// `#[model(soft_deletable)]` attribute emits the trait impl. Phase 8γ
/// T6 will land automatic default-filter composition once the `Q<T>`
/// substrate is in place (spec line 971, RESOLVED 2026-05-03, lens,
/// locked); T2.6 ships the trait impl plus the manual
/// [`QuerySet::not_deleted()`](crate::query::QuerySet::not_deleted)
/// helper that reads the column name through `<M as
/// SoftDeletable>::COLUMN` rather than a hard-coded string.
///
/// This trait is the bound surface used by code that needs to talk
/// generically about "models with soft-delete semantics" — for example,
/// the Phase 8 visage layer's "include trashed rows" toggle.
///
/// # Example bound
///
/// ```ignore
/// fn purge_window<M: djogi::SoftDeletable>(m: &M) -> Option<i64> {
/// m.deleted_at()
/// .map(|dt| (djogi::DateTime::now_utc() - dt).whole_seconds())
/// }
/// ```
// `Model`'s CRUD methods return `impl Future + Send` rather than using
// `async fn` syntax (pinned to `Send` explicitly). The inert stub below
// mirrors that trait shape, which trips `clippy::manual_async_fn` under
// Rust 1.93+. Mirror the allow used by `crate::query::field::tests`
// for the same reason.