fix(formation): stop checklist sub_stage guessing engaged - #2373
Conversation
deriveFormationSubStage fell through its default case to 'engaged' for any project stage outside the three queue sub-stages (Disengaged, Confidential, Active, Archived, Prospect, or an empty stage), so GET /api/projects/:slug/formation silently mislabeled those projects. Delete the redundant helper and normalize Formation.sub_stage through the shared normalizeFormationSubStage (added by #2370 for the queue path), sourced from the project record's stage since the checklist read carries no stage of its own. sub_stage is now FormationSubStage | null, with a new sub_stage_raw carrying the verbatim upstream value so a future consumer can render it honestly instead of guessing. Refs #2328 (scope item 4 only). Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
Pre-PR review flagged format:check drift (import line, it.each indent, markdown table padding) from the prior commit, and a prose ambiguity in the server-helpers.md row that could be read as claiming getFormationQueueStageDisplay is called from formation.service.ts and the checklist mapper — only normalizeFormationSubStage is; the display helper is called from formations-table.component.ts. Refs #2328. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Essentials Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
There was a problem hiding this comment.
Pull request overview
Fixes checklist API stage normalization so unmapped stages return null instead of incorrectly defaulting to engaged.
Changes:
- Reuses shared formation-stage normalization and preserves the raw stage.
- Widens the checklist contract and updates fixtures.
- Adds mapper and service regression coverage.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
packages/shared/src/interfaces/formation.interface.ts |
Updates the checklist stage contract. |
packages/shared/src/enums/formation.enum.spec.ts |
Updates typed contract fixtures. |
docs/architecture/backend/server-helpers.md |
Documents shared normalization. |
apps/lfx-one/src/server/services/formation.service.spec.ts |
Tests the full service path. |
apps/lfx-one/src/server/helpers/formation-mapper.helper.ts |
Replaces fallback derivation with normalization. |
apps/lfx-one/src/server/helpers/formation-mapper.helper.spec.ts |
Adds mapper regression tests. |
apps/lfx-one/src/app/modules/dashboards/components/formation-entry-card/formation-entry-card.component.spec.ts |
Updates the component fixture. |
apps/lfx-one/e2e/helpers/formation-checklist.helper.ts |
Updates checklist API mocks. |
apps/lfx-one/e2e/fixtures/mock-data/formation.mock.ts |
Updates shared E2E formation data. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Prod validation done — the gap flagged in the PR body is closedRun against the prod-backed integration tree (
All three previously resolved to No regression on the queue pathThe same session checked #2370's surface, since this branch and #2374 both touch The PR's correction to the issue standsThis PR's body corrects the 13 Sep comment on #2328, which claimed the three projects' checklist pages label them "Formation · Engaged" today. That correction is right: nothing renders Also worth knowing, from the same sessionEvery one of the 2142 items across all 126 production formations is
|
PR SummaryMedium Risk Overview
New Reviewed by Cursor Bugbot for commit 8aee0c0. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
Re-confirmed on the merged integration tree (this branch + #2374 + #2376, served against prod, 13 Sep) — the three changes compose without regression:
One merge conflict came up folding #2376 in, in An unrelated defect surfaced in the same session — #2378, the queue landing on 3 rows instead of 126 — which comes from #2369 and is already on |
MRashad26
left a comment
There was a problem hiding this comment.
Verified independently against origin/main and the pre-existing normalizeFormationSubStage helper (added by #2370, already tested/used by the queue path). The deleted deriveFormationSubStage did default every non-Formation/Disengaged/Confidential stage to 'engaged' exactly as described — confirmed by reading its old switch/default in the diff. The checklist mapper now sources from the same shared helper the queue uses, so the two screens can't disagree. sub_stage correctly widened to FormationSubStage | null without widening the union itself, and sub_stage_raw gives a future consumer the honest raw string to render instead of guessing. Tests use real upstream stage strings (not short keys) plus a constructor/prototype-collision guard — good given that short-key fixtures are exactly how this bug class reached production twice before.
Non-blocking nit: branch name fix/GH-2328-substage-derivation uses GH-XXX rather than this repo's documented issue-<number> convention (.claude/rules/commit-workflow.md) — same nit flagged on a couple of other PRs in this batch, just flagging for next time, not worth renaming.
Summary
deriveFormationSubStage(formation-mapper.helper.ts) fell through itsdefault:case to'engaged'for every project stage outside the three queue sub-stages —Formation - Disengaged,Formation - Confidential,Active,Archived,Prospect, and an empty stage all silently became'engaged'. In production, 3 of 126 projects (Agent Router, Durable Agents, KapunSDK) hit this. Same defect class #2370 already fixed on the queue path (getFormationsQueueLive).Fixes GH-2328 scope item 4 only — the other four items (propagating
lifecycle, 404-vs-read-only, gating the eight item mutations, tightening the route guard) stay on #2328.Correction to the issue's 13 Sep comment
The 13 Sep comment on #2328 states "their checklist pages label them Formation · Engaged right now." This is not reproducible on
main— verified by repo-wide grep ofsub_stage/SubStage:deriveFormationSubStagehad exactly one caller (mapUpstreamFormationChecklist), writingFormation.sub_stage. Its own doc comment claimed a second caller ingetProjectFormation's ROOT-collapse path — that was stale; ROOT-collapse never called it.Formation.sub_stage. The checklist page has no stage display at all; the checklist section only readsgating_items_open/gating_items_total.project-dashboard.component.html/formation-card.component.html→ProjectContextService.activeProjectFormationSubStage→getFormationSubStageLabel(project.stage), which uses the 5-valueProjectStagetaxonomy and already returnsnullforActive.So the wrong
'engaged'reached the wire inGET /api/projects/:slug/formation's response body, but no pixel rendered it. Still worth fixing — a confidently-wrong value on a shipped API contract, before any consumer trusts it — but it's a contract fix, not a rendering fix. No template changes are in this PR.Changes
deriveFormationSubStageoutright — its threecases duplicatedUPSTREAM_SUB_STAGE_TO_FORMATION_SUB_STAGE's three keys; the only difference was the wrongdefault:.mapUpstreamFormationChecklistnow normalizessub_stagevia the sharednormalizeFormationSubStage(added by fix(formation): normalize sub_stage at BFF boundary #2370 for the queue), sourced fromctx.project.stage— the checklist read (UpstreamFormationChecklist) carries no stage field of its own.Formation.sub_stagewidened toFormationSubStage | null; addedFormation.sub_stage_raw: stringcarrying the verbatim upstream stage, mirroringFormationQueueRow.nullmeans the project'sProjectStagehas no queue-taxonomy equivalent (FormationDisengaged/Confidential, or a non-Formation stage) — a future consumer renderssub_stage_rawthroughgetFormationQueueStageDisplayrather than guessing.FormationSubStageitself is not widened.Formation-typed fixtures (e2e mocks, component specs, enum spec) for the new required field.docs/architecture/backend/server-helpers.mdupdated to reflect the checklist mapper now also callingnormalizeFormationSubStage.Explicitly out of scope
FormationSubStageunion stays 3 values — no widening.getFormationsQueueLiveand anything else fix(formation): normalize sub_stage at BFF boundary #2370 changed — untouched beyond consuming its helpers.my-formations-card.component.html:24(unguarded label/severity index offMyFormationSummary.sub_stage) — reported, not fixed (below); already flagged latent on fix(formation): normalize sub_stage at BFF boundary #2370.Tests
apps/lfx-one/src/server/helpers/formation-mapper.helper.spec.ts(helper had no spec before) — coverssub_stage/sub_stage_rawwith real upstream stage strings, not short keys (short-key fixtures are how both Formations queue: sub_stage contract mismatch zeroes every tile, blanks stage chips and breaks the filter tabs #2366 and this bug reached production undetected): all 5 Formation-taxonomy stages,Active,Archived,Prospect, unrecognized string, empty string, and a'constructor'prototype-pollution guard case.formation.service.spec.ts: addedsub_stage/sub_stage_rawassertions to the existingActive-stage fixture, plus anit.eachacross 8 real upstream stage strings through the fullgetProjectFormationservice path.All green:
yarn check-types,yarn test(2757/2757),yarn lint,yarn format:check,./check-headers.sh,yarn build.Report — other instances of the same fallback-to-valid-value pattern (not fixed here)
A
default:/??/unguarded-index that turns an unrecognized upstream value into a specific valid value instead of "unknown":apps/lfx-one/src/server/services/org-lens-projects.service.ts:569-580—mapInfluence()defaults unrecognized SnowflakeTECHNICAL_INFLUENCE/ECOSYSTEM_INFLUENCEvalues to'silent'/'non-lf'— real, presentable bands, not "unknown." Reachable (live org-projects dashboard data).apps/lfx-one/src/server/services/org-lens-projects.service.ts:582-584—mapTrendDirection()collapses any unrecognized trend value to'flat', masking a real signal or anomaly as "no change." Reachable, same data path.apps/lfx-one/src/server/services/org-lens-project-detail.service.ts:1092-1103—mapDetailLevel()defaults unrecognized/null/mis-cased influence-level values to'Silent'(the most negative category). Reachable — feeds the org leaderboard detail drawer.apps/lfx-one/src/server/services/ai.service.ts:535-554—getMeetingTypeDescription()defaults anymeetingTypeoutside the 7 known enum members to'project team'— the same label as the legitimateOTHERcase, so a malformed value is indistinguishable from a real "Other" meeting. Reachable if the request body isn't runtime-validated before this call.packages/shared/src/utils/date-time.utils.ts:539-554—mapRecurrenceToFormValue()defaults any unrecognizedrecurrence.typeto'none'("does not repeat"). Latent today (closed 3-value enum), but would silently hide a genuinely recurring meeting the moment a new recurrence type is added upstream.Three-plus instances of the same shape (this PR's fix, plus #1–5) — worth considering a lint rule (e.g. flag
default:in a mapper that returns a literal from a known-value union) over one-off fixes.Validation gap — flagging explicitly
Per the plan, prod read-only validation (
GET /api/projects/<slug>/formationfor Agent Router, Durable Agents, KapunSDK, confirmingsub_stage: null/sub_stage_rawverbatim, before/after JSON) requires the integration worktree (~/lfx/ss-integration) wired to prod via/lfx-servein its own session — that's a separate, heavier bring-up step outside this per-ticket worktree's scope, and no integration server was running. Not done in this PR — recommend running it in the integration pass alongside the other in-flight tickets, per the standard workflow.Refs #2328 (scope item 4 only).
🤖 Generated with Claude Code
https://claude.ai/code/session_01FCkcxE9GbFf7gg8z4baA2e