@burdenoff/microfe-planmagnet 2026.1001.6 β†’ 2026.1001.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  2. package/dist/planmagnet/PlanMagnetRoutes.js +10 -6
  3. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  4. package/dist/planmagnet/automation/automationRuleTypes.d.ts +31 -19
  5. package/dist/planmagnet/automation/automationRuleTypes.d.ts.map +1 -1
  6. package/dist/planmagnet/automation/automationRuleTypes.js +4 -8
  7. package/dist/planmagnet/automation/automationRuleTypes.js.map +1 -1
  8. package/dist/planmagnet/controls/FeatureControls.d.ts +18 -1
  9. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
  10. package/dist/planmagnet/controls/FeatureControls.js +259 -202
  11. package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
  12. package/dist/planmagnet/controls/FeedbackControls.d.ts +70 -0
  13. package/dist/planmagnet/controls/FeedbackControls.d.ts.map +1 -0
  14. package/dist/planmagnet/controls/FeedbackControls.js +153 -0
  15. package/dist/planmagnet/controls/FeedbackControls.js.map +1 -0
  16. package/dist/planmagnet/controls/SyncControls.d.ts.map +1 -1
  17. package/dist/planmagnet/controls/SyncControls.js +1 -0
  18. package/dist/planmagnet/controls/SyncControls.js.map +1 -1
  19. package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts +47 -0
  20. package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts.map +1 -0
  21. package/dist/planmagnet/controls/WebhookSubscriptionControls.js +542 -0
  22. package/dist/planmagnet/controls/WebhookSubscriptionControls.js.map +1 -0
  23. package/dist/planmagnet/dashboards/DashboardUI.js +7 -7
  24. package/dist/planmagnet/dashboards/cards/RiskSeverityCard.js +4 -4
  25. package/dist/planmagnet/forecast/DeliveryForecastCard.js +8 -8
  26. package/dist/planmagnet/forecast/ProjectOutlookCard.js +10 -10
  27. package/dist/planmagnet/forecast/ProjectRiskSummaryCard.js +9 -9
  28. package/dist/planmagnet/forecast/ResourceForecastCard.js +6 -6
  29. package/dist/planmagnet/integrations/syncTypes.d.ts +2 -1
  30. package/dist/planmagnet/integrations/syncTypes.d.ts.map +1 -1
  31. package/dist/planmagnet/integrations/syncTypes.js +19 -9
  32. package/dist/planmagnet/integrations/syncTypes.js.map +1 -1
  33. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +8 -8
  34. package/dist/planmagnet/pages/DashboardDashboardPagePage.js +23 -23
  35. package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
  36. package/dist/planmagnet/pages/DocsDocDetailPagePage.js +340 -248
  37. package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
  38. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.d.ts.map +1 -1
  39. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js +172 -167
  40. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js.map +1 -1
  41. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.d.ts.map +1 -1
  42. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js +92 -83
  43. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js.map +1 -1
  44. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts +2 -0
  45. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts.map +1 -0
  46. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js +175 -0
  47. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js.map +1 -0
  48. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  49. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +151 -126
  50. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  51. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  52. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +136 -129
  53. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  54. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
  55. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +149 -119
  56. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  57. package/dist/planmagnet/pages/ProductsProductsPagePage.js +13 -13
  58. package/dist/planmagnet/pages/ProjectsAtRiskPagePage.js +9 -9
  59. package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js +8 -8
  60. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +12 -12
  61. package/dist/planmagnet/pages/ReportsReportsPagePage.js +23 -23
  62. package/dist/planmagnet/pages/TasksMyDashboardPagePage.js +18 -18
  63. package/dist/planmagnet/pages/TasksMyWorkPagePage.js +10 -10
  64. package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js +5 -5
  65. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +8 -8
  66. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +6 -6
  67. package/dist/planmagnet/pages/analytics/MetricsCard.js +6 -6
  68. package/dist/planmagnet/pages/analytics/QualityMetricsCard.js +6 -6
  69. package/dist/planmagnet/pages/analytics/SprintDeliveryCards.js +8 -8
  70. package/dist/planmagnet/pages/analytics/StatusFlowCard.js +7 -7
  71. package/dist/planmagnet/pages/analytics/ThroughputCard.js +7 -7
  72. package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js +7 -7
  73. package/dist/planmagnet/people/People.d.ts +14 -0
  74. package/dist/planmagnet/people/People.d.ts.map +1 -1
  75. package/dist/planmagnet/people/People.js +36 -22
  76. package/dist/planmagnet/people/People.js.map +1 -1
  77. package/dist/planmagnet/webhooks/webhookTypes.d.ts +54 -0
  78. package/dist/planmagnet/webhooks/webhookTypes.d.ts.map +1 -0
  79. package/dist/planmagnet/webhooks/webhookTypes.js +17 -0
  80. package/dist/planmagnet/webhooks/webhookTypes.js.map +1 -0
  81. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"automationRuleTypes.js","names":[],"sources":["../../../src/planmagnet/automation/automationRuleTypes.ts"],"sourcesContent":["/**\n * Automation rules (BOFF-8047). Shared trigger/condition/action vocabulary, kept in one place so the create\n * page, the edit page and the list's summaries can never drift from each other or from the engine.\n *\n * Scope note, rewritten from the real engine (wspace-planmagnet-svc `src/modules/automation/services/\n * automation-engine.ts` + `automation-scheduler.ts`, both merged and prod-verified β€” svc PR #64):\n *\n * - Triggers that fire AUTOMATICALLY: `WORK_ITEM_CREATED`, `WORK_ITEM_STATUS_CHANGED`, `WORK_ITEM_ASSIGNED`\n * (from every WorkItem write that changes status/assignee), `SPRINT_STARTED`/`SPRINT_COMPLETED` (a sprint\n * transition), `RELEASE_RELEASED` (a release's real transition into RELEASED β€” not a re-send) and `SCHEDULE`\n * (a daily background scan β€” see `automation-scheduler.ts`, NOT dispatched from a write path at all). Still\n * real, accepted, saved schema values but INERT β€” never fire on their own: `WORK_ITEM_UPDATED`,\n * `MILESTONE_DUE`, `WEBHOOK`. `MANUAL` is not inert, it is intentionally manual-only: it runs only via\n * \"Run now\" (`planmagnetExecuteAutomationRule`), which works for a rule of ANY trigger type because it\n * targets one named work item directly instead of listening for an event.\n * - Dispatchable actions: `ASSIGN_TO_USER`, `ADD_LABEL`, `SET_STATUS`, `SET_PRIORITY` (the four WorkItem\n * fields an automation can mutate), `POST_COMMENT` (a templated comment on the triggering work item,\n * authored by the fixed `automation:engine` identity β€” see `AUTOMATION_ACTOR_NAMES` in `../people/People`)\n * and `POST_TO_CHANNEL` (a templated message to a Conversations channel) β€” but `POST_TO_CHANNEL` is gated\n * OFF in prod (`isChannelPostingEnabled()` in the engine) until Platform ticket BOFF-8240 ships: saving one\n * is refused server-side with \"Posting to a chat channel from an automation is not available yet\", so this\n * UI never lets it be selected or saved either (see `CHANNEL_POSTING_AVAILABLE` below).\n * - `SPRINT_STARTED`/`SPRINT_COMPLETED`/`RELEASE_RELEASED` carry NO target work item (`sprint.service.ts`/\n * `release.service.ts` never set `workItemId` on their `TriggerData`), so every work-item action β€” all five\n * above except `POST_TO_CHANNEL` β€” cannot run for them. Today that leaves such a rule able to do nothing\n * (the only action that *could* apply, `POST_TO_CHANNEL`, is itself gated off), so the builder below refuses\n * to offer work-item actions for these triggers and blocks Save entirely rather than let someone build a\n * rule that silently never fires anything.\n */\n\nexport const TRIGGER_TYPES = [\n 'WORK_ITEM_CREATED',\n 'WORK_ITEM_STATUS_CHANGED',\n 'WORK_ITEM_ASSIGNED',\n 'SPRINT_STARTED',\n 'SPRINT_COMPLETED',\n 'RELEASE_RELEASED',\n 'SCHEDULE',\n 'WORK_ITEM_UPDATED',\n 'MILESTONE_DUE',\n 'WEBHOOK',\n 'MANUAL',\n] as const;\nexport type TriggerType = (typeof TRIGGER_TYPES)[number];\n\nexport const TRIGGER_TYPE_LABEL: Record<TriggerType, string> = {\n WORK_ITEM_CREATED: 'Work item created',\n WORK_ITEM_STATUS_CHANGED: 'Work item status changed',\n WORK_ITEM_ASSIGNED: 'Work item assigned',\n SPRINT_STARTED: 'Sprint started',\n SPRINT_COMPLETED: 'Sprint completed',\n RELEASE_RELEASED: 'Release shipped',\n SCHEDULE: 'Schedule',\n WORK_ITEM_UPDATED: 'Work item updated',\n MILESTONE_DUE: 'Milestone due',\n WEBHOOK: 'Webhook',\n MANUAL: 'Manual only',\n};\n\n/** Looks up a trigger type's label; falls back to the raw value for one this UI does not yet know (forward\n * compatible with a value the service adds later). Cast: `TRIGGER_TYPE_LABEL` is keyed by the known union, but\n * a value read back from the API is a plain string. */\nexport function triggerTypeLabel(value: string): string {\n return (TRIGGER_TYPE_LABEL as Record<string, string>)[value] ?? value;\n}\n\n/** Fires on its own β€” dispatched from a real write path or the daily SCHEDULE scan (automation-engine.ts's\n * `AutomationTriggerType` union, 7 values). Everything else in `TRIGGER_TYPES` is either inert (accepted,\n * saved, never fires) or intentionally manual-only. */\nexport const AUTOMATIC_TRIGGER_TYPES: ReadonlySet<TriggerType> = new Set([\n 'WORK_ITEM_CREATED',\n 'WORK_ITEM_STATUS_CHANGED',\n 'WORK_ITEM_ASSIGNED',\n 'SPRINT_STARTED',\n 'SPRINT_COMPLETED',\n 'RELEASE_RELEASED',\n 'SCHEDULE',\n]);\n\n/** Accepted and saved (so a rule can be prepared ahead of a future engine change) but never fires on its own β€”\n * no dispatcher call site exists yet for these (automation-engine.ts's header). Distinct from `MANUAL`, which\n * is deliberately manual-only rather than an unfinished trigger. */\nexport const INERT_TRIGGER_TYPES: ReadonlySet<TriggerType> = new Set([\n 'WORK_ITEM_UPDATED',\n 'MILESTONE_DUE',\n 'WEBHOOK',\n]);\n\n/** Fires automatically but its `TriggerData` never carries a `workItemId` (sprint.service.ts / release.service.ts\n * build it without one) β€” so no work-item action can ever dispatch for it. */\nexport const NO_WORK_ITEM_TRIGGER_TYPES: ReadonlySet<TriggerType> = new Set([\n 'SPRINT_STARTED',\n 'SPRINT_COMPLETED',\n 'RELEASE_RELEASED',\n]);\n\n/** The trigger-select option suffix β€” mirrors what the rule can actually do, not just whether it is automatic. */\nexport function triggerOptionSuffix(trigger: TriggerType): string {\n if (trigger === 'SCHEDULE') return ' (runs daily)';\n if (NO_WORK_ITEM_TRIGGER_TYPES.has(trigger)) return ' (no target work item)';\n if (INERT_TRIGGER_TYPES.has(trigger)) return ' (not yet automatic)';\n if (trigger === 'MANUAL') return ' (run manually)';\n return '';\n}\n\n/** The explanatory note shown under the trigger select β€” `null` for a trigger that needs no caveat (an\n * ordinary automatic, work-item-bearing trigger). Rendered with `data-testid=\"automation-trigger-hint\"`. */\nexport function triggerHint(trigger: TriggerType): string | null {\n if (NO_WORK_ITEM_TRIGGER_TYPES.has(trigger)) {\n return 'This trigger fires automatically, but it has no target work item β€” only \"Post to a channel\" could apply, and that is not available yet (waiting on a platform change). A rule with this trigger cannot do anything right now, so it cannot be saved yet.';\n }\n if (trigger === 'SCHEDULE') {\n return 'Runs once a day against every work item matching the statuses and staleness window below, and acts on each matching item at most once per day.';\n }\n if (INERT_TRIGGER_TYPES.has(trigger)) {\n return 'This trigger is saved but does not fire on its own yet β€” use Run now to run it against a work item.';\n }\n if (trigger === 'MANUAL') {\n return 'Manual rules never fire on their own β€” use Run now to run this rule against a work item.';\n }\n return null;\n}\n\nexport const RULE_STATUSES = ['ACTIVE', 'PAUSED', 'DISABLED', 'ERROR'] as const;\nexport type RuleStatus = (typeof RULE_STATUSES)[number];\n\nexport const ACTION_TYPES = [\n 'ASSIGN_TO_USER',\n 'ADD_LABEL',\n 'SET_STATUS',\n 'SET_PRIORITY',\n 'POST_COMMENT',\n 'POST_TO_CHANNEL',\n] as const;\nexport type ActionType = (typeof ACTION_TYPES)[number];\n\nexport const ACTION_TYPE_LABEL: Record<ActionType, string> = {\n ASSIGN_TO_USER: 'Assign to user',\n ADD_LABEL: 'Add label',\n SET_STATUS: 'Set status',\n SET_PRIORITY: 'Set priority',\n POST_COMMENT: 'Post a comment',\n POST_TO_CHANNEL: 'Post to a channel',\n};\n\n/** Every action that mutates/comments on the triggering WORK ITEM (automation-engine.ts's `dispatchAction`: only\n * `POST_TO_CHANNEL` is handled without one). These are the actions `NO_WORK_ITEM_TRIGGER_TYPES` cannot run. */\nexport const WORK_ITEM_ACTION_TYPES: ReadonlySet<ActionType> = new Set([\n 'ASSIGN_TO_USER',\n 'ADD_LABEL',\n 'SET_STATUS',\n 'SET_PRIORITY',\n 'POST_COMMENT',\n]);\n\n/** `POST_TO_CHANNEL` is gated off (BOFF-8240) β€” mirrors `isChannelPostingEnabled()` in automation-engine.ts,\n * which this frontend has no way to read directly (it is a server env flag). Flip this only once that ships\n * AND the option below is meant to become selectable β€” until then it stays visible but disabled everywhere. */\nexport const CHANNEL_POSTING_AVAILABLE = false;\n/** Short reason shown on the disabled option itself. */\nexport const CHANNEL_POSTING_OPTION_REASON = 'Not available yet β€” waiting on a platform change';\n/** The engine's own refusal message (`automation.errors.channelPostingUnavailable`), reused so a blocked save\n * never contradicts what the server would say if the block were somehow bypassed. */\nexport const CHANNEL_POSTING_BLOCKED_MESSAGE =\n 'Posting to a chat channel from an automation is not available yet.';\n\nexport interface RuleAction {\n type: ActionType;\n value: string;\n}\n\nexport const CONDITION_OPERATORS = [\n 'eq',\n 'neq',\n 'in',\n 'notIn',\n 'contains',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'exists',\n] as const;\nexport type ConditionOperator = (typeof CONDITION_OPERATORS)[number];\n\nexport const CONDITION_OPERATOR_LABEL: Record<ConditionOperator, string> = {\n eq: 'is',\n neq: 'is not',\n in: 'is one of',\n notIn: 'is not one of',\n contains: 'contains',\n gt: '>',\n gte: '>=',\n lt: '<',\n lte: '<=',\n exists: 'is set',\n};\n\n/** Fields present on every automatic trigger's snapshot (`TriggerData` in automation-engine.ts). Only meaningful\n * for non-`SCHEDULE` triggers β€” `SCHEDULE` has its own dedicated condition shape, see `ScheduleConditions` below. */\nexport const CONDITION_FIELDS = [\n 'status',\n 'previousStatus',\n 'priority',\n 'type',\n 'assigneeId',\n 'title',\n 'labels',\n 'sprintId',\n 'projectId',\n] as const;\n\nexport interface RuleCondition {\n field: string;\n operator: ConditionOperator;\n /** Raw editor value; for `in`/`notIn` this is a comma-separated list, converted to an array on submit. */\n value: string;\n}\n\n/** actions[] (form shape) -> actions JSON the API accepts. Never emits a `POST_TO_CHANNEL` action β€” nothing in\n * this builder can produce one while `CHANNEL_POSTING_AVAILABLE` is false (see `saveBlockReason`, which refuses\n * to save a form that somehow still carries one). */\nexport function actionsToApi(actions: RuleAction[]): { type: ActionType; value: string }[] {\n return actions\n .filter((a) => a.value.trim() !== '')\n .map((a) => ({ type: a.type, value: a.value.trim() }));\n}\n\n/** conditions[] (form shape) -> conditions JSON the API accepts, or undefined when there are none. Only used for\n * non-`SCHEDULE` triggers β€” see `scheduleConditionsToApi` for `SCHEDULE`'s own shape. */\nexport function conditionsToApi(\n conditions: RuleCondition[]\n): { field: string; operator: ConditionOperator; value?: unknown }[] | undefined {\n const usable = conditions.filter((c) => c.field.trim() !== '');\n if (usable.length === 0) return undefined;\n return usable.map((c) => {\n if (c.operator === 'exists') return { field: c.field.trim(), operator: c.operator };\n if (c.operator === 'in' || c.operator === 'notIn') {\n return {\n field: c.field.trim(),\n operator: c.operator,\n value: c.value\n .split(',')\n .map((v) => v.trim())\n .filter(Boolean),\n };\n }\n return { field: c.field.trim(), operator: c.operator, value: c.value };\n });\n}\n\n/** API conditions JSON -> form shape, for the edit page. Unrecognised shapes are dropped rather than crashing the form. */\nexport function conditionsFromApi(raw: unknown): RuleCondition[] {\n if (!Array.isArray(raw)) return [];\n return raw\n .filter(\n (v): v is { field: string; operator: string; value?: unknown } =>\n !!v && typeof v === 'object' && typeof (v as { field?: unknown }).field === 'string'\n )\n .map((c) => ({\n field: c.field,\n operator: (CONDITION_OPERATORS as readonly string[]).includes(c.operator)\n ? (c.operator as ConditionOperator)\n : 'eq',\n value: Array.isArray(c.value) ? c.value.join(', ') : c.value == null ? '' : String(c.value),\n }));\n}\n\n/** API actions JSON -> form shape, for the edit page. Action types this builder does not offer are kept (so an\n * existing rule's own configuration is never silently dropped by opening the edit page) but shown as read-only. */\nexport function actionsFromApi(raw: unknown): RuleAction[] {\n if (!Array.isArray(raw)) return [];\n return raw\n .filter(\n (v): v is { type: string; value?: unknown } =>\n !!v && typeof v === 'object' && typeof (v as { type?: unknown }).type === 'string'\n )\n .map((a) => ({ type: a.type as ActionType, value: a.value == null ? '' : String(a.value) }));\n}\n\n// ============================================================================\n// SCHEDULE conditions (AUT-27) β€” a DIFFERENT shape from the field/operator/value array above.\n//\n// automation-scheduler.ts's `parseScheduleConditions` reads a SCHEDULE rule's `conditions` as the plain object\n// `{ status: string[] (non-empty), notUpdatedForDays: number > 0 }`: which statuses are candidates, and how many\n// days since a matching item last changed before the rule considers it stale. Anything else (missing, an empty\n// status list, a non-positive/non-numeric window) makes the scheduler skip the rule silently every tick β€” so this\n// form gates Save on the same shape being valid, rather than letting a SCHEDULE rule be saved in a way that never\n// runs.\n// ============================================================================\n\nexport interface ScheduleConditions {\n status: string[];\n notUpdatedForDays: number;\n}\n\n/** A whole number of days, 1 or more β€” a stricter (but compatible) subset of the engine's own `number > 0` check,\n * chosen because \"half a day\" has no sensible UI meaning here. */\nexport function parseScheduleDays(raw: string): number | null {\n const trimmed = raw.trim();\n if (!/^\\d+$/.test(trimmed)) return null;\n const n = Number(trimmed);\n return n >= 1 ? n : null;\n}\n\n/** API `conditions` JSON -> the SCHEDULE editor's own form fields. Lenient (shows whatever is there, even if it\n * would fail `parseScheduleConditions` server-side) so an existing malformed rule can still be opened and fixed,\n * rather than silently reset. */\nexport function scheduleConditionsFromApi(raw: unknown): { statuses: string[]; days: string } {\n if (!raw || typeof raw !== 'object') return { statuses: [], days: '' };\n const status = (raw as Record<string, unknown>).status;\n const notUpdatedForDays = (raw as Record<string, unknown>).notUpdatedForDays;\n const statuses = Array.isArray(status)\n ? status.filter((s): s is string => typeof s === 'string')\n : [];\n const days =\n typeof notUpdatedForDays === 'number' && Number.isFinite(notUpdatedForDays)\n ? String(notUpdatedForDays)\n : '';\n return { statuses, days };\n}\n\n/** The SCHEDULE editor's own form fields -> exactly the `{ status, notUpdatedForDays }` shape\n * `parseScheduleConditions` reads. `undefined` when not yet valid β€” callers gate Save on this instead of ever\n * submitting an unreadable value. */\nexport function scheduleConditionsToApi(\n statuses: string[],\n daysText: string\n): ScheduleConditions | undefined {\n const days = parseScheduleDays(daysText);\n if (statuses.length === 0 || days === null) return undefined;\n return { status: statuses, notUpdatedForDays: days };\n}\n\nexport function isScheduleConditionsValid(statuses: string[], daysText: string): boolean {\n return statuses.length > 0 && parseScheduleDays(daysText) !== null;\n}\n\n// ============================================================================\n// POST_COMMENT / POST_TO_CHANNEL templating β€” mirrors automation-engine.ts's `renderAutomationTemplate` +\n// `buildAutomationTemplateValues` for a client-side PREVIEW only. The server re-renders for real at dispatch\n// time against the live trigger data; this never substitutes for that, it only shows what the template would\n// look like with representative sample values.\n// ============================================================================\n\n/** `AUTOMATION_TEMPLATE_MAX_LENGTH` in automation-engine.ts β€” the longest a template's raw `value` may be,\n * checked at rule create/update. */\nexport const AUTOMATION_TEMPLATE_MAX_LENGTH = 2_000;\n/** `POST_COMMENT_MAX_RENDERED_LENGTH` in automation-engine.ts β€” the longest a RENDERED comment body may be. */\nexport const POST_COMMENT_MAX_RENDERED_LENGTH = 2_000;\n/** `POST_TO_CHANNEL_MAX_RENDERED_LENGTH` in automation-engine.ts β€” shorter, matching a chat message. */\nexport const POST_TO_CHANNEL_MAX_RENDERED_LENGTH = 500;\n\nconst TEMPLATE_PLACEHOLDER_RE = /\\{\\{\\s*([a-zA-Z][a-zA-Z0-9_.]*)\\s*\\}\\}/g;\n\n/** Mirrors `escapeAutomationTemplateValue` in automation-engine.ts: a substituted value never carries a raw\n * control character or the exact bracket/paren characters CWF-13's `@[Name](actorId)` mention token uses. */\nfunction escapeTemplateValue(value: string): string {\n let out = '';\n for (const ch of value) {\n const code = ch.codePointAt(0) ?? 0;\n out +=\n code <= 0x1f || code === 0x7f || ch === '[' || ch === ']' || ch === '(' || ch === ')'\n ? ' '\n : ch;\n }\n return out.trim();\n}\n\n/** Mirrors `renderAutomationTemplate` in automation-engine.ts: an unknown or unpopulated `{{key}}` renders empty,\n * the result is trimmed, and it is truncated (with an ellipsis) to `maxLength`. */\nexport function renderTemplatePreview(\n template: string,\n values: Record<string, string | undefined>,\n maxLength: number\n): string {\n const rendered = template.replace(TEMPLATE_PLACEHOLDER_RE, (_match, key: string) => {\n const raw = values[key];\n return raw === undefined ? '' : escapeTemplateValue(raw);\n });\n const trimmed = rendered.trim();\n if (trimmed.length <= maxLength) return trimmed;\n return `${trimmed.slice(0, Math.max(0, maxLength - 1))}…`;\n}\n\n/** Every placeholder token `buildAutomationTemplateValues` (automation-engine.ts) knows how to fill, with a\n * short label for the picker and a representative sample value for the preview. */\nexport const TEMPLATE_PLACEHOLDER_INFO: Record<string, { label: string; sample: string }> = {\n 'workItem.key': { label: 'Work item key', sample: 'PM-42' },\n 'workItem.title': { label: 'Work item title', sample: 'Fix checkout crash' },\n fromStatus: { label: 'Previous status', sample: 'In Progress' },\n toStatus: { label: 'New status', sample: 'Done' },\n actor: { label: 'Actor', sample: 'Jane Doe' },\n 'sprint.name': { label: 'Sprint name', sample: 'Sprint 14' },\n 'release.version': { label: 'Release version', sample: '2026.930.1' },\n 'release.name': { label: 'Release name', sample: 'September release' },\n};\n\n/**\n * Exactly which placeholders will actually render something (non-empty) for a POST_COMMENT/POST_TO_CHANNEL\n * template on a rule of this trigger type β€” read from `buildAutomationTemplateValues` plus every call site that\n * builds a `TriggerData` for it (`workitem.service.ts`'s `toTriggerData`/`fireAutomationTrigger`,\n * `sprint.service.ts`'s `fireSprintAutomationTrigger`, `release.service.ts`'s `fireReleaseAutomationTrigger`,\n * `automation-scheduler.ts`'s `toScheduleTriggerData`, and `automation.service.ts`'s `execute()` for \"Run now\",\n * which every inert/manual trigger relies on):\n *\n * - `SPRINT_STARTED`/`SPRINT_COMPLETED`: `{ projectId, sprintId, sprintName }` only β€” no work item, no status.\n * - `RELEASE_RELEASED`: `{ projectId, releaseId, releaseVersion, releaseName }` only β€” no work item, no status.\n * - `WORK_ITEM_STATUS_CHANGED`: the only automatic trigger that sets `previousStatus` (to the status before the\n * write) alongside the new `status` β€” every other trigger either omits it or sets it to `null`.\n * - `WORK_ITEM_CREATED`/`WORK_ITEM_ASSIGNED`/`SCHEDULE` carry the work item's key/title/status but never a\n * `fromStatus` β€” their real dispatch paths (`workitem.service.ts`'s `toTriggerData`, `automation-scheduler.ts`'s\n * `toScheduleTriggerData`) both set `key: item.key` from the live row.\n * - `WORK_ITEM_UPDATED`/`MILESTONE_DUE`/`WEBHOOK`/`MANUAL` are the same \"work item title/status, no fromStatus\"\n * shape MINUS `workItem.key`: `automation.service.ts#execute()` β€” the ONLY path these ever run through, since\n * none of the four has a real dispatcher β€” builds its `mergedTriggerData` from the FE's `{ workItemId }`\n * payload (`AutomationRuleControls.tsx`'s `RunAutomationRuleDialog`) plus a fresh `prisma.workItem` read, and\n * that object never assigns a `key` field. Offering `{{workItem.key}}` here would let someone build a template\n * that is GUARANTEED to render an empty string every single time it runs β€” not merely omitted when unset like\n * a normal \"no value for this trigger\" placeholder, but structurally unreachable for these four trigger types.\n *\n * `actor` is always populated (the real triggering actor, `AUTOMATION_SCHEDULER_ACTOR_ID` for SCHEDULE, or the\n * \"Run now\" caller) so it is available for every trigger.\n */\nexport function templatePlaceholdersForTrigger(trigger: TriggerType): readonly string[] {\n if (trigger === 'SPRINT_STARTED' || trigger === 'SPRINT_COMPLETED')\n return ['sprint.name', 'actor'];\n if (trigger === 'RELEASE_RELEASED') return ['release.version', 'release.name', 'actor'];\n if (trigger === 'WORK_ITEM_STATUS_CHANGED')\n return ['workItem.key', 'workItem.title', 'fromStatus', 'toStatus', 'actor'];\n if (INERT_TRIGGER_TYPES.has(trigger) || trigger === 'MANUAL')\n return ['workItem.title', 'toStatus', 'actor'];\n return ['workItem.key', 'workItem.title', 'toStatus', 'actor'];\n}\n\n/** Sample values for exactly the placeholders `templatePlaceholdersForTrigger` says apply to this trigger β€” any\n * other `{{...}}` in the template previews empty, matching what the real engine would do. */\nexport function templateSampleValues(trigger: TriggerType): Record<string, string | undefined> {\n const values: Record<string, string | undefined> = {};\n for (const token of templatePlaceholdersForTrigger(trigger)) {\n values[token] = TEMPLATE_PLACEHOLDER_INFO[token]?.sample;\n }\n return values;\n}\n\n// ============================================================================\n// Save gating β€” a rule this builder would otherwise let someone save but that can never do anything, or that\n// carries an action this engine refuses to dispatch.\n// ============================================================================\n\n/** `null` when this trigger needs no explanation blocking Save; the reason string otherwise (same text as\n * `triggerHint` for a `NO_WORK_ITEM_TRIGGER_TYPES` trigger β€” the one case that blocks Save outright). */\nexport function triggerSaveBlockReason(trigger: TriggerType): string | null {\n return NO_WORK_ITEM_TRIGGER_TYPES.has(trigger) ? triggerHint(trigger) : null;\n}\n\n/** Combines every reason a rule form cannot be saved yet: a trigger with no possible action, a `POST_TO_CHANNEL`\n * action while channel posting is gated off, or (for `SCHEDULE`) a not-yet-valid schedule window. `null` means\n * the form is save-able as far as these rules are concerned (the caller still checks name/project separately). */\nexport function saveBlockReason(form: {\n triggerType: TriggerType;\n actions: RuleAction[];\n scheduleStatuses: string[];\n scheduleNotUpdatedForDays: string;\n}): string | null {\n const triggerReason = triggerSaveBlockReason(form.triggerType);\n if (triggerReason) return triggerReason;\n if (!CHANNEL_POSTING_AVAILABLE && form.actions.some((a) => a.type === 'POST_TO_CHANNEL')) {\n return CHANNEL_POSTING_BLOCKED_MESSAGE;\n }\n if (\n form.triggerType === 'SCHEDULE' &&\n !isScheduleConditionsValid(form.scheduleStatuses, form.scheduleNotUpdatedForDays)\n ) {\n return 'Choose at least one status and a whole number of days (1 or more) before saving this schedule.';\n }\n return null;\n}\n\n// ============================================================================\n// Execution errors β€” automation-engine.ts's `errorMessage` is one `'; '`-joined string (each action's\n// `\"<label>: <message>\"`, plus an optional \"N declared action(s) are not supported…\" summary). There is no\n// structured per-action array in the API (`PlanmagnetAutomationExecution.result` only carries\n// `dispatched`/`skipped`/counts β€” see the schema), so this is the best available split of what the API returns.\n// ============================================================================\n\nexport function splitExecutionErrors(message: string | null | undefined): string[] {\n if (!message) return [];\n return message\n .split('; ')\n .map((s) => s.trim())\n .filter(Boolean);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"automationRuleTypes.js","names":[],"sources":["../../../src/planmagnet/automation/automationRuleTypes.ts"],"sourcesContent":["/**\n * Automation rules (BOFF-8047). Shared trigger/condition/action vocabulary, kept in one place so the create\n * page, the edit page and the list's summaries can never drift from each other or from the engine.\n *\n * Scope note, rewritten from the real engine (wspace-planmagnet-svc `src/modules/automation/services/\n * automation-engine.ts` + `automation-scheduler.ts`, both merged and prod-verified β€” svc PR #64), and updated\n * for AUT-27/35 against the Hive-refreshed schema (`graphql/wspace.graphql`'s `PlanmagnetAutomationTriggerType`\n * doc comments): `WORK_ITEM_UPDATED` and `MILESTONE_DUE` are now REAL dispatchers too, not inert placeholders.\n *\n * - Triggers that fire AUTOMATICALLY: `WORK_ITEM_CREATED`, `WORK_ITEM_STATUS_CHANGED`, `WORK_ITEM_ASSIGNED`\n * (from every WorkItem write that changes status/assignee), `WORK_ITEM_UPDATED` (AUT-27: any field edit\n * through `WorkItemService.update()`'s general path β€” fires ALONGSIDE `WORK_ITEM_STATUS_CHANGED`/\n * `WORK_ITEM_ASSIGNED` on an edit that also touches status/assignee, not instead of them; the dispatcher does\n * not diff old vs new, so a save that writes back identical values still fires it β€” see `triggerHint`),\n * `SPRINT_STARTED`/`SPRINT_COMPLETED` (a sprint transition), `RELEASE_RELEASED` (a release's real transition\n * into RELEASED β€” not a re-send), `MILESTONE_DUE` (AUT-27: a rule's own Milestone falls due today, checked\n * once per `automation-scheduler.ts` tick β€” same cadence as `SCHEDULE`, no extra condition shape of its own)\n * and `SCHEDULE` (a daily background scan β€” see `automation-scheduler.ts`, NOT dispatched from a write path\n * at all). Still a real, accepted, saved schema value but INERT β€” never fires on its own β€” is `WEBHOOK`,\n * pending the public-gateway route (BOFF-8238). `MANUAL` is not inert, it is intentionally manual-only: it\n * runs only via \"Run now\" (`planmagnetExecuteAutomationRule`), which works for a rule of ANY trigger type\n * because it targets one named work item directly instead of listening for an event.\n * - Dispatchable actions: `ASSIGN_TO_USER`, `ADD_LABEL`, `SET_STATUS`, `SET_PRIORITY` (the four WorkItem\n * fields an automation can mutate), `POST_COMMENT` (a templated comment on the triggering work item,\n * authored by the fixed `automation:engine` identity β€” see `AUTOMATION_ACTOR_NAMES` in `../people/People`)\n * and `POST_TO_CHANNEL` (a templated message to a Conversations channel) β€” but `POST_TO_CHANNEL` is gated\n * OFF in prod (`isChannelPostingEnabled()` in the engine) until Platform ticket BOFF-8240 ships: saving one\n * is refused server-side with \"Posting to a chat channel from an automation is not available yet\", so this\n * UI never lets it be selected or saved either (see `CHANNEL_POSTING_AVAILABLE` below).\n * - `SPRINT_STARTED`/`SPRINT_COMPLETED`/`RELEASE_RELEASED` carry NO target work item (`sprint.service.ts`/\n * `release.service.ts` never set `workItemId` on their `TriggerData`), so every work-item action β€” all five\n * above except `POST_TO_CHANNEL` β€” cannot run for them. Today that leaves such a rule able to do nothing\n * (the only action that *could* apply, `POST_TO_CHANNEL`, is itself gated off), so the builder below refuses\n * to offer work-item actions for these triggers and blocks Save entirely rather than let someone build a\n * rule that silently never fires anything.\n */\n\nexport const TRIGGER_TYPES = [\n 'WORK_ITEM_CREATED',\n 'WORK_ITEM_STATUS_CHANGED',\n 'WORK_ITEM_ASSIGNED',\n 'SPRINT_STARTED',\n 'SPRINT_COMPLETED',\n 'RELEASE_RELEASED',\n 'SCHEDULE',\n 'WORK_ITEM_UPDATED',\n 'MILESTONE_DUE',\n 'WEBHOOK',\n 'MANUAL',\n] as const;\nexport type TriggerType = (typeof TRIGGER_TYPES)[number];\n\nexport const TRIGGER_TYPE_LABEL: Record<TriggerType, string> = {\n WORK_ITEM_CREATED: 'Work item created',\n WORK_ITEM_STATUS_CHANGED: 'Work item status changed',\n WORK_ITEM_ASSIGNED: 'Work item assigned',\n SPRINT_STARTED: 'Sprint started',\n SPRINT_COMPLETED: 'Sprint completed',\n RELEASE_RELEASED: 'Release shipped',\n SCHEDULE: 'Schedule',\n WORK_ITEM_UPDATED: 'Work item updated',\n MILESTONE_DUE: 'Milestone due',\n WEBHOOK: 'Webhook',\n MANUAL: 'Manual only',\n};\n\n/** Looks up a trigger type's label; falls back to the raw value for one this UI does not yet know (forward\n * compatible with a value the service adds later). Cast: `TRIGGER_TYPE_LABEL` is keyed by the known union, but\n * a value read back from the API is a plain string. */\nexport function triggerTypeLabel(value: string): string {\n return (TRIGGER_TYPE_LABEL as Record<string, string>)[value] ?? value;\n}\n\n/** Fires on its own β€” dispatched from a real write path or a daily scheduler tick (automation-engine.ts's\n * `AutomationTriggerType` union, now 9 values as of AUT-27/35). Everything else in `TRIGGER_TYPES` is either\n * inert (accepted, saved, never fires β€” `WEBHOOK`) or intentionally manual-only (`MANUAL`). */\nexport const AUTOMATIC_TRIGGER_TYPES: ReadonlySet<TriggerType> = new Set([\n 'WORK_ITEM_CREATED',\n 'WORK_ITEM_STATUS_CHANGED',\n 'WORK_ITEM_ASSIGNED',\n 'WORK_ITEM_UPDATED',\n 'SPRINT_STARTED',\n 'SPRINT_COMPLETED',\n 'RELEASE_RELEASED',\n 'MILESTONE_DUE',\n 'SCHEDULE',\n]);\n\n/** Accepted and saved (so a rule can be prepared ahead of a future engine change) but never fires on its own β€”\n * no dispatcher call site exists yet for this value (automation-engine.ts's header). Distinct from `MANUAL`,\n * which is deliberately manual-only rather than an unfinished trigger. AUT-27/35: `WORK_ITEM_UPDATED` and\n * `MILESTONE_DUE` graduated out of this set once their real dispatchers shipped β€” see `AUTOMATIC_TRIGGER_TYPES`\n * and this file's header comment. */\nexport const INERT_TRIGGER_TYPES: ReadonlySet<TriggerType> = new Set(['WEBHOOK']);\n\n/** Fires automatically but its `TriggerData` never carries a `workItemId` (sprint.service.ts / release.service.ts\n * build it without one) β€” so no work-item action can ever dispatch for it. */\nexport const NO_WORK_ITEM_TRIGGER_TYPES: ReadonlySet<TriggerType> = new Set([\n 'SPRINT_STARTED',\n 'SPRINT_COMPLETED',\n 'RELEASE_RELEASED',\n]);\n\n/** The trigger-select option suffix β€” mirrors what the rule can actually do, not just whether it is automatic. */\nexport function triggerOptionSuffix(trigger: TriggerType): string {\n if (trigger === 'SCHEDULE' || trigger === 'MILESTONE_DUE') return ' (runs daily)';\n if (NO_WORK_ITEM_TRIGGER_TYPES.has(trigger)) return ' (no target work item)';\n if (INERT_TRIGGER_TYPES.has(trigger)) return ' (not yet automatic)';\n if (trigger === 'MANUAL') return ' (run manually)';\n return '';\n}\n\n/** The explanatory note shown under the trigger select β€” `null` for a trigger that needs no caveat (an\n * ordinary automatic, work-item-bearing trigger). Rendered with `data-testid=\"automation-trigger-hint\"`. */\nexport function triggerHint(trigger: TriggerType): string | null {\n if (NO_WORK_ITEM_TRIGGER_TYPES.has(trigger)) {\n return 'This trigger fires automatically, but it has no target work item β€” only \"Post to a channel\" could apply, and that is not available yet (waiting on a platform change). A rule with this trigger cannot do anything right now, so it cannot be saved yet.';\n }\n if (trigger === 'SCHEDULE') {\n return 'Runs once a day against every work item matching the statuses and staleness window below, and acts on each matching item at most once per day.';\n }\n if (trigger === 'WORK_ITEM_UPDATED') {\n return 'Fires on any field edit, including a save that writes back exactly the values it already had β€” the dispatcher does not check whether anything actually changed. Fires alongside \"Work item status changed\" / \"Work item assigned\" on an edit that also touches status or assignee, not instead of them.';\n }\n if (trigger === 'MILESTONE_DUE') {\n return \"Runs once a day for a rule whose Milestone's due date is today β€” same daily check as Schedule, but with no extra condition shape of its own: use the conditions below as you would for any other trigger.\";\n }\n if (INERT_TRIGGER_TYPES.has(trigger)) {\n return 'This trigger is saved but does not fire on its own yet β€” use Run now to run it against a work item.';\n }\n if (trigger === 'MANUAL') {\n return 'Manual rules never fire on their own β€” use Run now to run this rule against a work item.';\n }\n return null;\n}\n\nexport const RULE_STATUSES = ['ACTIVE', 'PAUSED', 'DISABLED', 'ERROR'] as const;\nexport type RuleStatus = (typeof RULE_STATUSES)[number];\n\nexport const ACTION_TYPES = [\n 'ASSIGN_TO_USER',\n 'ADD_LABEL',\n 'SET_STATUS',\n 'SET_PRIORITY',\n 'POST_COMMENT',\n 'POST_TO_CHANNEL',\n] as const;\nexport type ActionType = (typeof ACTION_TYPES)[number];\n\nexport const ACTION_TYPE_LABEL: Record<ActionType, string> = {\n ASSIGN_TO_USER: 'Assign to user',\n ADD_LABEL: 'Add label',\n SET_STATUS: 'Set status',\n SET_PRIORITY: 'Set priority',\n POST_COMMENT: 'Post a comment',\n POST_TO_CHANNEL: 'Post to a channel',\n};\n\n/** Every action that mutates/comments on the triggering WORK ITEM (automation-engine.ts's `dispatchAction`: only\n * `POST_TO_CHANNEL` is handled without one). These are the actions `NO_WORK_ITEM_TRIGGER_TYPES` cannot run. */\nexport const WORK_ITEM_ACTION_TYPES: ReadonlySet<ActionType> = new Set([\n 'ASSIGN_TO_USER',\n 'ADD_LABEL',\n 'SET_STATUS',\n 'SET_PRIORITY',\n 'POST_COMMENT',\n]);\n\n/** `POST_TO_CHANNEL` is gated off (BOFF-8240) β€” mirrors `isChannelPostingEnabled()` in automation-engine.ts,\n * which this frontend has no way to read directly (it is a server env flag). Flip this only once that ships\n * AND the option below is meant to become selectable β€” until then it stays visible but disabled everywhere. */\nexport const CHANNEL_POSTING_AVAILABLE = false;\n/** Short reason shown on the disabled option itself. */\nexport const CHANNEL_POSTING_OPTION_REASON = 'Not available yet β€” waiting on a platform change';\n/** The engine's own refusal message (`automation.errors.channelPostingUnavailable`), reused so a blocked save\n * never contradicts what the server would say if the block were somehow bypassed. */\nexport const CHANNEL_POSTING_BLOCKED_MESSAGE =\n 'Posting to a chat channel from an automation is not available yet.';\n\nexport interface RuleAction {\n type: ActionType;\n value: string;\n}\n\nexport const CONDITION_OPERATORS = [\n 'eq',\n 'neq',\n 'in',\n 'notIn',\n 'contains',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'exists',\n] as const;\nexport type ConditionOperator = (typeof CONDITION_OPERATORS)[number];\n\nexport const CONDITION_OPERATOR_LABEL: Record<ConditionOperator, string> = {\n eq: 'is',\n neq: 'is not',\n in: 'is one of',\n notIn: 'is not one of',\n contains: 'contains',\n gt: '>',\n gte: '>=',\n lt: '<',\n lte: '<=',\n exists: 'is set',\n};\n\n/** Fields present on every automatic trigger's snapshot (`TriggerData` in automation-engine.ts). Only meaningful\n * for non-`SCHEDULE` triggers β€” `SCHEDULE` has its own dedicated condition shape, see `ScheduleConditions` below. */\nexport const CONDITION_FIELDS = [\n 'status',\n 'previousStatus',\n 'priority',\n 'type',\n 'assigneeId',\n 'title',\n 'labels',\n 'sprintId',\n 'projectId',\n] as const;\n\nexport interface RuleCondition {\n field: string;\n operator: ConditionOperator;\n /** Raw editor value; for `in`/`notIn` this is a comma-separated list, converted to an array on submit. */\n value: string;\n}\n\n/** actions[] (form shape) -> actions JSON the API accepts. Never emits a `POST_TO_CHANNEL` action β€” nothing in\n * this builder can produce one while `CHANNEL_POSTING_AVAILABLE` is false (see `saveBlockReason`, which refuses\n * to save a form that somehow still carries one). */\nexport function actionsToApi(actions: RuleAction[]): { type: ActionType; value: string }[] {\n return actions\n .filter((a) => a.value.trim() !== '')\n .map((a) => ({ type: a.type, value: a.value.trim() }));\n}\n\n/** conditions[] (form shape) -> conditions JSON the API accepts, or undefined when there are none. Only used for\n * non-`SCHEDULE` triggers β€” see `scheduleConditionsToApi` for `SCHEDULE`'s own shape. */\nexport function conditionsToApi(\n conditions: RuleCondition[]\n): { field: string; operator: ConditionOperator; value?: unknown }[] | undefined {\n const usable = conditions.filter((c) => c.field.trim() !== '');\n if (usable.length === 0) return undefined;\n return usable.map((c) => {\n if (c.operator === 'exists') return { field: c.field.trim(), operator: c.operator };\n if (c.operator === 'in' || c.operator === 'notIn') {\n return {\n field: c.field.trim(),\n operator: c.operator,\n value: c.value\n .split(',')\n .map((v) => v.trim())\n .filter(Boolean),\n };\n }\n return { field: c.field.trim(), operator: c.operator, value: c.value };\n });\n}\n\n/** API conditions JSON -> form shape, for the edit page. Unrecognised shapes are dropped rather than crashing the form. */\nexport function conditionsFromApi(raw: unknown): RuleCondition[] {\n if (!Array.isArray(raw)) return [];\n return raw\n .filter(\n (v): v is { field: string; operator: string; value?: unknown } =>\n !!v && typeof v === 'object' && typeof (v as { field?: unknown }).field === 'string'\n )\n .map((c) => ({\n field: c.field,\n operator: (CONDITION_OPERATORS as readonly string[]).includes(c.operator)\n ? (c.operator as ConditionOperator)\n : 'eq',\n value: Array.isArray(c.value) ? c.value.join(', ') : c.value == null ? '' : String(c.value),\n }));\n}\n\n/** API actions JSON -> form shape, for the edit page. Action types this builder does not offer are kept (so an\n * existing rule's own configuration is never silently dropped by opening the edit page) but shown as read-only. */\nexport function actionsFromApi(raw: unknown): RuleAction[] {\n if (!Array.isArray(raw)) return [];\n return raw\n .filter(\n (v): v is { type: string; value?: unknown } =>\n !!v && typeof v === 'object' && typeof (v as { type?: unknown }).type === 'string'\n )\n .map((a) => ({ type: a.type as ActionType, value: a.value == null ? '' : String(a.value) }));\n}\n\n// ============================================================================\n// SCHEDULE conditions (AUT-27) β€” a DIFFERENT shape from the field/operator/value array above.\n//\n// automation-scheduler.ts's `parseScheduleConditions` reads a SCHEDULE rule's `conditions` as the plain object\n// `{ status: string[] (non-empty), notUpdatedForDays: number > 0 }`: which statuses are candidates, and how many\n// days since a matching item last changed before the rule considers it stale. Anything else (missing, an empty\n// status list, a non-positive/non-numeric window) makes the scheduler skip the rule silently every tick β€” so this\n// form gates Save on the same shape being valid, rather than letting a SCHEDULE rule be saved in a way that never\n// runs.\n// ============================================================================\n\nexport interface ScheduleConditions {\n status: string[];\n notUpdatedForDays: number;\n}\n\n/** A whole number of days, 1 or more β€” a stricter (but compatible) subset of the engine's own `number > 0` check,\n * chosen because \"half a day\" has no sensible UI meaning here. */\nexport function parseScheduleDays(raw: string): number | null {\n const trimmed = raw.trim();\n if (!/^\\d+$/.test(trimmed)) return null;\n const n = Number(trimmed);\n return n >= 1 ? n : null;\n}\n\n/** API `conditions` JSON -> the SCHEDULE editor's own form fields. Lenient (shows whatever is there, even if it\n * would fail `parseScheduleConditions` server-side) so an existing malformed rule can still be opened and fixed,\n * rather than silently reset. */\nexport function scheduleConditionsFromApi(raw: unknown): { statuses: string[]; days: string } {\n if (!raw || typeof raw !== 'object') return { statuses: [], days: '' };\n const status = (raw as Record<string, unknown>).status;\n const notUpdatedForDays = (raw as Record<string, unknown>).notUpdatedForDays;\n const statuses = Array.isArray(status)\n ? status.filter((s): s is string => typeof s === 'string')\n : [];\n const days =\n typeof notUpdatedForDays === 'number' && Number.isFinite(notUpdatedForDays)\n ? String(notUpdatedForDays)\n : '';\n return { statuses, days };\n}\n\n/** The SCHEDULE editor's own form fields -> exactly the `{ status, notUpdatedForDays }` shape\n * `parseScheduleConditions` reads. `undefined` when not yet valid β€” callers gate Save on this instead of ever\n * submitting an unreadable value. */\nexport function scheduleConditionsToApi(\n statuses: string[],\n daysText: string\n): ScheduleConditions | undefined {\n const days = parseScheduleDays(daysText);\n if (statuses.length === 0 || days === null) return undefined;\n return { status: statuses, notUpdatedForDays: days };\n}\n\nexport function isScheduleConditionsValid(statuses: string[], daysText: string): boolean {\n return statuses.length > 0 && parseScheduleDays(daysText) !== null;\n}\n\n// ============================================================================\n// POST_COMMENT / POST_TO_CHANNEL templating β€” mirrors automation-engine.ts's `renderAutomationTemplate` +\n// `buildAutomationTemplateValues` for a client-side PREVIEW only. The server re-renders for real at dispatch\n// time against the live trigger data; this never substitutes for that, it only shows what the template would\n// look like with representative sample values.\n// ============================================================================\n\n/** `AUTOMATION_TEMPLATE_MAX_LENGTH` in automation-engine.ts β€” the longest a template's raw `value` may be,\n * checked at rule create/update. */\nexport const AUTOMATION_TEMPLATE_MAX_LENGTH = 2_000;\n/** `POST_COMMENT_MAX_RENDERED_LENGTH` in automation-engine.ts β€” the longest a RENDERED comment body may be. */\nexport const POST_COMMENT_MAX_RENDERED_LENGTH = 2_000;\n/** `POST_TO_CHANNEL_MAX_RENDERED_LENGTH` in automation-engine.ts β€” shorter, matching a chat message. */\nexport const POST_TO_CHANNEL_MAX_RENDERED_LENGTH = 500;\n\nconst TEMPLATE_PLACEHOLDER_RE = /\\{\\{\\s*([a-zA-Z][a-zA-Z0-9_.]*)\\s*\\}\\}/g;\n\n/** Mirrors `escapeAutomationTemplateValue` in automation-engine.ts: a substituted value never carries a raw\n * control character or the exact bracket/paren characters CWF-13's `@[Name](actorId)` mention token uses. */\nfunction escapeTemplateValue(value: string): string {\n let out = '';\n for (const ch of value) {\n const code = ch.codePointAt(0) ?? 0;\n out +=\n code <= 0x1f || code === 0x7f || ch === '[' || ch === ']' || ch === '(' || ch === ')'\n ? ' '\n : ch;\n }\n return out.trim();\n}\n\n/** Mirrors `renderAutomationTemplate` in automation-engine.ts: an unknown or unpopulated `{{key}}` renders empty,\n * the result is trimmed, and it is truncated (with an ellipsis) to `maxLength`. */\nexport function renderTemplatePreview(\n template: string,\n values: Record<string, string | undefined>,\n maxLength: number\n): string {\n const rendered = template.replace(TEMPLATE_PLACEHOLDER_RE, (_match, key: string) => {\n const raw = values[key];\n return raw === undefined ? '' : escapeTemplateValue(raw);\n });\n const trimmed = rendered.trim();\n if (trimmed.length <= maxLength) return trimmed;\n return `${trimmed.slice(0, Math.max(0, maxLength - 1))}…`;\n}\n\n/** Every placeholder token `buildAutomationTemplateValues` (automation-engine.ts) knows how to fill, with a\n * short label for the picker and a representative sample value for the preview. */\nexport const TEMPLATE_PLACEHOLDER_INFO: Record<string, { label: string; sample: string }> = {\n 'workItem.key': { label: 'Work item key', sample: 'PM-42' },\n 'workItem.title': { label: 'Work item title', sample: 'Fix checkout crash' },\n fromStatus: { label: 'Previous status', sample: 'In Progress' },\n toStatus: { label: 'New status', sample: 'Done' },\n actor: { label: 'Actor', sample: 'Jane Doe' },\n 'sprint.name': { label: 'Sprint name', sample: 'Sprint 14' },\n 'release.version': { label: 'Release version', sample: '2026.930.1' },\n 'release.name': { label: 'Release name', sample: 'September release' },\n};\n\n/**\n * Exactly which placeholders will actually render something (non-empty) for a POST_COMMENT/POST_TO_CHANNEL\n * template on a rule of this trigger type β€” read from `buildAutomationTemplateValues` plus every call site that\n * builds a `TriggerData` for it (`workitem.service.ts`'s `toTriggerData`/`fireAutomationTrigger`,\n * `sprint.service.ts`'s `fireSprintAutomationTrigger`, `release.service.ts`'s `fireReleaseAutomationTrigger`,\n * `automation-scheduler.ts`'s `toScheduleTriggerData`, and `automation.service.ts`'s `execute()` for \"Run now\",\n * which every inert/manual trigger relies on):\n *\n * - `SPRINT_STARTED`/`SPRINT_COMPLETED`: `{ projectId, sprintId, sprintName }` only β€” no work item, no status.\n * - `RELEASE_RELEASED`: `{ projectId, releaseId, releaseVersion, releaseName }` only β€” no work item, no status.\n * - `WORK_ITEM_STATUS_CHANGED`: the only automatic trigger that sets `previousStatus` (to the status before the\n * write) alongside the new `status` β€” every other trigger either omits it or sets it to `null`.\n * - `WORK_ITEM_CREATED`/`WORK_ITEM_ASSIGNED`/`SCHEDULE` carry the work item's key/title/status but never a\n * `fromStatus` β€” their real dispatch paths (`workitem.service.ts`'s `toTriggerData`, `automation-scheduler.ts`'s\n * `toScheduleTriggerData`) both set `key: item.key` from the live row.\n * - `WORK_ITEM_UPDATED`/`MILESTONE_DUE`/`WEBHOOK`/`MANUAL` are the same \"work item title/status, no fromStatus\"\n * shape MINUS `workItem.key`. This was originally true because none of the four had a real dispatcher and\n * every run went through `automation.service.ts#execute()` (`RunAutomationRuleDialog`'s `{ workItemId }` plus\n * a fresh `prisma.workItem` read, which never assigns `key`). AUT-27/35 gave `WORK_ITEM_UPDATED` and\n * `MILESTONE_DUE` real dispatchers (see this file's header and `AUTOMATIC_TRIGGER_TYPES`), but absent a\n * confirmed look at their real `TriggerData` shape in automation-engine.ts/automation-scheduler.ts, this\n * preview deliberately keeps the same, narrower placeholder set rather than guessing that `workItem.key` is\n * now populated β€” offering a placeholder this preview cannot confirm renders would risk showing a template\n * that silently renders empty at real dispatch time, for a preview whose whole job is to show that correctly.\n * `WEBHOOK` and `MANUAL` keep this shape for the original reason (no real dispatcher at all).\n *\n * `actor` is always populated (the real triggering actor, `AUTOMATION_SCHEDULER_ACTOR_ID` for SCHEDULE, or the\n * \"Run now\" caller) so it is available for every trigger.\n */\nexport function templatePlaceholdersForTrigger(trigger: TriggerType): readonly string[] {\n if (trigger === 'SPRINT_STARTED' || trigger === 'SPRINT_COMPLETED')\n return ['sprint.name', 'actor'];\n if (trigger === 'RELEASE_RELEASED') return ['release.version', 'release.name', 'actor'];\n if (trigger === 'WORK_ITEM_STATUS_CHANGED')\n return ['workItem.key', 'workItem.title', 'fromStatus', 'toStatus', 'actor'];\n // WORK_ITEM_UPDATED/MILESTONE_DUE are no longer in INERT_TRIGGER_TYPES (AUT-27/35 gave them real dispatchers)\n // but are named explicitly here too β€” see the doc comment above for why this preview keeps their placeholder\n // set unchanged rather than assuming `workItem.key` is now populated.\n if (\n trigger === 'WORK_ITEM_UPDATED' ||\n trigger === 'MILESTONE_DUE' ||\n INERT_TRIGGER_TYPES.has(trigger) ||\n trigger === 'MANUAL'\n )\n return ['workItem.title', 'toStatus', 'actor'];\n return ['workItem.key', 'workItem.title', 'toStatus', 'actor'];\n}\n\n/** Sample values for exactly the placeholders `templatePlaceholdersForTrigger` says apply to this trigger β€” any\n * other `{{...}}` in the template previews empty, matching what the real engine would do. */\nexport function templateSampleValues(trigger: TriggerType): Record<string, string | undefined> {\n const values: Record<string, string | undefined> = {};\n for (const token of templatePlaceholdersForTrigger(trigger)) {\n values[token] = TEMPLATE_PLACEHOLDER_INFO[token]?.sample;\n }\n return values;\n}\n\n// ============================================================================\n// Save gating β€” a rule this builder would otherwise let someone save but that can never do anything, or that\n// carries an action this engine refuses to dispatch.\n// ============================================================================\n\n/** `null` when this trigger needs no explanation blocking Save; the reason string otherwise (same text as\n * `triggerHint` for a `NO_WORK_ITEM_TRIGGER_TYPES` trigger β€” the one case that blocks Save outright). */\nexport function triggerSaveBlockReason(trigger: TriggerType): string | null {\n return NO_WORK_ITEM_TRIGGER_TYPES.has(trigger) ? triggerHint(trigger) : null;\n}\n\n/** Combines every reason a rule form cannot be saved yet: a trigger with no possible action, a `POST_TO_CHANNEL`\n * action while channel posting is gated off, or (for `SCHEDULE`) a not-yet-valid schedule window. `null` means\n * the form is save-able as far as these rules are concerned (the caller still checks name/project separately). */\nexport function saveBlockReason(form: {\n triggerType: TriggerType;\n actions: RuleAction[];\n scheduleStatuses: string[];\n scheduleNotUpdatedForDays: string;\n}): string | null {\n const triggerReason = triggerSaveBlockReason(form.triggerType);\n if (triggerReason) return triggerReason;\n if (!CHANNEL_POSTING_AVAILABLE && form.actions.some((a) => a.type === 'POST_TO_CHANNEL')) {\n return CHANNEL_POSTING_BLOCKED_MESSAGE;\n }\n if (\n form.triggerType === 'SCHEDULE' &&\n !isScheduleConditionsValid(form.scheduleStatuses, form.scheduleNotUpdatedForDays)\n ) {\n return 'Choose at least one status and a whole number of days (1 or more) before saving this schedule.';\n }\n return null;\n}\n\n// ============================================================================\n// Execution errors β€” automation-engine.ts's `errorMessage` is one `'; '`-joined string (each action's\n// `\"<label>: <message>\"`, plus an optional \"N declared action(s) are not supported…\" summary). There is no\n// structured per-action array in the API (`PlanmagnetAutomationExecution.result` only carries\n// `dispatched`/`skipped`/counts β€” see the schema), so this is the best available split of what the API returns.\n// ============================================================================\n\nexport function splitExecutionErrors(message: string | null | undefined): string[] {\n if (!message) return [];\n return message\n .split('; ')\n .map((s) => s.trim())\n .filter(Boolean);\n}\n"],"mappings":"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"}
@@ -6,7 +6,20 @@
6
6
  export declare const FEATURE_STATUSES: readonly ["SUBMITTED", "UNDER_REVIEW", "PLANNED", "IN_PROGRESS", "COMPLETED", "DECLINED", "DUPLICATE"];
7
7
  /** Every query that shows a feature request, its votes or feature counts. */
8
8
  export declare const FEATURE_QUERIES: string[];
9
- /** Vote for a request, or take your vote back. */
9
+ /**
10
+ * Vote for a request, or take your vote back. CWF-15: an unvoted caller can choose "Vote anonymously" (its own
11
+ * `FeatureVote.isAnonymous`, independent of the parent request's own anonymity) before casting β€” the checkbox
12
+ * only makes sense while deciding how to vote, so it disappears once a vote exists (unvoting needs no choice).
13
+ *
14
+ * `voted` (from the parent's `votes.some(v => v.voterId === currentUser.id)`) is unreliable right after an
15
+ * anonymous vote: the schema redacts `FeatureVote.voterId` to `"anonymous"` for a non-moderator caller's own
16
+ * anonymous vote (no self carve-out), so once `FEATURE_QUERIES` refetches, that comparison flips back to
17
+ * false even though the vote exists. Track this session's own vote/unvote outcome locally and prefer it over
18
+ * the (possibly-redacted) server-derived value, so the button and the anonymous checkbox stay correct. The
19
+ * caller remounts this component on feature change (`key={feature.id}`) rather than this resetting the
20
+ * override itself in an effect, matching React's own "adjust state when a prop changes" guidance β€” no
21
+ * cascading-render setState-in-effect, and no one-render-behind reset.
22
+ */
10
23
  export declare function FeatureVoteButton({ featureId, voted }: {
11
24
  featureId: string;
12
25
  voted: boolean;
@@ -31,6 +44,10 @@ export interface FeatureValues {
31
44
  impact?: number | null;
32
45
  confidence?: number | null;
33
46
  riceScore?: number | null;
47
+ /** CWF-15 β€” optional for the same "predates this pass" reason as the PRM-26/27 fields above. */
48
+ rating?: number | null;
49
+ category?: string | null;
50
+ isAnonymous?: boolean;
34
51
  }
35
52
  /** Status select, Edit and Delete for the feature detail header. */
36
53
  export declare function FeatureHeaderActions({ feature }: {
@@ -1 +1 @@
1
- {"version":3,"file":"FeatureControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/FeatureControls.tsx"],"names":[],"mappings":"AAwCA;;;;GAIG;AAEH,eAAO,MAAM,gBAAgB,wGAQnB,CAAC;AAEX,6EAA6E;AAC7E,eAAO,MAAM,eAAe,UAQ3B,CAAC;AA6CF,kDAAkD;AAClD,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,2CA6B5F;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,mGAAmG;IACnG,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CA4E3E;AA8QD,wBAAgB,yBAAyB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CAiKhF"}
1
+ {"version":3,"file":"FeatureControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/FeatureControls.tsx"],"names":[],"mappings":"AA6CA;;;;GAIG;AAEH,eAAO,MAAM,gBAAgB,wGAQnB,CAAC;AAEX,6EAA6E;AAC7E,eAAO,MAAM,eAAe,UAQ3B,CAAC;AAkDF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,2CAmD5F;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,mGAAmG;IACnG,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gGAAgG;IAChG,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CA4E3E;AA+SD,wBAAgB,yBAAyB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CAiKhF"}