merge: testing → main (reconcile 2-week divergence)
This commit is contained in:
150
decnet/realism/taxonomy.py
Normal file
150
decnet/realism/taxonomy.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""Content classes and the :class:`Plan` dataclass.
|
||||
|
||||
The planner emits :class:`Plan` instances; drivers consume them. Every
|
||||
planted artifact (inert noise file, email, callback-bearing canary)
|
||||
maps to exactly one :class:`ContentClass` member, which is what the
|
||||
realism engine uses to dispatch to the right namer / body generator /
|
||||
prompt template.
|
||||
|
||||
Categories:
|
||||
|
||||
* **User content** (LLM-eligible): ``note``, ``todo``, ``draft``,
|
||||
``script``. Created by humans on workstations; LLM enrichment makes
|
||||
them feel lived-in.
|
||||
* **System content** (deterministic only): ``log_cron``, ``log_daemon``,
|
||||
``cache_tmp``. These are *supposed* to look formulaic — that's how
|
||||
cron/journald actually write them. LLM here would harm realism.
|
||||
* **Email** (LLM-eligible): one persona writing to another. Owned by
|
||||
the email driver, not the file driver.
|
||||
* **Canary** (deterministic, callback-bearing): one ``canary_*`` member
|
||||
per :mod:`decnet.canary.factory.KNOWN_GENERATORS` entry. Picked
|
||||
rarely and rate-limited per-decky by the planner.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
from typing import Literal, Optional
|
||||
|
||||
|
||||
class ContentClass(StrEnum):
|
||||
"""The kind of artifact a planner has decided to produce.
|
||||
|
||||
Values are stable over the wire — they're persisted on
|
||||
``synthetic_files.content_class`` and used as bus-event discriminants
|
||||
so renaming a member is a schema change. Add new members at the
|
||||
bottom; never reorder.
|
||||
"""
|
||||
|
||||
# User-generated, LLM-enrichable
|
||||
NOTE = "note"
|
||||
TODO = "todo"
|
||||
DRAFT = "draft"
|
||||
SCRIPT = "script"
|
||||
|
||||
# System-generated, template-only (LLM would harm realism)
|
||||
LOG_CRON = "log_cron"
|
||||
LOG_DAEMON = "log_daemon"
|
||||
CACHE_TMP = "cache_tmp"
|
||||
|
||||
# Email — owned by the email driver, planner picks the action shape
|
||||
EMAIL = "email"
|
||||
|
||||
# Callback-bearing — provided by decnet.canary.cultivator at
|
||||
# dispatch time, not by realism.bodies. One member per generator
|
||||
# in decnet.canary.factory.KNOWN_GENERATORS.
|
||||
CANARY_AWS_CREDS = "canary_aws_creds"
|
||||
CANARY_ENV_FILE = "canary_env_file"
|
||||
CANARY_GIT_CONFIG = "canary_git_config"
|
||||
CANARY_SSH_KEY = "canary_ssh_key"
|
||||
CANARY_HONEYDOC = "canary_honeydoc"
|
||||
CANARY_HONEYDOC_DOCX = "canary_honeydoc_docx"
|
||||
CANARY_HONEYDOC_PDF = "canary_honeydoc_pdf"
|
||||
CANARY_MYSQL_DUMP = "canary_mysql_dump"
|
||||
|
||||
def is_canary(self) -> bool:
|
||||
return self.value.startswith("canary_")
|
||||
|
||||
def is_user_class(self) -> bool:
|
||||
return self in (
|
||||
ContentClass.NOTE,
|
||||
ContentClass.TODO,
|
||||
ContentClass.DRAFT,
|
||||
ContentClass.SCRIPT,
|
||||
)
|
||||
|
||||
def is_system_class(self) -> bool:
|
||||
return self in (
|
||||
ContentClass.LOG_CRON,
|
||||
ContentClass.LOG_DAEMON,
|
||||
ContentClass.CACHE_TMP,
|
||||
)
|
||||
|
||||
|
||||
PlanAction = Literal["create", "edit", "rotate"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Plan:
|
||||
"""One realism decision: what to do, where, as whom, when.
|
||||
|
||||
Frozen so the planner can return the same instance to multiple
|
||||
consumers (e.g. orchestrator dispatcher + canary cultivator) without
|
||||
them stomping each other's view of the schedule.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
decky_uuid, decky_name :
|
||||
Target decky. Both carried so drivers don't need a repo
|
||||
round-trip to map UUID → container name.
|
||||
persona :
|
||||
Persona name (``EmailPersona.name``) — this is the user the
|
||||
action is "performed by." Sampled from the topology's persona
|
||||
pool at plan time.
|
||||
content_class :
|
||||
:class:`ContentClass` member. Drives namer/body dispatch.
|
||||
action :
|
||||
``"create"`` mints a new artifact; ``"edit"`` mutates a
|
||||
previously-planted one (read-modify-write — requires
|
||||
:attr:`previous_body`); ``"rotate"`` is the log-rotation shape
|
||||
(``cron.log`` → ``cron.log.1``).
|
||||
target_path :
|
||||
Absolute container-side path the driver should write. Already
|
||||
persona-aware (e.g. ``/home/admin/TODO.md`` not
|
||||
``/home/{user}/TODO.md``).
|
||||
mtime :
|
||||
Backdated wall-clock the driver should ``touch -d`` after
|
||||
writing. Sampled by :func:`decnet.realism.diurnal.sample_mtime`
|
||||
so files don't all stamp at the moment they were created.
|
||||
body_hint :
|
||||
Deterministic body the engine has *already* committed to. LLM
|
||||
enrichment, when enabled, may replace it but on timeout/failure
|
||||
the driver falls back to this — so the tick never blocks
|
||||
unboundedly.
|
||||
previous_body :
|
||||
Required for ``action="edit"``. The bytes the driver read back
|
||||
from the decky before mutating; passed to
|
||||
:func:`decnet.realism.bodies.next_iteration`.
|
||||
"""
|
||||
|
||||
decky_uuid: str
|
||||
decky_name: str
|
||||
persona: str
|
||||
content_class: ContentClass
|
||||
action: PlanAction
|
||||
target_path: str
|
||||
mtime: datetime
|
||||
body_hint: Optional[str] = None
|
||||
previous_body: Optional[str] = None
|
||||
notes: tuple[str, ...] = field(default_factory=tuple)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.action == "edit" and self.previous_body is None:
|
||||
# Belt-and-braces: the planner produced an edit Plan without
|
||||
# the prior body. The driver would either have to make a
|
||||
# second docker exec to re-read or silently degrade to
|
||||
# create. Both bad. Fail loudly at construction.
|
||||
raise ValueError(
|
||||
"Plan.action='edit' requires previous_body; got None"
|
||||
)
|
||||
Reference in New Issue
Block a user