refactor(forge): classify account notifications beside the vocabulary

The triage mapped the forges' notification reasons onto the claw's
verdicts, but it co-evolves with the vocabulary, not with the claw:
a reason that enters it (Gitea's collapsed shape) needs a verdict in
the same change, and its tests pin the mapping against the
vocabulary's own types. It now lives in the integration crate as the
default classification; a consumer may reclassify on top. The claw
imports it like the rest of the forge seam.
This commit is contained in:
vhaudiquet committed 2026-10-20 05:49:14 +02:00
1 parent 5fdd17b901
commit fc0cbbcb6b
5 files changed
+22 -17

No files matched your search

+2 -2
View File
@@ -6,7 +6,7 @@
//! list_account_watches`] — and loops the kind's repos through a //! list_account_watches`] — and loops the kind's repos through a
//! [`Watcher`] (poll, ingest, advance the per-repo cursor), then the //! [`Watcher`] (poll, ingest, advance the per-repo cursor), then the
//! kind's account watches through the identity store's credentials: //! kind's account watches through the identity store's credentials:
//! poll the account's notifications, [`crate::triage::classify`] each //! poll the account's notifications, [`triage::classify`] each
//! one, and either ingest it as a role-tagged thread event, surface it //! one, and either ingest it as a role-tagged thread event, surface it
//! to the account's human, or drop it. Neither pass touches the model //! to the account's human, or drop it. Neither pass touches the model
//! (spec §1). Reading the tables every pass is what makes a watch added //! (spec §1). Reading the tables every pass is what makes a watch added
@@ -23,6 +23,7 @@ use std::sync::{Arc, Mutex, PoisonError};
use rhema_autonomous_agent::{Source, SourceError, async_trait}; use rhema_autonomous_agent::{Source, SourceError, async_trait};
use rhema_forge_integration::forge_key_fn; use rhema_forge_integration::forge_key_fn;
use rhema_forge_integration::triage::{self, Triage};
use rhema_forge_integration::{ use rhema_forge_integration::{
Forge, GitHubForge, GitLabForge, GiteaForge, Notification, Watcher, register_forge, Forge, GitHubForge, GitLabForge, GiteaForge, Notification, Watcher, register_forge,
}; };
@@ -30,7 +31,6 @@ use rhema_identity::{IdentityStore, SecretKind};
use rhema_rsd::{Clock, Dispatcher, Event, KeyFn, SystemClock}; use rhema_rsd::{Clock, Dispatcher, Event, KeyFn, SystemClock};
use crate::config::{ForgeKind, resolve_token}; use crate::config::{ForgeKind, resolve_token};
use crate::triage::{self, Triage};
use crate::watch_store::{AccountWatchRecord, ClawStore, WatchRecord}; use crate::watch_store::{AccountWatchRecord, ClawStore, WatchRecord};
/// The cache key of a forge client: everything that changes the client /// The cache key of a forge client: everything that changes the client
+1 -1
View File
@@ -25,12 +25,12 @@ use std::sync::{Arc, Mutex, PoisonError};
use crate::config::ForgeKind; use crate::config::ForgeKind;
use crate::forge::{record_client, resolve_watch_token}; use crate::forge::{record_client, resolve_watch_token};
use crate::triage::Role;
use crate::watch_store::ClawStore; use crate::watch_store::ClawStore;
use rhema_agent_tools::AllowAll; use rhema_agent_tools::AllowAll;
use rhema_autonomous_agent::{Handler, SessionContext, async_trait}; use rhema_autonomous_agent::{Handler, SessionContext, async_trait};
use rhema_coding_agent::{CodingAgent, CodingAgentConfig}; use rhema_coding_agent::{CodingAgent, CodingAgentConfig};
use rhema_core::{Content, Event as ModelEvent, EventSink, Message, Session}; use rhema_core::{Content, Event as ModelEvent, EventSink, Message, Session};
use rhema_forge_integration::triage::Role;
use rhema_forge_integration::{ use rhema_forge_integration::{
EventKind, Forge, ForgeError, ForgeEvent, Notification, RepoRef, Snapshot, ThreadState, decode, EventKind, Forge, ForgeError, ForgeEvent, Notification, RepoRef, Snapshot, ThreadState, decode,
parse_key, parse_key,
-1
View File
@@ -94,7 +94,6 @@ pub mod settings;
pub mod task_source; pub mod task_source;
pub mod task_store; pub mod task_store;
pub mod task_tools; pub mod task_tools;
pub mod triage;
pub mod turn_store; pub mod turn_store;
pub mod watch_store; pub mod watch_store;
pub mod watch_tools; pub mod watch_tools;
@@ -41,6 +41,9 @@
//! - [`watcher`] — the dispatcher seam: polls a forge into an RSD //! - [`watcher`] — the dispatcher seam: polls a forge into an RSD
//! `Dispatcher` (keys, dedup, cursors) and decodes delivered inbox //! `Dispatcher` (keys, dedup, cursors) and decodes delivered inbox
//! items back into forge events //! items back into forge events
//! - [`triage`] — what an account notification earns: the default
//! classification of the notification vocabulary into agent
//! dispatches, surfaces, and drops
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny(missing_docs)] #![deny(missing_docs)]
@@ -51,6 +54,7 @@ pub mod forge;
pub mod gitea; pub mod gitea;
pub mod github; pub mod github;
pub mod gitlab; pub mod gitlab;
pub mod triage;
pub mod watcher; pub mod watcher;
// Re-exported so implementors of the async seam can apply #[async_trait] // Re-exported so implementors of the async seam can apply #[async_trait]
@@ -65,6 +69,7 @@ pub use forge::{Forge, ForgePage};
pub use gitea::GiteaForge; pub use gitea::GiteaForge;
pub use github::GitHubForge; pub use github::GitHubForge;
pub use gitlab::GitLabForge; pub use gitlab::GitLabForge;
pub use triage::{Role, Triage};
pub use watcher::{ pub use watcher::{
WatchError, Watcher, decode, forge_key_fn, parse_key, register_forge, resource_key, WatchError, Watcher, decode, forge_key_fn, parse_key, register_forge, resource_key,
}; };
@@ -1,23 +1,24 @@
//! The triage: what a notification earns, decided deterministically //! The notification triage: what a notification earns, decided
//! before any model call (spec §1 — the model is never the detector). //! deterministically before any model call.
//! //!
//! A [`Notification`] carries the forge's own reason for bothering the //! A [`Notification`] carries the forge's own reason for bothering the
//! account; [`classify`] reduces it to one of three verdicts. An //! account; [`classify`] reduces it to one of three verdicts. A
//! [`Triage::Agent`] dispatch becomes an RSD event whose payload names //! [`Triage::Agent`] dispatch becomes an RSD event whose payload names
//! the [`Role`] — the persona the handler's prompt opens with, so one //! the [`Role`] — the persona the handler's prompt opens with, so one
//! thread can move from review-requested to implement-feedback across //! thread can move from review-requested to implement-feedback across
//! dispatches with the session carrying the continuity. A //! dispatches with the session carrying the continuity. A
//! [`Triage::Surface`] never dispatches an agent: a mention is not a //! [`Triage::Surface`] never dispatches an agent: a mention is not an
//! assignment, and the account's human hears about it through the //! assignment, and the account's human hears about it some other way.
//! device-notification queue instead. [`Triage::Ignore`] drops the noise //! [`Triage::Ignore`] drops the noise (CI churn) on the floor.
//! (CI churn) on the floor.
//! //!
//! The mapping is a pure function and this module's tests pin it: the //! The mapping lives beside the vocabulary on purpose: a reason that
//! forges' vocabularies are external data, and the policy must not drift //! enters it (Gitea's collapsed shape, a new adapter's spelling) needs
//! with them. //! a verdict in the same change, and this module's tests pin the
//! mapping so the co-edit cannot drift. A consumer may reclassify on
//! top — [`classify`] is the default, not a mandate.
use rhema_forge_integration::gitea::COLLAPSED_REASON; use crate::gitea::COLLAPSED_REASON;
use rhema_forge_integration::{Notification, NotificationReason}; use crate::{Notification, NotificationReason};
/// The agent persona a dispatched notification runs as. The key stays /// The agent persona a dispatched notification runs as. The key stays
/// the thread — one session, one voice per thread, ever — and the role /// the thread — one session, one voice per thread, ever — and the role
@@ -83,7 +84,7 @@ pub fn classify(notification: &Notification) -> Triage {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use rhema_forge_integration::{RepoRef, Timestamp}; use crate::{RepoRef, Timestamp};
fn notification(reason: NotificationReason) -> Notification { fn notification(reason: NotificationReason) -> Notification {
Notification { Notification {