CR-014 - bodies carry customer context and the link carries the content
Nick's decision, 2026-08-20: 'email bodies provide links back to the system. we can talk about customers we just cant exposed their confidential documents.' The T7.6-era rule (no customer IP at all, so number + link only) is refined: context IN, content OUT. - wp_titled() and wp_where() compose 'number - title' and the CR-004 location (structured paths first, legacy free text second); the where-line is dropped entirely when unset rather than mailing 'Where: '. - assign, qa-ready, qa-reject and hold bodies gain title + location. The scope summary the original CR asked for stays OUT - scope text is document content; the link is its summary. Rejection comments stay on the package. - hold_body gains the house footer it alone lacked. - kitting and material-request bodies adopt wp_titled for the same identity line (their delivery-location rule is unchanged). - notify.py's docstring states the new rule where the transport documents it. Pins flipped WITH the rule, reasons in code: qa_gate_check's location canary is now asserted PRESENT in QA bodies; a new DESC_CANARY (document content) is asserted absent from every message (40 -> 41 checks). The sink also gains a decoded-body view: the em-dash switches smtplib to quoted-printable, whose column-76 soft breaks made raw-payload substring pins pass or fail on luck of line position - content pins now read the decoded body, header pins still read the wire payload. Battery: qa_gate_check 41/41, kitting_notify_check 17/17, mreq_check 19/19. Items: CR-014 (rule per decisions-2026-08-20.md), CR-011 pins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -458,15 +458,41 @@ def wp_link(db: Session, wp: "models.WorkPackage") -> str:
|
||||
return (base + path) if base else path
|
||||
|
||||
|
||||
def wp_titled(wp: "models.WorkPackage") -> str:
|
||||
"""Number — title, for a message body. Decided 2026-08-20: the title is
|
||||
customer CONTEXT and may ride in mail; document CONTENT may not."""
|
||||
t = (wp.subject or "").strip()
|
||||
n = wp.number or "a work package"
|
||||
return f"{n} — {t}" if t else n
|
||||
|
||||
|
||||
def wp_where(wp: "models.WorkPackage") -> str:
|
||||
"""Where the work happens, for a message body: the CR-004 structured
|
||||
fields (stored as paths — stable, and readable to the people these mails
|
||||
address), else the pre-CR-004 free text. Empty string when unset, and
|
||||
callers drop the line entirely rather than mail 'Where: '."""
|
||||
data = wp.data or {}
|
||||
parts = [str(data.get(d) or "").strip() for d in LOCATION_DIMENSIONS]
|
||||
parts = [p for p in parts if p]
|
||||
return " / ".join(parts) if parts else str(data.get("location") or "").strip()
|
||||
|
||||
|
||||
def _where_line(wp: "models.WorkPackage") -> str:
|
||||
w = wp_where(wp)
|
||||
return f"Where: {w}\n" if w else ""
|
||||
|
||||
|
||||
def assign_body(assignee: "models.User", wp: "models.WorkPackage", actor: "models.User", link: str) -> str:
|
||||
# Deliberately minimal — a WP number + a link, NOT the package contents (keeps
|
||||
# customer IP inside the app, behind login).
|
||||
# Number, title and location — customer context, allowed since the
|
||||
# 2026-08-20 decision (decisions-2026-08-20.md). Contents stay behind
|
||||
# the link: no scope text, no descriptions, no attachments.
|
||||
who = actor.full_name or actor.username
|
||||
name = assignee.full_name or assignee.username
|
||||
return (
|
||||
f"Hi {name},\n\n"
|
||||
f"{who} assigned you a work package: {wp.number or '(no number)'}.\n\n"
|
||||
f"Open the Work Package Suite to view and action it:\n{link}\n\n"
|
||||
f"{who} assigned you a work package: {wp_titled(wp)}.\n"
|
||||
+ _where_line(wp) +
|
||||
f"\nOpen the Work Package Suite to view and action it:\n{link}\n\n"
|
||||
f"— This is an automated message from the Work Package Suite."
|
||||
)
|
||||
|
||||
@@ -1652,14 +1678,15 @@ def enforce_qa_rejection_comment(data: Optional[dict], new_status: str,
|
||||
|
||||
def qa_ready_body(user: "models.User", wp: "models.WorkPackage",
|
||||
actor: "models.User", link: str) -> str:
|
||||
# A WP number and a deep link - NOT the package contents. The task text asked
|
||||
# for location and a scope summary, but the done-when list (and the standing
|
||||
# rule) says no customer IP in a message body; the link is the summary.
|
||||
# Number, title and location ride in the body — the 2026-08-20 decision
|
||||
# restored the location the T7.6 done-when had excluded. The SCOPE summary
|
||||
# stays out: scope text is document content, and the link is its summary.
|
||||
who = actor.full_name or actor.username
|
||||
name = user.full_name or user.username
|
||||
return (
|
||||
f"Hi {name},\n\n"
|
||||
f"{who} moved {wp.number or 'a work package'} to Ready for QA.\n"
|
||||
f"{who} moved {wp_titled(wp)} to Ready for QA.\n"
|
||||
+ _where_line(wp) +
|
||||
f"It is in the QA queue waiting to be accepted or returned.\n\n"
|
||||
f"Open it here:\n{link}\n\n"
|
||||
f"— This is an automated message from the Work Package Suite."
|
||||
@@ -1672,7 +1699,8 @@ def qa_reject_body(user: "models.User", wp: "models.WorkPackage",
|
||||
name = user.full_name or user.username
|
||||
return (
|
||||
f"Hi {name},\n\n"
|
||||
f"{who} returned {wp.number or 'a work package'} from Ready for QA to In Progress.\n"
|
||||
f"{who} returned {wp_titled(wp)} from Ready for QA to In Progress.\n"
|
||||
+ _where_line(wp) +
|
||||
f"The reason is recorded on the package.\n\n"
|
||||
f"Open it here:\n{link}\n\n"
|
||||
f"— This is an automated message from the Work Package Suite."
|
||||
@@ -1705,18 +1733,20 @@ def notify_qa_transition(db: Session, wp: "models.WorkPackage",
|
||||
|
||||
def hold_body(user: "models.User", wp: "models.WorkPackage", names: list[str],
|
||||
actor: "models.User", link: str) -> str:
|
||||
# Constraint names and a WP number only — no package contents, same rule as the
|
||||
# assignment mail.
|
||||
# Constraint names, number, title and location — customer context per the
|
||||
# 2026-08-20 decision. No package contents; the link carries those.
|
||||
who = user.full_name or user.username
|
||||
by = actor.full_name or actor.username
|
||||
which = ", ".join(names)
|
||||
return (
|
||||
f"Hi {who},\n\n"
|
||||
f"A critical constraint was reopened on {wp.number or 'a work package'} "
|
||||
f"A critical constraint was reopened on {wp_titled(wp)} "
|
||||
f"after it was released to the field, so the package is on hold.\n\n"
|
||||
f"Constraint: {which}\n"
|
||||
+ _where_line(wp) +
|
||||
f"Reopened by: {by}\n\n"
|
||||
f"Open the package:\n{link}\n"
|
||||
f"Open the package:\n{link}\n\n"
|
||||
f"— This is an automated message from the Work Package Suite."
|
||||
)
|
||||
|
||||
|
||||
@@ -1733,7 +1763,7 @@ def kitting_body(user: "models.User", wp: "models.WorkPackage", actor: "models.U
|
||||
or (wp.data or {}).get("mimoLoc") or "").strip() or "not set"
|
||||
return (
|
||||
f"Hi {name},\n\n"
|
||||
f"{who} moved kitting on {wp.number or 'a work package'} from "
|
||||
f"{who} moved kitting on {wp_titled(wp)} from "
|
||||
f"{old_status or 'Not Started'} to {new_status or 'Not Started'}.\n"
|
||||
f"Delivery location: {delivery}.\n\n"
|
||||
f"Open it here:\n{link}\n\n"
|
||||
@@ -1749,7 +1779,7 @@ def material_request_body(user: "models.User", wp: "models.WorkPackage",
|
||||
needed_line = f" needed by {needed}" if needed else ""
|
||||
return (
|
||||
f"Hi {name},\n\n"
|
||||
f"{who} raised a material request on {wp.number or 'a work package'}: "
|
||||
f"{who} raised a material request on {wp_titled(wp)}: "
|
||||
f"{n_lines} line{'' if n_lines == 1 else 's'}{needed_line}.\n"
|
||||
f"Delivery location: {delivery}.\n\n"
|
||||
f"Open it here:\n{link}\n\n"
|
||||
|
||||
Reference in New Issue
Block a user