Skip to content

feat(s-escalate2): a second (final) escalation tier → Top Management#300

Merged
CoJoA13 merged 9 commits into
mainfrom
feat/s-escalate2
Jun 26, 2026
Merged

feat(s-escalate2): a second (final) escalation tier → Top Management#300
CoJoA13 merged 9 commits into
mainfrom
feat/s-escalate2

Conversation

@CoJoA13

@CoJoA13 CoJoA13 commented Jun 26, 2026

Copy link
Copy Markdown
Owner

S-escalate2 — a second (final) escalation tier

When an escalation-enabled task stays unactioned to +3 business days past due, fire a distinct task.escalated_final notification to the Top Management role (→ QMS Owner floor), reusing the existing task-timer sweep. BE-only; migration 0069. Closes the named escalate_2 residual.

Changes

  • Migration 0069 — adds sla_policy.escalate_2_after (Interval) + task.escalated_2_at (timestamptz), seeds a global task.escalated_final template, and sets escalate_2_after = INTERVAL '3 days' for escalate-enabled policies (WHERE escalate_1_after IS NOT NULL carve-out keeps DOC_ACK / PERIODIC_REVIEW tier-2-free). RESTRICT-FK-guarded downgrade; ORM mirrors.
  • timer.pyTimerStep.ESCALATE_2 + a due_steps AFTER-direction branch (reuses business_threshold, now_is_working-gated, chronologically last).
  • escalation.py — a 5th policy-aware _due_task_ids claim disjunct (symmetric with due_steps); resolve_escalation_2_recipients (Top Management → QMS Owner floor); a tier-2 dispatch branch emitting task.escalated_final + a TASK_ESCALATED audit carrying a tier discriminator (tier:1/tier:2).
  • constants.py / classes.pytask.escalated_final event key + variable whitelist + CRITICAL class (pierces quiet hours).

Design notes

  • Distinct event key task.escalated_final (not task.escalated) — the (recipient, task_id, event_key) dedup would otherwise collapse a 2nd escalation onto tier-1 (the S-remind2 lesson).
  • Tier-2 recipient = the seeded "Top Management" role (migration 0038) — no grand-manager walk, since app_user.manager_id is unpopulated in practice; QMS Owner is the always-delivers floor.
  • Claim ⇄ due_steps symmetry preserved — the claim is a strict superset of the firing condition (can only DELAY, never DROP a real tier-2) and configured-offset-gated (no S-claim-filter re-claim churn). The carve-out keeps escalate_2_after = NULL for DOC_ACK / PERIODIC_REVIEW.

Verification

  • ruff · ruff-format · mypy-strict (412) · pytest -m unit 1071 · web 1292 (unchanged — BE-only).
  • Alembic up↔down↔check clean on a throwaway PG16. Seed live-smoke confirmed the carve-out: escalate_2_after = 3 days for the 10 escalate-enabled types, NULL for DOC_ACK / PERIODIC_REVIEW; guarded downgrade drops the column + template cleanly.
  • Integration: timer-sweep 29/29 + the new tier-2 wiring / QM-fallback tests; the cross-file QM-fallback isolation proven (test_capatest_escalate_2_qm_fallback).
  • Final whole-branch review (opus + diff-critic) CONVERGED: all production invariants clean; one cross-shard test-isolation defect in the QM-fallback test fixed and empirically proven.

Tests: api unit +8 (6 timer + 2 classes), +3 integration (tier-2 wiring, QM fallback, claim positive-control). Migration head 0069.

🤖 Generated with Claude Code

CoJoA13 and others added 8 commits June 26, 2026 13:27
Tier-2 escalate_2 at +3 business days past due → Top Management role
(→ QMS Owner floor), reusing the task-timer machinery. BE-only,
migration 0069 (escalate_2_after / escalated_2_at + global
task.escalated_final template seed). Owner-ratified policy: recipient
+ timing; DOC_ACK/PERIODIC_REVIEW carve-out preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6 tasks: migration 0069 + ORM mirrors → timer.py ESCALATE_2 step →
event wiring (constants/classes) → claim disjunct → escalation.py
tier-2 dispatch (Top Management → QMS floor) → verification + live-smoke.
TDD steps with copy-pasteable code; blast-radius + cross-test-leak
handled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….escalated_final + tier audit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-2 QM-fallback test

test_escalate_2_qm_fallback asserted via=="qm_fallback" but required zero Top Management
role-holders in the shared default org — a shard-composition-dependent precondition that other
integration files (test_capa / *_authorization) violate by leaking un-cleaned SYSTEM-scoped
assignments. Fixed by clearing any leaked Top Management RoleAssignment rows at test-top before
seeding the scenario, self-providing the precondition per engineering-patterns.

Also adds a clarifying comment to TimerPolicy.escalate_2_after explaining the = None default
is deliberate (existing constructors stay valid; production sites wire by keyword).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5759ce3c8c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/api/src/easysynq_api/services/notifications/escalation.py Outdated
Comment thread apps/api/src/easysynq_api/services/notifications/escalation.py Outdated
… valid recipient

Codex P1: resolve_escalation_2_recipients returned non-empty-but-all-invalid
Top Management ids, so the dispatch loop dropped them (attempted==0), stamped
escalated_2_at as a terminal no-op, and silently never escalated even with an
active QMS Owner. Fall through to the QM floor on "no VALID Top Management
recipient" (mirrors the tier-1 manager→QM fallthrough; honors the spec's
"always delivers" intent). + a mutation-distinguishing integration test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@CoJoA13 CoJoA13 merged commit 374c9ef into main Jun 26, 2026
11 checks passed
@CoJoA13 CoJoA13 deleted the feat/s-escalate2 branch June 26, 2026 23:05
CoJoA13 added a commit that referenced this pull request Jun 26, 2026
- slice-history.md: full per-slice narrative under NOTIFICATIONS
  (newest-first) + migration head → 0069.
- CLAUDE.md: Recent-learnings bullet (oldest S-notify-5b demoted to the
  trimmed index to hold the ~8 cap) + Current-status escalate_2 pointer
  + head → 0069 (next 0070).
- dev-workflow.md: the "full -m integration isn't a clean local gate on
  this box" note (pg_dump host-missing + single-DB Organization/mirror
  pollution → use scoped runs + CI shards).

No code change.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant