Wave 3: predecessor references with a release gate, and critical-constraint reopen alerts
Predecessors are real references now - data.predecessors holds work-package ids, replacing a free-text SOP phase label that couldn't express "WP04 waits on WP02" and gated nothing. The SOP phase survives beside it as the descriptive "Sequence phase" field. - readiness() has two gates: constraints clear AND every predecessor Closed. The banner, sticky bar, left rail, dashboard Gates column and the ready counters all reflect the second one. - Enforced server-side by enforce_release_gates() on every path that sets a status — the plain upsert included, since that's how the browser and the offline outbox save. /issue and /status would otherwise have been ways around it. - Cycles are refused directly and through a chain, with a message naming the package that already waits on this one. The Creator's picker also hides itself and its own descendants, so a cycle is hard to build in the first place. - A deleted predecessor does not block: it would freeze everything downstream of a package someone removed. - The gate is refusable, on purpose. Planners release ahead of upstream close-out, so an explicit reason (data.gateOverride) allows it, gets a gate_overridden audit event naming what was skipped, and prints on the package. A blank reason is not an override, and changing the predecessor set clears it. The dashboard won't release a blocked package at all — it points at the form where the reason is captured. Critical constraints reopened after release - Reopening a SOP-critical constraint on a released package emails the owner, PM, CM and the package's distribution list (minus whoever did it) and writes a constraint_reopened audit event. - Detected by diffing the incoming constraints against the stored ones inside the normal upsert rather than via a new endpoint: the sync outbox only replays POST /api/wps, so a dedicated route would be lost offline. It fires only on a real cleared→open transition, so re-saving an already-open constraint doesn't re-announce, and never before release or for a non-critical constraint. - Bodies carry the constraint name, WP number and a link — never package contents. Verified: 139 API checks on one fresh database (44 permissions + 22 password reset + 34 search/localization + 39 gates/notifications), including every bypass path, cycle shapes, the deleted-predecessor case, blank-reason overrides, and the four recipients confirmed both in the outbox and on the wire against a local SMTP sink. 27 driven UI checks against the real Creator page in headless Chrome covering the picker, the override prompt (accept and cancel), override invalidation, the cycle exclusions and the dashboard refusal. Screenshots reviewed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -346,6 +346,48 @@ hides the BIM/VDC section and every project is install-only (IWP). A SOP that
|
||||
already has BIM enabled keeps its data — it just stops being offered — so turning
|
||||
the flag off never deletes BIM types, gates, or sequence steps.
|
||||
|
||||
## Release gates (constraints + predecessors)
|
||||
|
||||
A work package reaches **Issued** only when both gates are met:
|
||||
|
||||
1. every constraint is **Cleared** or **N/A** — a hard gate, no override;
|
||||
2. every **predecessor work package** (`data.predecessors`, a list of WP ids) is
|
||||
**Closed**.
|
||||
|
||||
Enforced by `enforce_release_gates()` on **every** path that can set a status —
|
||||
`/api/wps` (the browser and the offline outbox both save through it),
|
||||
`/api/wps/{id}/issue`, and `/api/wps/{id}/status`. Also:
|
||||
|
||||
- **Overridable, deliberately.** Planners legitimately release ahead of upstream
|
||||
close-out, so the predecessor gate accepts `data.gateOverride = {reason, by, at}`.
|
||||
A blank reason is not an override. The server writes a `gate_overridden` audit
|
||||
event naming the reason and what was skipped, and the reason prints on the
|
||||
package. Changing the predecessor set clears the override.
|
||||
- **Cycles are refused** (`check_predecessor_cycle`) — direct and through a chain,
|
||||
with a 400 explaining which package already waits on this one.
|
||||
- **A deleted predecessor does not block.** It would otherwise freeze everything
|
||||
downstream of a package someone removed.
|
||||
- The Creator's picker hides itself and any package that already waits on it, so a
|
||||
cycle is hard to build in the first place; the dashboard refuses to issue a
|
||||
blocked package and points at the form for the logged override.
|
||||
|
||||
`data.seq` (the SOP sequence phase) is still stored and shown, but it is
|
||||
descriptive — it gates nothing.
|
||||
|
||||
## Critical constraints reopened after release
|
||||
|
||||
A constraint marked **Critical** on the SOP that reopens **after** the package was
|
||||
released emails the **owner, PM, CM and everyone on the package's distribution
|
||||
list** (minus whoever reopened it), and writes a `constraint_reopened` audit event.
|
||||
|
||||
Detected by comparing incoming constraints against the stored ones inside the
|
||||
normal upsert — *not* a separate endpoint, because the browser saves through the
|
||||
sync outbox, which only replays `POST /api/wps`; anything hung off another route
|
||||
would be lost offline. It fires only on a real transition (cleared/N-A → open), so
|
||||
re-saving an already-open constraint doesn't re-announce, and never for a package
|
||||
that was never released or a non-critical constraint. Bodies carry the constraint
|
||||
name, WP number and a link — never the package contents.
|
||||
|
||||
## Localization (dates, times, numbers)
|
||||
|
||||
Three levels, most specific first — resolved in `html/wp-format.js`:
|
||||
|
||||
Reference in New Issue
Block a user