Phase 2: core domain, policy engine, packet API, Clerical screen

Migration 0003: projects, aliases, renumbers, units, packets and every child table,
change orders, assets, documents, ITPs, station limits, department weld flag.
Policy package: Principal, PacketState, Action rules, custody, preconditions,
CO hold, order of checks, can/blocked/holder on reads. docs/POLICY.md with the
tightenings table for Seth. Write pipeline: FOR UPDATE, CAS on status and dept,
If-Match row versions, server-assigned numbers, field CAS, version bump, packet
log, audit, NOTIFY, signatures, IT reasons, filed lock and HMAC seal.
API: /api/packets (header, transitions, work orders, pages, docs, move, remove,
BOM, shorts, welds, heat tags, heat table, stage, checklist, clock, warehouse
stage, Q.C., change orders, log, seal), /api/projects, /api/units, /api/assets,
/_blob. Frontend: Clerical home, projects and units, packet page with pdf.js
and QR codes, hand draft, heat map, send to warehouse, department or Q.C.
Tests: policy matrix, API, concurrency, schema, projects; Playwright Clerical
draft scenario. Verified on docker-1 with the CI stack and the Samba DC.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-10-16 09:31:45 -06:00
co-authored by Claude Fable 5.1
parent 97b0973130
commit 37d9a0cb50
83 changed files with 16490 additions and 55 deletions
+99
View File
@@ -0,0 +1,99 @@
"""The policy's view of a person (Principal) and of a packet (PacketState)."""
from __future__ import annotations
from collections.abc import Iterable, Mapping
from dataclasses import dataclass, field
from types import MappingProxyType
from ..auth.groups import SHOP_LEVELS, parse_role
SUPPORT = ("CL", "WH", "QC")
LEVELS: dict[str, int] = {level: i + 1 for i, level in enumerate(SHOP_LEVELS)} # worker 1, lead 2, supervisor 3
@dataclass(frozen=True)
class Principal:
user_id: str | None # uuid as text; None for system
admin: bool = False
support: frozenset[str] = frozenset() # codes among CL, WH, QC the user is a member of (any level)
support_sup: frozenset[str] = frozenset() # codes among CL, WH, QC where the user is Supervisor
shop: Mapping[str, int] = field(default_factory=lambda: MappingProxyType({})) # code -> 1 worker, 2 lead, 3 sup
station_limits: frozenset[int] = frozenset() # station ids a worker is limited to; empty = any
pm: bool = False
session_kind: str = "password" # password | pin
system: str | None = None # "visual-feed" for the worker; never from a session
def level(self, dept: str) -> int:
"""Shop level in a department: 0 none, 1 worker, 2 lead, 3 supervisor."""
return int(self.shop.get(dept, 0))
@dataclass(frozen=True)
class PacketState:
id: int
status: str # packet_status value
dept: str # dept_code
station_id: int | None
welder_id: str | None # uuid text of the assigned welder
wh_skipped: bool
routed_to: str | None # databook | shipping | None
vclosed: bool
removed: bool
source_kind: str # visual | hand
weld_dept: bool # departments.weld
co_hold: bool # an approved CO with pending verifications, or a pending CO with hold=true
co_hold_why: str = "" # text for the error
open_shorts: int = 0
drawing_attached: bool = False
heat_map: bool | None = None
bom_total: int = 0 # lines not marked n/a
bom_ready: int = 0 # lines pulled, or with an open short
welds: int = 0
checklist_complete: bool = False # every key of the department checklist is done (empty list is complete)
filler_set: bool = False
qc_missing: tuple[str, ...] = () # the old qcMissing() list, computed by the read model
has_qc_sent: bool = False # qc_inspections row exists for the current stage
SYSTEM_FEED = Principal(user_id=None, system="visual-feed")
def principal_from_roles(
user_id: str | None,
roles: Iterable[str],
station_limits: Iterable[int] = (),
session_kind: str = "password",
) -> Principal:
"""Build the policy Principal from phase 1 role strings. A role that does not parse is ignored."""
admin = False
pm = False
support: set[str] = set()
support_sup: set[str] = set()
shop: dict[str, int] = {}
for text in roles:
try:
role = parse_role(text)
except ValueError:
continue
if role.kind == "admin":
admin = True
elif role.kind == "pm":
pm = True
elif role.kind.startswith("shop_") and role.dept_code:
level = LEVELS[role.kind.removeprefix("shop_")]
shop[role.dept_code] = max(level, shop.get(role.dept_code, 0))
elif role.dept_code in SUPPORT:
support.add(role.dept_code)
if role.kind.endswith("_supervisor"):
support_sup.add(role.dept_code)
return Principal(
user_id=user_id,
admin=admin,
support=frozenset(support),
support_sup=frozenset(support_sup),
shop=MappingProxyType(shop),
station_limits=frozenset(int(s) for s in station_limits),
pm=pm,
session_kind=session_kind,
)