File size: 5,016 Bytes
f51c224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
<?php
defined( 'ABSPATH' ) || exit;

/**
 * Actor Handoff Log β€” two-axis attribution (Board #23).
 *
 * Provides a clear, auditable boundary between AI-operated actor mode
 * and human-operated actor mode WITHOUT mutating Fluent's own comment
 * tables. Strictly additive: a small dedicated table records who
 * "pressed send" for a given actor-authored comment.
 *
 * The two axes:
 *   created_by    β€” the actor (Bridget, etc.). The displayed author. Unchanged.
 *   committed_by  β€” the user who pressed send (a human user, or an agent
 *                   service account user). Recorded here, queried via
 *                   SA_Orch_Actor_Handoff::get_committer().
 *
 * Operator-mode detection β€” the full provenance chain works once Board #16
 * (Token + Usage Visibility) ships its LLM token log. Until then, the
 * token-presence check returns 'unknown' and detect_mode reflects that
 * honestly:
 *
 *   committer recorded + token row found        β†’ 'ai'
 *   committer recorded + token row NOT found    β†’ 'human'
 *   committer recorded + token state unknown    β†’ 'unknown'  (pre-#16; honest)
 *   committer NOT recorded                      β†’ 'unknown'
 *
 * The token-presence check is implemented as a filter:
 *
 *   apply_filters( 'sa_orch_handoff_has_llm_token', null, $kind, $comment_id );
 *
 * Board #16 will register a callback that consults the token log. The
 * default return of null means "unknown" β€” never a false positive.
 *
 * Doctrine constraints honored:
 *   - Actors remain non-binding; this layer is observational, not enforcing
 *   - No impact to Board progression rules
 *   - No schema refactors β€” strictly additive (one new table)
 *   - No Asterion writes
 *   - No automation
 *   - No UI surface beyond what already exists
 */
class SA_Orch_Actor_Handoff {

    const KIND_FLUENT_BOARDS  = 'fluent_boards';
    const KIND_FLUENT_SUPPORT = 'fluent_support';

    const MODE_AI      = 'ai';
    const MODE_HUMAN   = 'human';
    const MODE_UNKNOWN = 'unknown';

    private static function table(): string {
        global $wpdb;
        return $wpdb->prefix . SA_ORCH_DB_PREFIX . 'actor_handoff';
    }

    /**
     * Record (or update) the committer for an actor-authored comment.
     * Idempotent on (kind, comment_id) β€” re-recording replaces the prior row.
     *
     * Returns true on success, false on invalid input or DB failure.
     */
    public static function record_committer( string $kind, int $comment_id, int $committed_by_user_id ): bool {
        if ( $comment_id <= 0 || $committed_by_user_id <= 0 ) {
            return false;
        }
        $kind = sanitize_key( $kind );
        if ( $kind === '' ) return false;

        global $wpdb;
        $tbl = self::table();
        // Replace prior row, if any. Keeps the table thin.
        $wpdb->delete( $tbl, [
            'comment_kind' => $kind,
            'comment_id'   => $comment_id,
        ] );
        $ok = $wpdb->insert( $tbl, [
            'comment_kind'         => $kind,
            'comment_id'           => $comment_id,
            'committed_by_user_id' => $committed_by_user_id,
            'recorded_at'          => current_time( 'mysql', true ),
        ] );
        return $ok !== false;
    }

    /**
     * Get the recorded committer for a comment, or null if none recorded.
     */
    public static function get_committer( string $kind, int $comment_id ): ?int {
        if ( $comment_id <= 0 ) return null;
        $kind = sanitize_key( $kind );
        if ( $kind === '' ) return null;

        global $wpdb;
        $row = $wpdb->get_var( $wpdb->prepare(
            "SELECT committed_by_user_id FROM " . self::table() . "
             WHERE comment_kind = %s AND comment_id = %d
             ORDER BY id DESC LIMIT 1",
            $kind,
            $comment_id
        ) );
        return $row !== null ? (int) $row : null;
    }

    /**
     * Detect operator mode for a comment. Returns one of the MODE_* constants.
     * Never lies about uncertainty β€” returns 'unknown' rather than guessing.
     */
    public static function detect_mode( string $kind, int $comment_id ): string {
        $committer = self::get_committer( $kind, $comment_id );
        if ( $committer === null ) {
            return self::MODE_UNKNOWN;
        }

        /**
         * Filter that signals whether an LLM token row exists for this commit.
         * Returns: true (token row found β†’ AI mode),
         *          false (no token row β†’ human mode),
         *          null (unknown β€” token log not yet integrated, default).
         *
         * Board #16 will register a callback that consults the token log.
         */
        $token_present = apply_filters(
            'sa_orch_handoff_has_llm_token',
            null,
            $kind,
            $comment_id
        );

        if ( $token_present === null )  return self::MODE_UNKNOWN;
        if ( $token_present === true )  return self::MODE_AI;
        return self::MODE_HUMAN;
    }
}