File indexing completed on 2026-07-21 08:46:25
0001 """AI subsystem data models.
0002
0003 The AI app owns the human-in-the-loop automation machinery
0004 (AI_PROPOSALS.md): proposals and their decisions. Executors — the
0005 services that actually mutate domain state — stay in their domain apps
0006 (e.g. ``pcs.services.dataset_propagation_set``); this app proposes,
0007 reviews, and dispatches to them.
0008 """
0009 from django.db import models
0010
0011
0012
0013
0014
0015 ACTION_REF_PREFIXES = {
0016 'propagation': 'cp',
0017 }
0018
0019
0020 class Proposal(models.Model):
0021 """AI (or rule-based) proposal of a concrete action, pending or decided —
0022 the canonical record behind the AI proposal list (AI_PROPOSALS.md).
0023
0024 A proposal is a frozen executable payload: the action identifier plus
0025 the exact validated arguments of the call it wants. Terminal rows are
0026 retained — the AI proposal list is where AI-proposed activity and its
0027 human decisions remain visible and queryable (per-proposer track
0028 records, approval and wrong-rates). Records the proposal targets carry a
0029 render projection in their metadata, written and cleared by the same
0030 services that write these rows; this table is the truth.
0031 """
0032 EXECUTOR_CHOICES = [('service', 'service'), ('agent_message', 'agent_message')]
0033
0034
0035
0036
0037 STATUS_CHOICES = [
0038 ('proposed', 'proposed'), ('approved_pending_execution',
0039 'approved_pending_execution'), ('executed', 'executed'),
0040 ('denied', 'denied'), ('withdrawn', 'withdrawn'), ('stale', 'stale'),
0041 ]
0042
0043 QUALITY_CHOICES = [('wrong', 'wrong'), ('poor', 'poor'), ('ok', 'ok'),
0044 ('good', 'good')]
0045
0046 action = models.CharField(max_length=40)
0047 subject_type = models.CharField(max_length=40)
0048 subject_key = models.CharField(max_length=255, db_index=True)
0049
0050
0051 counterpart_key = models.CharField(max_length=255, blank=True, default='')
0052 payload = models.JSONField(default=dict)
0053 comment = models.TextField()
0054 confidence = models.CharField(max_length=16, blank=True, default='')
0055 proposer = models.CharField(max_length=100)
0056 scan_version = models.IntegerField(default=1)
0057 batch_id = models.CharField(max_length=100, blank=True, default='',
0058 db_index=True)
0059 executor = models.CharField(max_length=20, choices=EXECUTOR_CHOICES,
0060 default='service')
0061
0062 precondition = models.JSONField(default=dict)
0063 input_hash = models.CharField(max_length=40, db_index=True)
0064 status = models.CharField(max_length=30, choices=STATUS_CHOICES,
0065 default='proposed', db_index=True)
0066
0067
0068 quality = models.CharField(max_length=10, choices=QUALITY_CHOICES,
0069 blank=True, default='')
0070 created_by = models.CharField(max_length=100)
0071 created_at = models.DateTimeField(auto_now_add=True)
0072 decided_by = models.CharField(max_length=100, blank=True, default='')
0073 decided_at = models.DateTimeField(null=True, blank=True)
0074
0075 executed_log_id = models.BigIntegerField(null=True, blank=True)
0076
0077
0078 undone_by = models.CharField(max_length=100, blank=True, default='')
0079 undone_at = models.DateTimeField(null=True, blank=True)
0080 undone_log_id = models.BigIntegerField(null=True, blank=True)
0081
0082 class Meta:
0083 db_table = 'ai_proposal'
0084 ordering = ['-created_at']
0085 indexes = [
0086 models.Index(fields=['action', 'subject_key', 'status']),
0087 ]
0088
0089 @property
0090 def ref(self):
0091 """The short reference humans use across surfaces, e.g. 'cp-123':
0092 category prefix + row id. The prefix doubles as corroboration — a
0093 decide call whose prefix mismatches the row's category is refused."""
0094 return f"{ACTION_REF_PREFIXES.get(self.action, 'ap')}-{self.pk}"
0095
0096 def __str__(self):
0097 return f'{self.action} {self.subject_key} [{self.status}]'