Skip to content

fix(#2732): group/mission completion cascade + self-heal - #2744

Merged
jung-thomas merged 14 commits into
DEVfrom
worktree-issue-2732-cascade-selfheal
Oct 13, 2026
Merged

jung-thomas merged 14 commits into
DEVfrom
worktree-issue-2732-cascade-selfheal

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

Closes #2732 (root cause of #2718 / #2719 / #2722).

Group/Mission completion was a stored, event-triggered TaskRecord that was never recomputed on read and did not self-heal when completion paths changed. This makes it self-healing.

What changed

  • Never downgrade a COMPLETED parent on recompute (upsertRollupRecord gains allowDowngrade, default false; opt-in true only on the explicit tutorial reset/supersede path). Matches the legacy IMS short-circuit.
  • Exclude INACTIVE tutorials from completion slots (loadGroupSlots / loadMissionSlots) so a retired tutorial no longer counts as an unsatisfiable required slot — the cp-aibus-dox-booster-app class of breakage.
  • Empty-slot guard: a mission/group that filters to zero active slots is NOT auto-completed; it logs + increments rollup.empty_slots.
  • Self-heal reconcile — new srv/lib/completion-reconcile.js:
    • reconcileEntityForUsers recomputes a mission/group's GROUP/MISSION records for affected users (candidate set = existing parent-record holders ∪ users who already satisfy the slots), so a repaired mapping newly grants completion and a stale record is corrected — without demoting historical completers.
    • Targeted on admin edit: after('SAVE', 'Missions'/'Groups') root hooks (the Fiori draft-activate path) + child DELETE hooks fire a fire-and-forget reconcile. Never fails the author's SAVE.
    • Nightly reconcile-completions job (reconcileAll) sweeps all published missions/groups — the backstop that repairs the ~50 pre-existing dead-slot entities and keeps them healed.
  • AdminService.completionHealth() — on-demand admin function listing missions/groups whose paths contain an INACTIVE/unresolvable tutorial, so authors can find and fix broken mappings.

Stored GROUP/MISSION TaskRecords remain the source of truth (analytics + /build/my-progress depend on them); this repairs drift rather than deriving on read. No schema change. All new behavior is fail-open.

Testing

  • 7 new Vitest files, 14 tests, all green; existing rollup (12) + admin (38) tests unchanged.
  • npx cds compile srv --to sql exits 0.
  • srv-qa cp-list audit: completion-reconcile.js is not a content-store.js dependency — no .deploy/mta.yaml change.

Post-deploy (per spec Rollout — "test the actual thing")

After DEV deploy: run reconcile-completions via admin runJob, spot-check missions 12479 / 12052 / 16326 complete for users who earned them, and open completionHealth to confirm it lists the INACTIVE-slot missions.

Design: docs/superpowers/specs/2026-10-13-group-mission-cascade-selfheal-design.md
Plan: docs/superpowers/plans/2026-10-13-group-mission-cascade-selfheal.md

…ath item

Wires after(['CREATE','UPDATE','DELETE'], 'CompletionPathItems') and
after(['CREATE','UPDATE','DELETE'], 'GroupPathItems') hooks in AdminService.
Each hook resolves the parent MISSION/GROUP legacyId and fire-and-forgets
reconcileEntityForUsers via setImmediate + its own cds.tx so a reconcile
fault never fails the author's SAVE.

DELETE parent resolution: a matching before('DELETE') hook captures path_ID
(or group_ID) from req.query.DELETE.where[2].val before the row is removed;
the after hook reads it from req._cpiPathId / req._gpiGroupId.

Tests: two integration tests verify the DELETE path via AdminService.tx()
with mocked Admin user (pattern from test/pat-admin-grant.test.js) — one
asserts a partial-completion mission heals to COMPLETED after the blocking
item is removed; one asserts the fire-and-forget never throws.
…tions

CAP fires SAVE on the root active entity (Missions/Groups) on draftActivate,
not on composition children -- so the child CompletionPathItems/GroupPathItems
after(CREATE/UPDATE) hooks never fire for normal Fiori edit-and-save. Fix:
add after('SAVE','Missions') and after('SAVE','Groups') root hooks that read
legacyId from req.data, fire-and-forget reconcileEntityForUsers. On draft-
enabled entities SAVE fires specifically on draftActivate; on non-draft active
entities it is a shorthand for CREATE+UPDATE -- both production paths covered.

Also:
- Replace 150ms setTimeout settle with vi.waitFor() polling in tests
- Add draftActivate end-to-end test (full HTTP flow) for the SAVE path
- Tighten fail-open test assertion to expect status === 'COMPLETED' (no change)
  rather than accepting either COMPLETED or IN_PROGRESS
@jung-thomas
jung-thomas merged commit b3b7611 into DEV Oct 13, 2026
5 of 6 checks passed
@jung-thomas
jung-thomas deleted the worktree-issue-2732-cascade-selfheal branch October 13, 2026 12:09
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