diff --git a/CHANGELOG.md b/CHANGELOG.md index e71ebdf5..a94b2fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable user-visible changes to CASCADE are documented here. The format is l ### Added +- **Linear status mapping — full parity with Trello and JIRA.** The Linear PM wizard's Field Mapping step now exposes all eight CASCADE stages that drive agent dispatch (backlog, splitting, planning, todo, inProgress, inReview, done, merged) in lifecycle order, instead of only four. An operator can now map a Linear workflow state to any of `splitting`, `planning`, `todo`, or `merged` and have the corresponding agent (splitting, planning, implementation, backlog-manager) dispatch on issue transitions — previously these four stages were unreachable from Linear because the wizard had no slot to save them. Existing Linear integrations upgrade in place: the four new slots render as "not set" on next wizard visit; pre-existing mappings are untouched. No migration required. The normalized `ProjectPMConfig.statuses` type widens to declare the full nine-stage vocabulary (including `debug`, reserved for a future trigger), so providers can no longer silently drift from the trigger layer's dispatch map. (Spec [003](docs/specs/003-linear-status-mapping-parity.md), plan [1/1](docs/plans/003-linear-status-mapping-parity/1-status-parity.md).) - **Linear wizard — inline webhook signing-secret field and accurate events list.** The Webhooks step of the Linear PM wizard now renders a `ProjectSecretField` bound to `LINEAR_WEBHOOK_SECRET` directly beneath the webhook URL, so operators can paste Linear's signing secret in place instead of navigating to the Credentials tab. The "Enable events" instructions now list the three event families CASCADE actually consumes — `Issues` (status transitions), `Comments` (bot @mentions), and `Issue Labels` ("Ready to Process") — each with a one-line rationale tracing back to the registered trigger handlers. (Spec [002](docs/specs/002-linear-webhook-setup-ux.md), plan [2/2](docs/plans/002-linear-webhook-setup-ux/2-wizard-webhooks-step.md).) ### Changed @@ -14,6 +15,7 @@ All notable user-visible changes to CASCADE are documented here. The format is l ### Fixed +- **JIRA `resolveLifecycleConfig` silent-drop of splitting / planning / todo.** The JIRA PM wizard accepts mappings for all eight CASCADE stages, but the normalization step that feeds them to `PMLifecycleManager` was dropping `splitting`, `planning`, and `todo` on the floor. Any agent lifecycle hook that moved JIRA issues to those statuses silently no-op'd. Now passes all eight keys through. No operator action required — existing JIRA mappings start working once the fix deploys. (Spec [003](docs/specs/003-linear-status-mapping-parity.md), plan [1/1](docs/plans/003-linear-status-mapping-parity/1-status-parity.md).) - **Linear wizard Save — HTTP 500 on `projects.integrations.upsert`.** A check constraint (`chk_integration_category_provider`) restricted the `pm` category to `trello` or `jira`; Linear support shipped without a matching constraint update, so every attempt to save a Linear PM integration failed with SQLSTATE 23514. Migration 0049 adds `linear` to the allowed `pm` providers. (Spec [002](docs/specs/002-linear-webhook-setup-ux.md), plan [1/2](docs/plans/002-linear-webhook-setup-ux/1-save-path-fix.md).) - **Dashboard error logs now surface DB diagnostic fields.** Unhandled errors in the Hono app error handler and tRPC error formatter now include PG error code, detail, constraint, table, and column (unwrapped from `.cause` when Drizzle wraps a pg driver error). Clients still receive a generic "Internal server error" for unexpected `INTERNAL_SERVER_ERROR` throws — real diagnostics go to stdout for operators to grep. (Spec [002](docs/specs/002-linear-webhook-setup-ux.md), plan [1/2](docs/plans/002-linear-webhook-setup-ux/1-save-path-fix.md).) diff --git a/docs/plans/003-linear-status-mapping-parity/1-status-parity.md.done b/docs/plans/003-linear-status-mapping-parity/1-status-parity.md.done new file mode 100644 index 00000000..e824de3f --- /dev/null +++ b/docs/plans/003-linear-status-mapping-parity/1-status-parity.md.done @@ -0,0 +1,352 @@ +--- +id: 003 +slug: linear-status-mapping-parity +plan: 1 +plan_slug: status-parity +level: plan +parent_spec: docs/specs/003-linear-status-mapping-parity.md +depends_on: [] +status: done +--- + +# 003/1: Linear + JIRA Status Mapping Parity — Full Stage Vocabulary End-to-End + +> Part 1 of 1 in the 003-linear-status-mapping-parity plan. See [parent spec](../../specs/003-linear-status-mapping-parity.md). + +## Summary + +Single coherent plan that delivers spec 003 in its entirety: widens the canonical `ProjectPMConfig.statuses` type to cover all 9 CASCADE stages; expands the Linear wizard's Field Mapping step from 4 slots to 8 (backlog, splitting, planning, todo, inProgress, inReview, done, merged); extends `LinearIntegration.resolveLifecycleConfig` to pass those 4 new keys through; and fixes `JiraIntegration.resolveLifecycleConfig` to stop silently dropping splitting/planning/todo that its wizard already accepts. + +Why one plan, not several: the type widening is 4 additive optional keys and has no downstream that rejects it — splitting would ship dead weight in plan 1 with no consumer. The Linear wizard UI and Linear's `resolveLifecycleConfig` are tightly coupled (both must ship together for a user to actually persist and act on new mappings). JIRA's backend fix is a 3-key add to one object literal and reads naturally alongside Linear's identical change. All of it reviews and reverts as a single logical unit. + +**Components delivered:** +- `src/pm/lifecycle.ts` — widen `ProjectPMConfig.statuses` to the full 9 keys (all optional). +- `src/pm/linear/integration.ts` — `resolveLifecycleConfig` returns `splitting`, `planning`, `todo`, `merged` in addition to the current four. +- `src/pm/jira/integration.ts` — `resolveLifecycleConfig` returns `splitting`, `planning`, `todo` in addition to the current five. +- `web/src/components/projects/pm-wizard-linear-steps.tsx` — `LINEAR_STATUS_SLOTS` grows from 4 to 8 (lifecycle order). +- `web/src/components/projects/pm-wizard-state.ts` — if `linearStatuses` has a known-narrow-type, widen it to accept the new keys. +- Tests under `tests/unit/pm/`, `tests/unit/triggers/`, `tests/unit/web/`. +- `CHANGELOG.md` — one Unreleased entry describing the parity fix. + +**Deferred to later plans in this spec:** +- Nothing. Plan 1 closes out spec 003. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #1 (Linear wizard surface — 8 rows, lifecycle order) — **full** +- Spec AC #2 (Persistence) — **full** +- Spec AC #3 (Agent dispatch end-to-end on Linear transitions for the 4 newly-reachable stages) — **full** +- Spec AC #4 (Canonical type coverage — 9 keys declared) — **full** +- Spec AC #5 (Trello parity preserved) — **full** +- Spec AC #6 (JIRA agent dispatch for splitting/planning/todo works) — **full** +- Spec AC #7 (Existing Linear integrations upgrade cleanly) — **full** +- Spec AC #8 (No required migration) — **full** (change is type-widening + JSONB-additive) +- Spec AC #9 (Unmapped transitions are a no-op) — **full** +- Spec AC #10 (Zod / tRPC validation accepts any subset) — **full** +- Spec AC #11 (Lint / typecheck / tests green) — **full** + +--- + +## Depends On + +- Nothing external. Plan is foundation + target + UI in one hop. + +Context to lift from the spec: +- Linear wizard exposes **8** slots; canonical type declares **9** (`debug` is declared but not UI-surfaced — Strategic decision #2). +- Manual-only mapping; no auto-discovery (Strategic decision #3). +- Existing Linear rows upgrade in place — new slots render "not set" (Strategic decision #4). +- Out of scope: broadening `STATUS_TO_AGENT`, label mappings, Sentry, JIRA wizard UI. + +--- + +## Detailed Task List (TDD) + +### 1. Widen the canonical type + +**Tests first** (`tests/unit/pm/lifecycle-config-shape.test.ts` — new file): + +- `ProjectPMConfig.statuses accepts all 9 canonical CASCADE stages as optional keys` — type-level assertion: construct an object typed as `ProjectPMConfig` with every one of `backlog, splitting, planning, todo, inProgress, inReview, done, merged, debug` set to a string; assert TypeScript accepts it. (Use `satisfies` or an explicit typed const; compile-time check.) +- `ProjectPMConfig.statuses accepts an empty object (all keys optional)` — compile-time check. +- `Every key in STATUS_TO_AGENT is a declared key of ProjectPMConfig.statuses` — runtime check: iterate `Object.keys(STATUS_TO_AGENT)`, assert each is a valid `ProjectPMConfig['statuses']` key (use a helper that treats the keys as a string union). + +**Implementation** (`src/pm/lifecycle.ts`, `ProjectPMConfig` interface): + +Expand the `statuses` shape from: +```ts +statuses: { + backlog?: string; + inProgress?: string; + inReview?: string; + done?: string; + merged?: string; +}; +``` +to: +```ts +statuses: { + backlog?: string; + splitting?: string; + planning?: string; + todo?: string; + inProgress?: string; + inReview?: string; + done?: string; + merged?: string; + debug?: string; +}; +``` + +All new keys optional. Order matches CASCADE lifecycle. No other changes in this file. + +**Stale-reference check:** grep for any narrowed type that derives from `ProjectPMConfig.statuses` (e.g. `keyof ProjectPMConfig['statuses']`). If a consumer exhaustively switches on the 5-key union, it needs a `default` case or must add branches — surface before editing. + +### 2. Linear resolveLifecycleConfig — pass the new keys through + +**Tests first** (`tests/unit/pm/linear-integration.test.ts` — augment existing or new file): + +- `resolveLifecycleConfig returns all 8 status keys from pm.config.statuses` — given `project.pm.config.statuses = { backlog: 's-bl', splitting: 's-sp', planning: 's-pl', todo: 's-td', inProgress: 's-ip', inReview: 's-ir', done: 's-dn', merged: 's-mg' }`, assert the returned `ProjectPMConfig.statuses` contains each key with the corresponding value. +- `resolveLifecycleConfig preserves undefined for keys not provided` — given a partial input (only `inProgress` set), assert only `inProgress` is present on the output, others are `undefined`. +- `resolveLifecycleConfig does not read or return debug` — Linear UI does not surface `debug`; integration must not write it to the canonical shape from Linear-side config. Assert it's not present on the returned `statuses` object. + +**Implementation** (`src/pm/linear/integration.ts`, `resolveLifecycleConfig`): + +Expand the `statuses` object from: +```ts +statuses: { + backlog: linearConfig?.statuses?.backlog, + inProgress: linearConfig?.statuses?.inProgress, + inReview: linearConfig?.statuses?.inReview, + done: linearConfig?.statuses?.done, + merged: linearConfig?.statuses?.merged, +} +``` +to: +```ts +statuses: { + backlog: linearConfig?.statuses?.backlog, + splitting: linearConfig?.statuses?.splitting, + planning: linearConfig?.statuses?.planning, + todo: linearConfig?.statuses?.todo, + inProgress: linearConfig?.statuses?.inProgress, + inReview: linearConfig?.statuses?.inReview, + done: linearConfig?.statuses?.done, + merged: linearConfig?.statuses?.merged, +} +``` + +Linear's config-shape type (e.g. `LinearConfig.statuses`, likely in `src/types/index.ts` or `src/pm/linear/types.ts`) must permit those keys. Grep for the type before editing; widen it the same way as `ProjectPMConfig.statuses` (optional new keys). No `debug` key on Linear — Linear doesn't surface it. + +### 3. JIRA resolveLifecycleConfig — stop dropping splitting/planning/todo + +**Tests first** (`tests/unit/pm/jira-integration.test.ts` — augment existing or new file): + +- `resolveLifecycleConfig returns splitting, planning, todo when present in jira config` — given `jiraConfig.statuses = { splitting: 'SPL', planning: 'PLAN', todo: 'TODO', inProgress: 'IP', done: 'DN' }`, assert each is present on the returned `ProjectPMConfig.statuses`. +- `resolveLifecycleConfig still returns backlog / inProgress / inReview / done / merged` — regression guard: the 5 keys that worked pre-spec still work. + +**Implementation** (`src/pm/jira/integration.ts`, `resolveLifecycleConfig`): + +Expand the `statuses` object from: +```ts +statuses: { + backlog: jiraConfig?.statuses?.backlog, + inProgress: jiraConfig?.statuses?.inProgress, + inReview: jiraConfig?.statuses?.inReview, + done: jiraConfig?.statuses?.done, + merged: jiraConfig?.statuses?.merged, +} +``` +to: +```ts +statuses: { + backlog: jiraConfig?.statuses?.backlog, + splitting: jiraConfig?.statuses?.splitting, + planning: jiraConfig?.statuses?.planning, + todo: jiraConfig?.statuses?.todo, + inProgress: jiraConfig?.statuses?.inProgress, + inReview: jiraConfig?.statuses?.inReview, + done: jiraConfig?.statuses?.done, + merged: jiraConfig?.statuses?.merged, +} +``` + +Check `JiraConfig` type (`src/types/index.ts` or `src/pm/jira/types.ts`): its `statuses` shape likely already accepts arbitrary string keys or already includes splitting/planning/todo (since the wizard writes them). If it doesn't, widen it. + +### 4. Linear wizard — expand the slot list + +**Tests first** (`tests/unit/web/linear-field-mapping-step.test.ts` — new file, Node SSR style, same pattern as `linear-webhook-info-panel.test.ts`): + +- `renders 8 status mapping rows in lifecycle order` — SSR-render `LinearFieldMappingStep` with a minimal wizard state containing an `availableStatuses` list. Assert the rendered HTML contains, in order: `backlog`, `splitting`, `planning`, `todo`, `inProgress`, `inReview`, `done`, `merged`. Assert the `inverse` order is not present (guard against alphabetical). +- `does not render a debug row` — assert the string `debug` is not present among rendered status slot labels. +- `each row has a dropdown and a manual-entry fallback` — assert that for each of the 8 slots a ` { + const html = render({ + linearStatusMappings: { + splitting: 'Splitting', + planning: 'Planning', + }, + }); + // The persisted values should appear as selected option values. + expect(html).toContain('value="Splitting"'); + expect(html).toContain('value="Planning"'); + }); +}); diff --git a/web/src/components/projects/pm-wizard-linear-steps.tsx b/web/src/components/projects/pm-wizard-linear-steps.tsx index e07c406f..a1165335 100644 --- a/web/src/components/projects/pm-wizard-linear-steps.tsx +++ b/web/src/components/projects/pm-wizard-linear-steps.tsx @@ -13,7 +13,16 @@ import { FieldMappingRow, SearchableSelect } from './wizard-shared.js'; // Slot definitions // ============================================================================ -const LINEAR_STATUS_SLOTS = ['backlog', 'inProgress', 'inReview', 'done']; +const LINEAR_STATUS_SLOTS = [ + 'backlog', + 'splitting', + 'planning', + 'todo', + 'inProgress', + 'inReview', + 'done', + 'merged', +] as const; const LINEAR_LABEL_SLOTS = ['processing', 'processed', 'error', 'readyToProcess', 'auto'];