@mastra/factory 0.12.0-alpha.1 → 0.12.0-alpha.3

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 (68) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/factory.d.ts.map +1 -1
  3. package/dist/factory.js +24 -14
  4. package/dist/factory.js.map +1 -1
  5. package/dist/integrations/base.d.ts +9 -0
  6. package/dist/integrations/base.d.ts.map +1 -1
  7. package/dist/integrations/github/issue-reconciler.d.ts.map +1 -1
  8. package/dist/integrations/github/issue-reconciler.js +22 -14
  9. package/dist/integrations/github/issue-reconciler.js.map +1 -1
  10. package/dist/integrations/github/rules.d.ts +6 -0
  11. package/dist/integrations/github/rules.d.ts.map +1 -1
  12. package/dist/integrations/github/rules.js +58 -12
  13. package/dist/integrations/github/rules.js.map +1 -1
  14. package/dist/integrations/slack/feed-publisher.d.ts +26 -0
  15. package/dist/integrations/slack/feed-publisher.d.ts.map +1 -0
  16. package/dist/integrations/slack/feed-publisher.js +44 -0
  17. package/dist/integrations/slack/feed-publisher.js.map +1 -0
  18. package/dist/integrations/slack/integration.d.ts +2 -0
  19. package/dist/integrations/slack/integration.d.ts.map +1 -1
  20. package/dist/integrations/slack/integration.js +5 -0
  21. package/dist/integrations/slack/integration.js.map +1 -1
  22. package/dist/integrations/slack/slack.d.ts +13 -1
  23. package/dist/integrations/slack/slack.d.ts.map +1 -1
  24. package/dist/integrations/slack/slack.js +78 -6
  25. package/dist/integrations/slack/slack.js.map +1 -1
  26. package/dist/routes/surface.d.ts +5 -0
  27. package/dist/routes/surface.d.ts.map +1 -1
  28. package/dist/routes/surface.js +8 -4
  29. package/dist/routes/surface.js.map +1 -1
  30. package/dist/routes/work-items.d.ts.map +1 -1
  31. package/dist/routes/work-items.js +11 -1
  32. package/dist/routes/work-items.js.map +1 -1
  33. package/dist/rules/defaults.d.ts.map +1 -1
  34. package/dist/rules/defaults.js +20 -8
  35. package/dist/rules/defaults.js.map +1 -1
  36. package/dist/rules/dispatcher.d.ts.map +1 -1
  37. package/dist/rules/dispatcher.js +24 -10
  38. package/dist/rules/dispatcher.js.map +1 -1
  39. package/dist/rules/processor.d.ts.map +1 -1
  40. package/dist/rules/processor.js +2 -3
  41. package/dist/rules/processor.js.map +1 -1
  42. package/dist/rules/transition-service.d.ts +2 -2
  43. package/dist/rules/transition-service.d.ts.map +1 -1
  44. package/dist/rules/transition-service.js +33 -12
  45. package/dist/rules/transition-service.js.map +1 -1
  46. package/dist/rules/types.d.ts +27 -1
  47. package/dist/rules/types.d.ts.map +1 -1
  48. package/dist/rules/types.js +37 -1
  49. package/dist/rules/types.js.map +1 -1
  50. package/dist/rules/validation.d.ts.map +1 -1
  51. package/dist/rules/validation.js +3 -1
  52. package/dist/rules/validation.js.map +1 -1
  53. package/dist/storage/domains/comments/base.d.ts.map +1 -1
  54. package/dist/storage/domains/comments/base.js +2 -4
  55. package/dist/storage/domains/comments/base.js.map +1 -1
  56. package/dist/storage/domains/comments/domain.d.ts +9 -2
  57. package/dist/storage/domains/comments/domain.d.ts.map +1 -1
  58. package/dist/storage/domains/comments/domain.js +18 -15
  59. package/dist/storage/domains/comments/domain.js.map +1 -1
  60. package/dist/storage/domains/comments/feed-sync.d.ts +9 -26
  61. package/dist/storage/domains/comments/feed-sync.d.ts.map +1 -1
  62. package/dist/storage/domains/work-items/base.d.ts +23 -2
  63. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  64. package/dist/storage/domains/work-items/base.js +82 -50
  65. package/dist/storage/domains/work-items/base.js.map +1 -1
  66. package/factory-skills/factory-rereview/SKILL.md +13 -12
  67. package/factory-skills/factory-review/SKILL.md +12 -11
  68. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../src/rules/types.ts"],"sourcesContent":["export type WorkItemSource = 'github-issue' | 'github-pr' | 'linear-issue' | 'manual';\n\nexport const FACTORY_RULE_STAGES = ['intake', 'triage', 'planning', 'execute', 'review', 'done', 'canceled'] as const;\nexport type FactoryRuleStage = (typeof FACTORY_RULE_STAGES)[number];\n\nexport const FACTORY_TRIAGE_TYPES = [\n 'bug',\n 'feature request',\n 'docs',\n 'question/support',\n 'maintenance',\n 'duplicate',\n 'resolved',\n 'invalid',\n 'spam',\n 'out-of-scope',\n 'other',\n] as const;\nexport type FactoryTriageType = (typeof FACTORY_TRIAGE_TYPES)[number];\n\nexport function isFactoryTriageType(value: unknown): value is FactoryTriageType {\n return typeof value === 'string' && FACTORY_TRIAGE_TYPES.some(type => type === value);\n}\n\nexport function isFactoryRuleStage(value: unknown): value is FactoryRuleStage {\n return typeof value === 'string' && FACTORY_RULE_STAGES.some(stage => stage === value);\n}\n\nexport function factoryRuleStage(stages: readonly string[]): FactoryRuleStage | undefined {\n const stage = stages.length === 1 ? stages[0] : undefined;\n return isFactoryRuleStage(stage) ? stage : undefined;\n}\n\nexport function isTerminalFactoryRuleStage(stages: readonly string[]): boolean {\n const stage = factoryRuleStage(stages);\n return stage === 'done' || stage === 'canceled';\n}\n\nexport const FACTORY_RULE_BOARDS = ['work', 'review'] as const;\nexport type FactoryRuleBoard = (typeof FACTORY_RULE_BOARDS)[number];\n\nexport const FACTORY_RULE_SOURCES = ['issue', 'pullRequest', 'linearIssue', 'manual'] as const;\nexport type FactoryRuleSource = (typeof FACTORY_RULE_SOURCES)[number];\n\nexport const FACTORY_GITHUB_EVENTS = [\n 'issueOpened',\n 'issueEdited',\n 'issueClosed',\n 'issueCommentCreated',\n 'issueCommentEdited',\n 'issueCommentDeleted',\n 'pullRequestOpened',\n 'pullRequestUpdated',\n 'pullRequestCommentCreated',\n 'pullRequestReviewRequested',\n 'pullRequestReviewSubmitted',\n 'pullRequestMerged',\n 'pullRequestClosed',\n] as const;\nexport type FactoryGithubEventName = (typeof FACTORY_GITHUB_EVENTS)[number];\n\nexport const FACTORY_LINEAR_EVENTS = ['issueObserved', 'issueClosed'] as const;\nexport type FactoryLinearEventName = (typeof FACTORY_LINEAR_EVENTS)[number];\n\nexport type FactoryRuleJsonValue =\n | null\n | boolean\n | number\n | string\n | FactoryRuleJsonValue[]\n | { [key: string]: FactoryRuleJsonValue };\n\nexport interface FactoryRuleItemContext {\n id: string;\n source: WorkItemSource;\n sourceKey: string | null;\n parentWorkItemId: string | null;\n title: string;\n url: string | null;\n stages: readonly string[];\n /** Intake-stamped facts about the source — repository id, reporter login, labels. */\n metadata: Record<string, unknown> | null;\n}\n\nexport type FactoryRuleActor =\n | { type: 'human'; id: string }\n | { type: 'agent'; bindingId: string; role: string }\n | { type: 'github'; login: string; trusted: boolean; factoryAuthored: boolean }\n | { type: 'system'; id: string };\n\nexport interface FactoryRuleIngressIdentity {\n type: 'human' | 'agent' | 'toolResult' | 'github' | 'linear' | 'rule';\n id: string;\n}\n\nexport interface FactoryRuleCausalEntry {\n ingressId: string;\n decisionType: FactoryCommitDecision['type'];\n}\n\nexport interface FactoryRuleContextBase {\n tenant: { orgId: string; projectId: string };\n actor: FactoryRuleActor;\n ingress: FactoryRuleIngressIdentity;\n cause: string;\n causalChain: readonly FactoryRuleCausalEntry[];\n ruleSetVersion: string;\n}\n\nexport interface FactoryBoundRuleContext extends FactoryRuleContextBase {\n item: FactoryRuleItemContext;\n board: FactoryRuleBoard;\n itemRevision: number;\n}\n\nexport interface FactoryStageRuleContext extends FactoryBoundRuleContext {\n source: FactoryRuleSource;\n stage: FactoryRuleStage;\n fromStage: FactoryRuleStage;\n toStage: FactoryRuleStage;\n}\n\nexport interface FactoryToolResultRuleContext extends FactoryBoundRuleContext {\n toolName: string;\n threadId: string;\n assistantMessageId: string;\n toolCallId: string;\n result: {\n status: 'success' | 'error';\n value: FactoryRuleJsonValue;\n };\n}\n\nexport interface FactoryGithubRuleContext extends FactoryRuleContextBase {\n item?: FactoryRuleItemContext;\n board?: FactoryRuleBoard;\n itemRevision?: number;\n event: FactoryGithubEventName;\n deliveryId: string;\n factory: { createdAt: string };\n repository: { id: number; fullName: string };\n issue?: {\n number: number;\n title: string;\n url: string;\n createdAt?: string;\n updatedAt?: string;\n assignees?: string[];\n labels?: string[];\n state?: 'open' | 'closed';\n /** GitHub close reason: `completed`, `not_planned`, or `duplicate`. */\n stateReason?: string;\n };\n issueChange?: { title: boolean; body: boolean };\n issueComment?: {\n id: number;\n body?: string;\n url?: string;\n author?: string;\n authorType?: string;\n createdAt?: string;\n updatedAt?: string;\n };\n pullRequest?: {\n number: number;\n title: string;\n url: string;\n createdAt?: string;\n state: 'open' | 'closed';\n draft: boolean;\n merged: boolean;\n assignees?: string[];\n requestedReviewers?: string[];\n labels?: string[];\n headBranch: string;\n baseBranch: string;\n };\n /** Present on `pullRequestReviewRequested`: who review was (re-)requested from. */\n reviewRequest?: { reviewer: string; factoryReviewer: boolean };\n /** Present on `pullRequestReviewSubmitted`: the review that was just posted. */\n review?: { id: number; state: string; url: string };\n}\n\nexport interface FactoryLinearRuleContext extends FactoryRuleContextBase {\n item?: FactoryRuleItemContext;\n board?: FactoryRuleBoard;\n itemRevision?: number;\n event: FactoryLinearEventName;\n issue: {\n id: string;\n identifier: string;\n title: string;\n url: string;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n creator: string | null;\n team: string | null;\n labels: readonly string[];\n createdAt: string;\n updatedAt: string;\n };\n}\n\nexport type FactoryRuleHandler<TContext> = (\n context: Readonly<TContext>,\n) => FactoryRuleDecision | void | Promise<FactoryRuleDecision | void>;\n\nexport interface FactoryBoardRuleLeaf {\n onEnter?: FactoryRuleHandler<FactoryStageRuleContext>;\n onExit?: FactoryRuleHandler<FactoryStageRuleContext>;\n}\n\nexport interface FactoryToolRuleLeaf {\n onResult?: FactoryRuleHandler<FactoryToolResultRuleContext>;\n}\n\nexport interface FactoryGithubRuleLeaf {\n onEvent?: FactoryRuleHandler<FactoryGithubRuleContext>;\n}\n\nexport interface FactoryLinearRuleLeaf {\n onEvent?: FactoryRuleHandler<FactoryLinearRuleContext>;\n}\n\nexport type FactoryBoardRules = Partial<\n Record<FactoryRuleStage, Partial<Record<FactoryRuleSource, FactoryBoardRuleLeaf>>>\n>;\n\nexport interface FactoryRules {\n version: string;\n work: FactoryBoardRules;\n review: FactoryBoardRules;\n tools: Record<string, FactoryToolRuleLeaf>;\n github: Partial<Record<FactoryGithubEventName, FactoryGithubRuleLeaf>>;\n linear: Partial<Record<FactoryLinearEventName, FactoryLinearRuleLeaf>>;\n}\n\nexport interface FactoryRulesOverrides {\n work?: FactoryBoardRules;\n review?: FactoryBoardRules;\n tools?: Record<string, FactoryToolRuleLeaf>;\n github?: Partial<Record<FactoryGithubEventName, FactoryGithubRuleLeaf>>;\n linear?: Partial<Record<FactoryLinearEventName, FactoryLinearRuleLeaf>>;\n}\n\nexport type FactoryRuleRejectionCode =\n | 'forbidden'\n | 'invalid_transition'\n | 'missing_binding'\n | 'stale'\n | 'timeout'\n | 'rule_error'\n | 'causal_depth_exceeded'\n | 'repeated_transition'\n | 'approval_required';\n\nexport interface FactoryRuleRejectDecision {\n type: 'reject';\n code: FactoryRuleRejectionCode;\n reason: string;\n}\n\ninterface FactoryCommitDecisionBase {\n idempotencyKey: string;\n}\n\nexport interface FactoryTransitionDecision extends FactoryCommitDecisionBase {\n type: 'transition';\n board: FactoryRuleBoard;\n stage: FactoryRuleStage;\n /**\n * Delivered to the item's active session (waking it if idle) after the\n * transition commits. Skipped when the item has no active run binding, so\n * informational messages never fail the transition.\n */\n message?: { text: string; role?: string };\n /**\n * Runs the stage's entry rules even when the item is already in that stage.\n * A transition to the current stage is normally inert, because most callers\n * are correcting a board into a state it already holds. Re-entry is for the\n * opposite case: the stage's work is in flight and has been invalidated, so\n * it has to start over.\n */\n reenter?: boolean;\n}\n\nexport interface FactoryUpsertLinkedWorkItemDecision extends FactoryCommitDecisionBase {\n type: 'upsertLinkedWorkItem';\n board: FactoryRuleBoard;\n source: WorkItemSource;\n sourceKey: string;\n title: string;\n url: string | null;\n stage: FactoryRuleStage;\n metadata?: Record<string, FactoryRuleJsonValue>;\n}\n\ninterface FactoryInvokeSkillDecisionBase extends FactoryCommitDecisionBase {\n type: 'invokeSkill';\n role: string;\n arguments?: string;\n precedingMessage?: string;\n cancelInFlight?: boolean;\n}\n\n/**\n * Starting an agent run. Most runs activate a skill, because the skill carries\n * the handoff contract later rules match on. A run whose completion is already\n * signalled some other way — Building finishes by opening a pull request, which\n * arrives as its own event — needs no contract, so it can carry a plain prompt\n * instead of an otherwise empty skill.\n */\nexport type FactoryInvokeSkillDecision = FactoryInvokeSkillDecisionBase &\n ({ skillName: string; prompt?: never } | { prompt: string; skillName?: never });\n\nexport interface FactorySendMessageDecision extends FactoryCommitDecisionBase {\n type: 'sendMessage';\n role: string;\n message: string;\n priority?: 'medium' | 'high' | 'urgent';\n idleBehavior?: 'persist' | 'wake';\n prepareBinding?: boolean;\n}\n\nexport interface FactoryNotifyDecision extends FactoryCommitDecisionBase {\n type: 'notify';\n title: string;\n body?: string;\n level?: 'info' | 'warning' | 'error';\n}\n\nexport type FactoryCommitDecision =\n | FactoryTransitionDecision\n | FactoryUpsertLinkedWorkItemDecision\n | FactoryInvokeSkillDecision\n | FactorySendMessageDecision\n | FactoryNotifyDecision;\n\nexport type FactoryRuleDecision = FactoryRuleRejectDecision | FactoryCommitDecision;\n\nexport interface FactoryTransitionResultAccepted {\n status: 'accepted';\n transitionId: string;\n itemId: string;\n revision: number;\n stage: FactoryRuleStage;\n decisions: FactoryCommitDecision[];\n}\n\nexport interface FactoryTransitionResultRejected {\n status: 'rejected';\n transitionId: string;\n itemId: string;\n code: FactoryRuleRejectionCode;\n reason: string;\n}\n\nexport type FactoryTransitionResult = FactoryTransitionResultAccepted | FactoryTransitionResultRejected;\n\nexport function factoryRuleSourceForWorkItem(source: WorkItemSource): FactoryRuleSource {\n switch (source) {\n case 'github-issue':\n return 'issue';\n case 'github-pr':\n return 'pullRequest';\n case 'linear-issue':\n return 'linearIssue';\n case 'manual':\n return 'manual';\n }\n}\n"],"mappings":";AAEA,MAAa,sBAAsB;CAAC;CAAU;CAAU;CAAY;CAAW;CAAU;CAAQ;AAAU;AAG3G,MAAa,uBAAuB;CAClC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAGA,SAAgB,oBAAoB,OAA4C;CAC9E,OAAO,OAAO,UAAU,YAAY,qBAAqB,MAAK,SAAQ,SAAS,KAAK;AACtF;AAEA,SAAgB,mBAAmB,OAA2C;CAC5E,OAAO,OAAO,UAAU,YAAY,oBAAoB,MAAK,UAAS,UAAU,KAAK;AACvF;AAEA,SAAgB,iBAAiB,QAAyD;CACxF,MAAM,QAAQ,OAAO,WAAW,IAAI,OAAO,KAAK,KAAA;CAChD,OAAO,mBAAmB,KAAK,IAAI,QAAQ,KAAA;AAC7C;AAEA,SAAgB,2BAA2B,QAAoC;CAC7E,MAAM,QAAQ,iBAAiB,MAAM;CACrC,OAAO,UAAU,UAAU,UAAU;AACvC;AAEA,MAAa,sBAAsB,CAAC,QAAQ,QAAQ;AAGpD,MAAa,uBAAuB;CAAC;CAAS;CAAe;CAAe;AAAQ;AAGpF,MAAa,wBAAwB;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAGA,MAAa,wBAAwB,CAAC,iBAAiB,aAAa;AA4SpE,SAAgB,6BAA6B,QAA2C;CACtF,QAAQ,QAAR;EACE,KAAK,gBACH,OAAO;EACT,KAAK,aACH,OAAO;EACT,KAAK,gBACH,OAAO;EACT,KAAK,UACH,OAAO;CACX;AACF"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../src/rules/types.ts"],"sourcesContent":["import type { ExternalWorkItemSource } from '../storage/domains/work-items/base.js';\n\nexport type WorkItemSource = 'github-issue' | 'github-pr' | 'linear-issue' | 'manual';\n\nexport function workItemSource(source: ExternalWorkItemSource | null): WorkItemSource {\n if (!source) return 'manual';\n if (source.integrationId === 'linear') return 'linear-issue';\n // Only GitHub and Linear have provider-specific rules; anything else (a Slack\n // thread, say) is a plain work item, not a mislabeled GitHub issue.\n if (source.integrationId !== 'github') return 'manual';\n return source.type === 'pull-request' ? 'github-pr' : 'github-issue';\n}\n\n// Authored outside the write-access circle: a missing trust stamp fails closed until\n// the reconcile sweep backfills it, and Factory's own PRs pass through `factoryAuthored`.\nexport function externallyAuthored(item: { source: string; metadata: Record<string, unknown> | null }): boolean {\n if (item.source !== 'github-pr' && item.source !== 'github-issue') return false;\n if (item.metadata?.factoryAuthored === true) return false;\n return item.metadata?.authorTrusted !== true;\n}\n\n// The board mark claims only what GitHub answered: a missing stamp is silence, not an outside contribution.\nexport function knownExternalAuthor(item: { source: string; metadata: Record<string, unknown> | null }): boolean {\n return externallyAuthored(item) && item.metadata?.authorTrusted === false;\n}\n\nexport function externallyAuthoredWorkItem(item: {\n externalSource: ExternalWorkItemSource | null;\n metadata: Record<string, unknown> | null;\n}): boolean {\n return externallyAuthored({ source: workItemSource(item.externalSource), metadata: item.metadata });\n}\n\nexport const FACTORY_RULE_STAGES = ['intake', 'triage', 'planning', 'execute', 'review', 'done', 'canceled'] as const;\nexport type FactoryRuleStage = (typeof FACTORY_RULE_STAGES)[number];\n\n// Each role and the working stage its run holds the card in. Key order is the\n// seat pipeline order — Resume depth derives from it.\nexport const FACTORY_ROLE_STAGES = {\n triage: 'triage',\n plan: 'planning',\n work: 'execute',\n review: 'review',\n} as const satisfies Record<string, FactoryRuleStage>;\nexport type FactoryRole = keyof typeof FACTORY_ROLE_STAGES;\n\nexport function isFactoryRole(value: string): value is FactoryRole {\n return value in FACTORY_ROLE_STAGES;\n}\n\nexport const FACTORY_TRIAGE_TYPES = [\n 'bug',\n 'feature request',\n 'docs',\n 'question/support',\n 'maintenance',\n 'duplicate',\n 'resolved',\n 'invalid',\n 'spam',\n 'out-of-scope',\n 'other',\n] as const;\nexport type FactoryTriageType = (typeof FACTORY_TRIAGE_TYPES)[number];\n\nexport function isFactoryTriageType(value: unknown): value is FactoryTriageType {\n return typeof value === 'string' && FACTORY_TRIAGE_TYPES.some(type => type === value);\n}\n\nexport function isFactoryRuleStage(value: unknown): value is FactoryRuleStage {\n return typeof value === 'string' && FACTORY_RULE_STAGES.some(stage => stage === value);\n}\n\nexport function factoryRuleStage(stages: readonly string[]): FactoryRuleStage | undefined {\n const stage = stages.length === 1 ? stages[0] : undefined;\n return isFactoryRuleStage(stage) ? stage : undefined;\n}\n\nexport function isTerminalFactoryRuleStage(stages: readonly string[]): boolean {\n const stage = factoryRuleStage(stages);\n return stage === 'done' || stage === 'canceled';\n}\n\n/** Working lanes hold cards with a seat engaged; Intake, Done and Canceled rest them. */\nexport function isWorkingFactoryRuleStage(stage: FactoryRuleStage): boolean {\n return stage !== 'intake' && !isTerminalFactoryRuleStage([stage]);\n}\n\n// Consulted only for the Intake exit: roles don't own lanes, so a card already\n// in a working or terminal lane stays put when a run starts.\nexport function factoryLaneForRole(role: string): FactoryRuleStage | undefined {\n return isFactoryRole(role) ? FACTORY_ROLE_STAGES[role] : undefined;\n}\n\nexport const FACTORY_RULE_BOARDS = ['work', 'review'] as const;\nexport type FactoryRuleBoard = (typeof FACTORY_RULE_BOARDS)[number];\n\nexport const FACTORY_RULE_SOURCES = ['issue', 'pullRequest', 'linearIssue', 'manual'] as const;\nexport type FactoryRuleSource = (typeof FACTORY_RULE_SOURCES)[number];\n\nexport const FACTORY_GITHUB_EVENTS = [\n 'issueOpened',\n 'issueEdited',\n 'issueClosed',\n 'issueCommentCreated',\n 'issueCommentEdited',\n 'issueCommentDeleted',\n 'pullRequestOpened',\n 'pullRequestUpdated',\n 'pullRequestCommentCreated',\n 'pullRequestReviewRequested',\n 'pullRequestReviewSubmitted',\n 'pullRequestMerged',\n 'pullRequestClosed',\n] as const;\nexport type FactoryGithubEventName = (typeof FACTORY_GITHUB_EVENTS)[number];\n\nexport const FACTORY_LINEAR_EVENTS = ['issueObserved', 'issueClosed'] as const;\nexport type FactoryLinearEventName = (typeof FACTORY_LINEAR_EVENTS)[number];\n\nexport type FactoryRuleJsonValue =\n | null\n | boolean\n | number\n | string\n | FactoryRuleJsonValue[]\n | { [key: string]: FactoryRuleJsonValue };\n\nexport interface FactoryRuleItemContext {\n id: string;\n source: WorkItemSource;\n sourceKey: string | null;\n parentWorkItemId: string | null;\n title: string;\n url: string | null;\n stages: readonly string[];\n /** Intake-stamped facts about the source — repository id, reporter login, labels. */\n metadata: Record<string, unknown> | null;\n}\n\nexport type FactoryRuleActor =\n | { type: 'human'; id: string }\n | { type: 'agent'; bindingId: string; role: string }\n | { type: 'github'; login: string; trusted: boolean; factoryAuthored: boolean }\n | { type: 'system'; id: string };\n\nexport interface FactoryRuleIngressIdentity {\n type: 'human' | 'agent' | 'toolResult' | 'github' | 'linear' | 'rule';\n id: string;\n}\n\nexport interface FactoryRuleCausalEntry {\n ingressId: string;\n decisionType: FactoryCommitDecision['type'];\n}\n\nexport interface FactoryRuleContextBase {\n tenant: { orgId: string; projectId: string };\n actor: FactoryRuleActor;\n ingress: FactoryRuleIngressIdentity;\n cause: string;\n causalChain: readonly FactoryRuleCausalEntry[];\n ruleSetVersion: string;\n}\n\nexport interface FactoryBoundRuleContext extends FactoryRuleContextBase {\n item: FactoryRuleItemContext;\n board: FactoryRuleBoard;\n itemRevision: number;\n}\n\nexport interface FactoryStageRuleContext extends FactoryBoundRuleContext {\n source: FactoryRuleSource;\n stage: FactoryRuleStage;\n fromStage: FactoryRuleStage;\n toStage: FactoryRuleStage;\n}\n\nexport interface FactoryToolResultRuleContext extends FactoryBoundRuleContext {\n toolName: string;\n threadId: string;\n assistantMessageId: string;\n toolCallId: string;\n result: {\n status: 'success' | 'error';\n value: FactoryRuleJsonValue;\n };\n}\n\nexport interface FactoryGithubRuleContext extends FactoryRuleContextBase {\n item?: FactoryRuleItemContext;\n board?: FactoryRuleBoard;\n itemRevision?: number;\n event: FactoryGithubEventName;\n deliveryId: string;\n factory: { createdAt: string };\n repository: { id: number; fullName: string };\n issue?: {\n number: number;\n title: string;\n url: string;\n createdAt?: string;\n updatedAt?: string;\n assignees?: string[];\n labels?: string[];\n state?: 'open' | 'closed';\n /** GitHub close reason: `completed`, `not_planned`, or `duplicate`. */\n stateReason?: string;\n };\n issueChange?: { title: boolean; body: boolean };\n issueComment?: {\n id: number;\n body?: string;\n url?: string;\n author?: string;\n authorType?: string;\n createdAt?: string;\n updatedAt?: string;\n };\n pullRequest?: {\n number: number;\n title: string;\n url: string;\n createdAt?: string;\n state: 'open' | 'closed';\n draft: boolean;\n merged: boolean;\n assignees?: string[];\n requestedReviewers?: string[];\n labels?: string[];\n headBranch: string;\n baseBranch: string;\n };\n /** Present on `pullRequestReviewRequested`: who review was (re-)requested from. */\n reviewRequest?: { reviewer: string; factoryReviewer: boolean };\n /** Present on `pullRequestReviewSubmitted`: the review that was just posted. */\n review?: { id: number; state: string; url: string };\n}\n\nexport interface FactoryLinearRuleContext extends FactoryRuleContextBase {\n item?: FactoryRuleItemContext;\n board?: FactoryRuleBoard;\n itemRevision?: number;\n event: FactoryLinearEventName;\n issue: {\n id: string;\n identifier: string;\n title: string;\n url: string;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n creator: string | null;\n team: string | null;\n labels: readonly string[];\n createdAt: string;\n updatedAt: string;\n };\n}\n\nexport type FactoryRuleHandler<TContext> = (\n context: Readonly<TContext>,\n) => FactoryRuleDecision | void | Promise<FactoryRuleDecision | void>;\n\nexport interface FactoryBoardRuleLeaf {\n onEnter?: FactoryRuleHandler<FactoryStageRuleContext>;\n onExit?: FactoryRuleHandler<FactoryStageRuleContext>;\n}\n\nexport interface FactoryToolRuleLeaf {\n onResult?: FactoryRuleHandler<FactoryToolResultRuleContext>;\n}\n\nexport interface FactoryGithubRuleLeaf {\n onEvent?: FactoryRuleHandler<FactoryGithubRuleContext>;\n}\n\nexport interface FactoryLinearRuleLeaf {\n onEvent?: FactoryRuleHandler<FactoryLinearRuleContext>;\n}\n\nexport type FactoryBoardRules = Partial<\n Record<FactoryRuleStage, Partial<Record<FactoryRuleSource, FactoryBoardRuleLeaf>>>\n>;\n\nexport interface FactoryRules {\n version: string;\n work: FactoryBoardRules;\n review: FactoryBoardRules;\n tools: Record<string, FactoryToolRuleLeaf>;\n github: Partial<Record<FactoryGithubEventName, FactoryGithubRuleLeaf>>;\n linear: Partial<Record<FactoryLinearEventName, FactoryLinearRuleLeaf>>;\n}\n\nexport interface FactoryRulesOverrides {\n work?: FactoryBoardRules;\n review?: FactoryBoardRules;\n tools?: Record<string, FactoryToolRuleLeaf>;\n github?: Partial<Record<FactoryGithubEventName, FactoryGithubRuleLeaf>>;\n linear?: Partial<Record<FactoryLinearEventName, FactoryLinearRuleLeaf>>;\n}\n\nexport type FactoryRuleRejectionCode =\n | 'forbidden'\n | 'invalid_transition'\n | 'missing_binding'\n | 'stale'\n | 'timeout'\n | 'rule_error'\n | 'causal_depth_exceeded'\n | 'repeated_transition'\n | 'approval_required';\n\nexport interface FactoryRuleRejectDecision {\n type: 'reject';\n code: FactoryRuleRejectionCode;\n reason: string;\n}\n\ninterface FactoryCommitDecisionBase {\n idempotencyKey: string;\n}\n\nexport interface FactoryTransitionDecision extends FactoryCommitDecisionBase {\n type: 'transition';\n board: FactoryRuleBoard;\n stage: FactoryRuleStage;\n /**\n * Delivered to the item's active session (waking it if idle) after the\n * transition commits. Skipped when the item has no active run binding, so\n * informational messages never fail the transition.\n */\n message?: { text: string; role?: string };\n /**\n * Runs the stage's entry rules even when the item is already in that stage.\n * A transition to the current stage is normally inert, because most callers\n * are correcting a board into a state it already holds. Re-entry is for the\n * opposite case: the stage's work is in flight and has been invalidated, so\n * it has to start over.\n */\n reenter?: boolean;\n}\n\nexport interface FactoryUpsertLinkedWorkItemDecision extends FactoryCommitDecisionBase {\n type: 'upsertLinkedWorkItem';\n board: FactoryRuleBoard;\n source: WorkItemSource;\n sourceKey: string;\n title: string;\n url: string | null;\n stage: FactoryRuleStage;\n metadata?: Record<string, FactoryRuleJsonValue>;\n}\n\ninterface FactoryInvokeSkillDecisionBase extends FactoryCommitDecisionBase {\n type: 'invokeSkill';\n role: string;\n arguments?: string;\n precedingMessage?: string;\n cancelInFlight?: boolean;\n}\n\n/**\n * Starting an agent run. Most runs activate a skill, because the skill carries\n * the handoff contract later rules match on. A run whose completion is already\n * signalled some other way — Building finishes by opening a pull request, which\n * arrives as its own event — needs no contract, so it can carry a plain prompt\n * instead of an otherwise empty skill.\n */\nexport type FactoryInvokeSkillDecision = FactoryInvokeSkillDecisionBase &\n ({ skillName: string; prompt?: never } | { prompt: string; skillName?: never });\n\nexport interface FactorySendMessageDecision extends FactoryCommitDecisionBase {\n type: 'sendMessage';\n /** Omitted: the card's live session, whichever seat holds it. Required with `prepareBinding`. */\n role?: string;\n message: string;\n priority?: 'medium' | 'high' | 'urgent';\n idleBehavior?: 'persist' | 'wake';\n prepareBinding?: boolean;\n}\n\nexport interface FactoryNotifyDecision extends FactoryCommitDecisionBase {\n type: 'notify';\n title: string;\n body?: string;\n level?: 'info' | 'warning' | 'error';\n}\n\nexport type FactoryCommitDecision =\n | FactoryTransitionDecision\n | FactoryUpsertLinkedWorkItemDecision\n | FactoryInvokeSkillDecision\n | FactorySendMessageDecision\n | FactoryNotifyDecision;\n\nexport type FactoryRuleDecision = FactoryRuleRejectDecision | FactoryCommitDecision;\n\nexport interface FactoryTransitionResultAccepted {\n status: 'accepted';\n transitionId: string;\n itemId: string;\n revision: number;\n stage: FactoryRuleStage;\n decisions: FactoryCommitDecision[];\n}\n\nexport interface FactoryTransitionResultRejected {\n status: 'rejected';\n transitionId: string;\n itemId: string;\n code: FactoryRuleRejectionCode;\n reason: string;\n}\n\nexport type FactoryTransitionResult = FactoryTransitionResultAccepted | FactoryTransitionResultRejected;\n\nexport function factoryRuleSourceForWorkItem(source: WorkItemSource): FactoryRuleSource {\n switch (source) {\n case 'github-issue':\n return 'issue';\n case 'github-pr':\n return 'pullRequest';\n case 'linear-issue':\n return 'linearIssue';\n case 'manual':\n return 'manual';\n }\n}\n"],"mappings":";AAIA,SAAgB,eAAe,QAAuD;CACpF,IAAI,CAAC,QAAQ,OAAO;CACpB,IAAI,OAAO,kBAAkB,UAAU,OAAO;CAG9C,IAAI,OAAO,kBAAkB,UAAU,OAAO;CAC9C,OAAO,OAAO,SAAS,iBAAiB,cAAc;AACxD;AAIA,SAAgB,mBAAmB,MAA6E;CAC9G,IAAI,KAAK,WAAW,eAAe,KAAK,WAAW,gBAAgB,OAAO;CAC1E,IAAI,KAAK,UAAU,oBAAoB,MAAM,OAAO;CACpD,OAAO,KAAK,UAAU,kBAAkB;AAC1C;AAGA,SAAgB,oBAAoB,MAA6E;CAC/G,OAAO,mBAAmB,IAAI,KAAK,KAAK,UAAU,kBAAkB;AACtE;AAEA,SAAgB,2BAA2B,MAG/B;CACV,OAAO,mBAAmB;EAAE,QAAQ,eAAe,KAAK,cAAc;EAAG,UAAU,KAAK;CAAS,CAAC;AACpG;AAEA,MAAa,sBAAsB;CAAC;CAAU;CAAU;CAAY;CAAW;CAAU;CAAQ;AAAU;AAK3G,MAAa,sBAAsB;CACjC,QAAQ;CACR,MAAM;CACN,MAAM;CACN,QAAQ;AACV;AAGA,SAAgB,cAAc,OAAqC;CACjE,OAAO,SAAS;AAClB;AAEA,MAAa,uBAAuB;CAClC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAGA,SAAgB,oBAAoB,OAA4C;CAC9E,OAAO,OAAO,UAAU,YAAY,qBAAqB,MAAK,SAAQ,SAAS,KAAK;AACtF;AAEA,SAAgB,mBAAmB,OAA2C;CAC5E,OAAO,OAAO,UAAU,YAAY,oBAAoB,MAAK,UAAS,UAAU,KAAK;AACvF;AAEA,SAAgB,iBAAiB,QAAyD;CACxF,MAAM,QAAQ,OAAO,WAAW,IAAI,OAAO,KAAK,KAAA;CAChD,OAAO,mBAAmB,KAAK,IAAI,QAAQ,KAAA;AAC7C;AAEA,SAAgB,2BAA2B,QAAoC;CAC7E,MAAM,QAAQ,iBAAiB,MAAM;CACrC,OAAO,UAAU,UAAU,UAAU;AACvC;;AAGA,SAAgB,0BAA0B,OAAkC;CAC1E,OAAO,UAAU,YAAY,CAAC,2BAA2B,CAAC,KAAK,CAAC;AAClE;AAIA,SAAgB,mBAAmB,MAA4C;CAC7E,OAAO,cAAc,IAAI,IAAI,oBAAoB,QAAQ,KAAA;AAC3D;AAEA,MAAa,sBAAsB,CAAC,QAAQ,QAAQ;AAGpD,MAAa,uBAAuB;CAAC;CAAS;CAAe;CAAe;AAAQ;AAGpF,MAAa,wBAAwB;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAGA,MAAa,wBAAwB,CAAC,iBAAiB,aAAa;AA6SpE,SAAgB,6BAA6B,QAA2C;CACtF,QAAQ,QAAR;EACE,KAAK,gBACH,OAAO;EACT,KAAK,aACH,OAAO;EACT,KAAK,gBACH,OAAO;EACT,KAAK,UACH,OAAO;CACX;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/rules/validation.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EAGb,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,6BAA6B,IAAI,CAAC;AA+B/C,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,0BAA0B;gBAE3B,OAAO,EAAE,MAAM;CAI5B;AAoCD,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,OAAO,EACd,KAAK,SAAI,EACT,IAAI,cAAoB,GACvB,oBAAoB,CAkCtB;AA8BD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,YAAY,CAqChF;AAQD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,SAAI,GAAG,mBAAmB,CAuJhG;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,EAAE,WAAW,SAAI,GAAG,qBAAqB,EAAE,CAiBjH"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/rules/validation.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EAGb,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,6BAA6B,IAAI,CAAC;AA+B/C,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,0BAA0B;gBAE3B,OAAO,EAAE,MAAM;CAI5B;AAoCD,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,OAAO,EACd,KAAK,SAAI,EACT,IAAI,cAAoB,GACvB,oBAAoB,CAkCtB;AA8BD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,YAAY,CAqChF;AAQD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,SAAI,GAAG,mBAAmB,CA8JhG;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,EAAE,WAAW,SAAI,GAAG,qBAAqB,EAAE,CAiBjH"}
@@ -263,10 +263,12 @@ function validateFactoryRuleDecision(value, causalDepth = 0) {
263
263
  ], "Factory message priority");
264
264
  const idleBehavior = value.idleBehavior === void 0 ? void 0 : enumValue(value.idleBehavior, ["persist", "wake"], "Factory message idle behavior");
265
265
  if (value.prepareBinding !== void 0 && typeof value.prepareBinding !== "boolean") throw new FactoryRuleValidationError("Factory message prepareBinding must be a boolean.");
266
+ if (value.prepareBinding === true && value.role === void 0) throw new FactoryRuleValidationError("Factory message prepareBinding requires a role.");
267
+ const role = value.role === void 0 ? void 0 : boundedString(value.role, "Factory message role", MAX_ROLE_LENGTH, IDENTIFIER_RE);
266
268
  return {
267
269
  type,
268
270
  ...commonCommitFields(value),
269
- role: boundedString(value.role, "Factory message role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
271
+ ...role ? { role } : {},
270
272
  message: boundedString(value.message, "Factory message", MAX_MESSAGE_LENGTH),
271
273
  ...priority ? { priority } : {},
272
274
  ...idleBehavior ? { idleBehavior } : {},
@@ -1 +1 @@
1
- {"version":3,"file":"validation.js","names":[],"sources":["../../src/rules/validation.ts"],"sourcesContent":["import {\n FACTORY_GITHUB_EVENTS,\n FACTORY_LINEAR_EVENTS,\n FACTORY_RULE_BOARDS,\n FACTORY_RULE_SOURCES,\n FACTORY_RULE_STAGES,\n} from './types.js';\nimport type {\n FactoryBoardRules,\n FactoryCommitDecision,\n FactoryRuleDecision,\n FactoryRuleJsonValue,\n FactoryRules,\n FactoryRuleRejectionCode,\n WorkItemSource,\n} from './types.js';\n\nexport const MAX_FACTORY_RULE_CAUSAL_DEPTH = 8;\n\nconst MAX_VERSION_LENGTH = 128;\nconst MAX_IDEMPOTENCY_KEY_LENGTH = 256;\nconst MAX_REASON_LENGTH = 512;\nconst MAX_TITLE_LENGTH = 512;\nconst MAX_MESSAGE_LENGTH = 8_192;\nconst MAX_ARGUMENTS_LENGTH = 4_096;\nconst MAX_ROLE_LENGTH = 32;\nconst MAX_SKILL_NAME_LENGTH = 128;\nconst MAX_SOURCE_KEY_LENGTH = 256;\nconst MAX_URL_LENGTH = 2_048;\nconst MAX_METADATA_JSON_LENGTH = 16_384;\nconst MAX_JSON_DEPTH = 8;\nconst MAX_JSON_COLLECTION_SIZE = 100;\n\nconst IDENTIFIER_RE = /^[a-z0-9][a-z0-9_-]*$/i;\nconst SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nconst SENSITIVE_KEY_RE = /(?:authorization|cookie|credential|password|secret|token)/i;\nconst WORK_ITEM_SOURCES: readonly WorkItemSource[] = ['github-issue', 'github-pr', 'linear-issue', 'manual'];\nconst REJECTION_CODES: readonly FactoryRuleRejectionCode[] = [\n 'forbidden',\n 'invalid_transition',\n 'missing_binding',\n 'stale',\n 'timeout',\n 'rule_error',\n 'causal_depth_exceeded',\n 'repeated_transition',\n];\n\nexport class FactoryRuleValidationError extends Error {\n readonly code = 'invalid_factory_rule';\n\n constructor(message: string) {\n super(message);\n this.name = 'FactoryRuleValidationError';\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nfunction assertExactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {\n const allowed = new Set(keys);\n if (Object.keys(value).some(key => !allowed.has(key))) {\n throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);\n }\n}\n\nfunction boundedString(value: unknown, label: string, max: number, pattern?: RegExp): string {\n if (typeof value !== 'string') throw new FactoryRuleValidationError(`${label} must be a string.`);\n const normalized = value.trim();\n if (normalized.length === 0 || normalized.length > max || (pattern && !pattern.test(normalized))) {\n throw new FactoryRuleValidationError(`${label} is invalid.`);\n }\n return normalized;\n}\n\nfunction optionalBoundedString(value: unknown, label: string, max: number): string | undefined {\n if (value === undefined) return undefined;\n return boundedString(value, label, max);\n}\n\nfunction enumValue<T extends string>(value: unknown, allowed: readonly T[], label: string): T {\n if (typeof value !== 'string' || !allowed.includes(value as T)) {\n throw new FactoryRuleValidationError(`${label} is invalid.`);\n }\n return value as T;\n}\n\nexport function normalizeFactoryRuleJsonValue(\n value: unknown,\n depth = 0,\n seen = new Set<object>(),\n): FactoryRuleJsonValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new FactoryRuleValidationError('Rule metadata must contain finite numbers.');\n return value;\n }\n if (depth >= MAX_JSON_DEPTH || (typeof value !== 'object' && !Array.isArray(value))) {\n throw new FactoryRuleValidationError('Rule metadata is not bounded JSON.');\n }\n if (seen.has(value as object)) throw new FactoryRuleValidationError('Rule metadata must not contain cycles.');\n seen.add(value as object);\n try {\n if (Array.isArray(value)) {\n if (value.length > MAX_JSON_COLLECTION_SIZE) {\n throw new FactoryRuleValidationError('Rule metadata contains too many entries.');\n }\n return value.map(entry => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));\n }\n if (!isPlainObject(value)) throw new FactoryRuleValidationError('Rule metadata must use plain objects.');\n const entries = Object.entries(value);\n if (entries.length > MAX_JSON_COLLECTION_SIZE) {\n throw new FactoryRuleValidationError('Rule metadata contains too many fields.');\n }\n const sanitized: Record<string, FactoryRuleJsonValue> = {};\n for (const [key, entry] of entries) {\n const normalizedKey = boundedString(key, 'Rule metadata key', 128, IDENTIFIER_RE);\n sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey)\n ? '[REDACTED]'\n : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);\n }\n return sanitized;\n } finally {\n seen.delete(value as object);\n }\n}\n\nfunction sanitizeMetadata(value: unknown): Record<string, FactoryRuleJsonValue> | undefined {\n if (value === undefined) return undefined;\n const sanitized = normalizeFactoryRuleJsonValue(value);\n if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError('Rule metadata must be an object.');\n if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {\n throw new FactoryRuleValidationError('Rule metadata is too large.');\n }\n return sanitized;\n}\n\nfunction validateBoardRules(rules: unknown, label: string): asserts rules is FactoryBoardRules {\n if (!isPlainObject(rules)) throw new FactoryRuleValidationError(`${label} must be an object.`);\n for (const [stage, sources] of Object.entries(rules)) {\n enumValue(stage, FACTORY_RULE_STAGES, `${label} stage`);\n if (!isPlainObject(sources)) throw new FactoryRuleValidationError(`${label}.${stage} must be an object.`);\n for (const [source, leaf] of Object.entries(sources)) {\n enumValue(source, FACTORY_RULE_SOURCES, `${label}.${stage} source`);\n if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`${label}.${stage}.${source} must be an object.`);\n assertExactKeys(leaf, ['onEnter', 'onExit'], `${label}.${stage}.${source}`);\n for (const handler of Object.values(leaf)) {\n if (handler !== undefined && typeof handler !== 'function') {\n throw new FactoryRuleValidationError(`${label}.${stage}.${source} handlers must be functions.`);\n }\n }\n }\n }\n}\n\nexport function assertFactoryRules(rules: unknown): asserts rules is FactoryRules {\n if (!isPlainObject(rules)) throw new FactoryRuleValidationError('Factory rules must be an object.');\n assertExactKeys(rules, ['version', 'work', 'review', 'tools', 'github', 'linear'], 'Factory rules');\n boundedString(rules.version, 'Factory rule version', MAX_VERSION_LENGTH);\n validateBoardRules(rules.work, 'Factory rules.work');\n validateBoardRules(rules.review, 'Factory rules.review');\n\n if (!isPlainObject(rules.tools)) throw new FactoryRuleValidationError('Factory rules.tools must be an object.');\n for (const [toolName, leaf] of Object.entries(rules.tools)) {\n boundedString(toolName, 'Factory tool name', 128, IDENTIFIER_RE);\n if (!isPlainObject(leaf))\n throw new FactoryRuleValidationError(`Factory rules.tools.${toolName} must be an object.`);\n assertExactKeys(leaf, ['onResult'], `Factory rules.tools.${toolName}`);\n if (leaf.onResult !== undefined && typeof leaf.onResult !== 'function') {\n throw new FactoryRuleValidationError(`Factory rules.tools.${toolName}.onResult must be a function.`);\n }\n }\n\n if (!isPlainObject(rules.github)) throw new FactoryRuleValidationError('Factory rules.github must be an object.');\n for (const [event, leaf] of Object.entries(rules.github)) {\n enumValue(event, FACTORY_GITHUB_EVENTS, 'Factory GitHub event');\n if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`Factory rules.github.${event} must be an object.`);\n assertExactKeys(leaf, ['onEvent'], `Factory rules.github.${event}`);\n if (leaf.onEvent !== undefined && typeof leaf.onEvent !== 'function') {\n throw new FactoryRuleValidationError(`Factory rules.github.${event}.onEvent must be a function.`);\n }\n }\n\n if (!isPlainObject(rules.linear)) throw new FactoryRuleValidationError('Factory rules.linear must be an object.');\n for (const [event, leaf] of Object.entries(rules.linear)) {\n enumValue(event, FACTORY_LINEAR_EVENTS, 'Factory Linear event');\n if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`Factory rules.linear.${event} must be an object.`);\n assertExactKeys(leaf, ['onEvent'], `Factory rules.linear.${event}`);\n if (leaf.onEvent !== undefined && typeof leaf.onEvent !== 'function') {\n throw new FactoryRuleValidationError(`Factory rules.linear.${event}.onEvent must be a function.`);\n }\n }\n}\n\nfunction commonCommitFields(value: Record<string, unknown>): { idempotencyKey: string } {\n return {\n idempotencyKey: boundedString(value.idempotencyKey, 'Factory decision idempotencyKey', MAX_IDEMPOTENCY_KEY_LENGTH),\n };\n}\n\nexport function validateFactoryRuleDecision(value: unknown, causalDepth = 0): FactoryRuleDecision {\n if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {\n throw new FactoryRuleValidationError('Factory rule causal depth exceeded.');\n }\n if (!isPlainObject(value)) throw new FactoryRuleValidationError('Factory rule decision must be an object.');\n const type = value.type;\n if (typeof type !== 'string') throw new FactoryRuleValidationError('Factory rule decision type is required.');\n\n switch (type) {\n case 'reject': {\n assertExactKeys(value, ['type', 'code', 'reason'], 'Factory reject decision');\n return {\n type,\n code: enumValue(value.code, REJECTION_CODES, 'Factory rejection code'),\n reason: boundedString(value.reason, 'Factory rejection reason', MAX_REASON_LENGTH),\n };\n }\n case 'transition': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'board', 'stage', 'message', 'reenter'],\n 'Factory transition decision',\n );\n if (value.reenter !== undefined && typeof value.reenter !== 'boolean') {\n throw new FactoryRuleValidationError('Factory transition reenter must be a boolean.');\n }\n let message: { text: string; role?: string } | undefined;\n if (value.message !== undefined) {\n if (!isPlainObject(value.message)) {\n throw new FactoryRuleValidationError('Factory transition message must be an object.');\n }\n assertExactKeys(value.message, ['text', 'role'], 'Factory transition message');\n const role =\n value.message.role === undefined\n ? undefined\n : boundedString(value.message.role, 'Factory transition message role', MAX_ROLE_LENGTH, IDENTIFIER_RE);\n message = {\n text: boundedString(value.message.text, 'Factory transition message text', MAX_MESSAGE_LENGTH),\n ...(role ? { role } : {}),\n };\n }\n return {\n type,\n ...commonCommitFields(value),\n board: enumValue(value.board, FACTORY_RULE_BOARDS, 'Factory transition board'),\n stage: enumValue(value.stage, FACTORY_RULE_STAGES, 'Factory transition stage'),\n ...(message ? { message } : {}),\n ...(value.reenter === true ? { reenter: true } : {}),\n };\n }\n case 'upsertLinkedWorkItem': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'board', 'source', 'sourceKey', 'title', 'url', 'stage', 'metadata'],\n 'Factory linked work item decision',\n );\n const url = value.url;\n if (url !== null && (typeof url !== 'string' || url.length > MAX_URL_LENGTH || !/^https?:\\/\\//.test(url))) {\n throw new FactoryRuleValidationError('Factory linked work item URL is invalid.');\n }\n const metadata = sanitizeMetadata(value.metadata);\n return {\n type,\n ...commonCommitFields(value),\n board: enumValue(value.board, FACTORY_RULE_BOARDS, 'Factory linked work item board'),\n source: enumValue(value.source, WORK_ITEM_SOURCES, 'Factory linked work item source'),\n sourceKey: boundedString(value.sourceKey, 'Factory linked work item sourceKey', MAX_SOURCE_KEY_LENGTH),\n title: boundedString(value.title, 'Factory linked work item title', MAX_TITLE_LENGTH),\n url,\n stage: enumValue(value.stage, FACTORY_RULE_STAGES, 'Factory linked work item stage'),\n ...(metadata ? { metadata } : {}),\n };\n }\n case 'invokeSkill': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'role', 'skillName', 'prompt', 'arguments', 'precedingMessage', 'cancelInFlight'],\n 'Factory invoke skill decision',\n );\n // A run activates a skill or carries a prompt, never both: they are two\n // ways to author the same kickoff message, so accepting both would leave\n // the dispatcher picking a winner.\n if ((value.skillName === undefined) === (value.prompt === undefined)) {\n throw new FactoryRuleValidationError('Factory skill invocation needs exactly one of skillName or prompt.');\n }\n const args = optionalBoundedString(value.arguments, 'Factory skill arguments', MAX_ARGUMENTS_LENGTH);\n const precedingMessage = optionalBoundedString(\n value.precedingMessage,\n 'Factory skill preceding message',\n MAX_MESSAGE_LENGTH,\n );\n if (value.cancelInFlight !== undefined && typeof value.cancelInFlight !== 'boolean') {\n throw new FactoryRuleValidationError('Factory skill cancelInFlight must be a boolean.');\n }\n return {\n type,\n ...commonCommitFields(value),\n role: boundedString(value.role, 'Factory skill role', MAX_ROLE_LENGTH, IDENTIFIER_RE),\n ...(value.skillName === undefined\n ? { prompt: boundedString(value.prompt, 'Factory skill prompt', MAX_MESSAGE_LENGTH) }\n : { skillName: boundedString(value.skillName, 'Factory skill name', MAX_SKILL_NAME_LENGTH, SKILL_NAME_RE) }),\n ...(args ? { arguments: args } : {}),\n ...(precedingMessage ? { precedingMessage } : {}),\n ...(value.cancelInFlight === true ? { cancelInFlight: true } : {}),\n };\n }\n case 'sendMessage': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'role', 'message', 'priority', 'idleBehavior', 'prepareBinding'],\n 'Factory send message decision',\n );\n const priority =\n value.priority === undefined\n ? undefined\n : enumValue(value.priority, ['medium', 'high', 'urgent'] as const, 'Factory message priority');\n const idleBehavior =\n value.idleBehavior === undefined\n ? undefined\n : enumValue(value.idleBehavior, ['persist', 'wake'] as const, 'Factory message idle behavior');\n if (value.prepareBinding !== undefined && typeof value.prepareBinding !== 'boolean') {\n throw new FactoryRuleValidationError('Factory message prepareBinding must be a boolean.');\n }\n return {\n type,\n ...commonCommitFields(value),\n role: boundedString(value.role, 'Factory message role', MAX_ROLE_LENGTH, IDENTIFIER_RE),\n message: boundedString(value.message, 'Factory message', MAX_MESSAGE_LENGTH),\n ...(priority ? { priority } : {}),\n ...(idleBehavior ? { idleBehavior } : {}),\n ...(value.prepareBinding === true ? { prepareBinding: true } : {}),\n };\n }\n case 'notify': {\n assertExactKeys(value, ['type', 'idempotencyKey', 'title', 'body', 'level'], 'Factory notify decision');\n const body = optionalBoundedString(value.body, 'Factory notification body', MAX_MESSAGE_LENGTH);\n const level =\n value.level === undefined\n ? undefined\n : enumValue(value.level, ['info', 'warning', 'error'] as const, 'Factory notification level');\n return {\n type,\n ...commonCommitFields(value),\n title: boundedString(value.title, 'Factory notification title', MAX_TITLE_LENGTH),\n ...(body ? { body } : {}),\n ...(level ? { level } : {}),\n };\n }\n default:\n throw new FactoryRuleValidationError('Factory rule decision type is unsupported.');\n }\n}\n\nexport function validateFactoryRuleDecisions(values: readonly unknown[], causalDepth = 0): FactoryCommitDecision[] {\n if (values.length > MAX_JSON_COLLECTION_SIZE) {\n throw new FactoryRuleValidationError('Factory rule produced too many decisions.');\n }\n const decisions: FactoryCommitDecision[] = [];\n for (const value of values) {\n const decision = validateFactoryRuleDecision(value, causalDepth);\n if (decision.type === 'reject') {\n throw new FactoryRuleValidationError('A rejection cannot be persisted with commit decisions.');\n }\n decisions.push(decision);\n }\n const keys = decisions.map(decision => decision.idempotencyKey);\n if (new Set(keys).size !== keys.length) {\n throw new FactoryRuleValidationError('Factory decisions require unique idempotency keys.');\n }\n return decisions;\n}\n"],"mappings":";;AAiBA,MAAa,gCAAgC;AAE7C,MAAM,qBAAqB;AAC3B,MAAM,6BAA6B;AACnC,MAAM,oBAAoB;AAC1B,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;AAC3B,MAAM,uBAAuB;AAC7B,MAAM,kBAAkB;AACxB,MAAM,wBAAwB;AAC9B,MAAM,wBAAwB;AAC9B,MAAM,iBAAiB;AACvB,MAAM,2BAA2B;AACjC,MAAM,iBAAiB;AACvB,MAAM,2BAA2B;AAEjC,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;AACtB,MAAM,mBAAmB;AACzB,MAAM,oBAA+C;CAAC;CAAgB;CAAa;CAAgB;AAAQ;AAC3G,MAAM,kBAAuD;CAC3D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,IAAa,6BAAb,cAAgD,MAAM;CACpD,OAAgB;CAEhB,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAS,cAAc,OAAkD;CACvE,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG,OAAO;CAChF,MAAM,YAAY,OAAO,eAAe,KAAK;CAC7C,OAAO,cAAc,OAAO,aAAa,cAAc;AACzD;AAEA,SAAS,gBAAgB,OAAgC,MAAyB,OAAqB;CACrG,MAAM,UAAU,IAAI,IAAI,IAAI;CAC5B,IAAI,OAAO,KAAK,KAAK,CAAC,CAAC,MAAK,QAAO,CAAC,QAAQ,IAAI,GAAG,CAAC,GAClD,MAAM,IAAI,2BAA2B,GAAG,MAAM,gCAAgC;AAElF;AAEA,SAAS,cAAc,OAAgB,OAAe,KAAa,SAA0B;CAC3F,IAAI,OAAO,UAAU,UAAU,MAAM,IAAI,2BAA2B,GAAG,MAAM,mBAAmB;CAChG,MAAM,aAAa,MAAM,KAAK;CAC9B,IAAI,WAAW,WAAW,KAAK,WAAW,SAAS,OAAQ,WAAW,CAAC,QAAQ,KAAK,UAAU,GAC5F,MAAM,IAAI,2BAA2B,GAAG,MAAM,aAAa;CAE7D,OAAO;AACT;AAEA,SAAS,sBAAsB,OAAgB,OAAe,KAAiC;CAC7F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;CAChC,OAAO,cAAc,OAAO,OAAO,GAAG;AACxC;AAEA,SAAS,UAA4B,OAAgB,SAAuB,OAAkB;CAC5F,IAAI,OAAO,UAAU,YAAY,CAAC,QAAQ,SAAS,KAAU,GAC3D,MAAM,IAAI,2BAA2B,GAAG,MAAM,aAAa;CAE7D,OAAO;AACT;AAEA,SAAgB,8BACd,OACA,QAAQ,GACR,uBAAO,IAAI,IAAY,GACD;CACtB,IAAI,UAAU,QAAQ,OAAO,UAAU,aAAa,OAAO,UAAU,UAAU,OAAO;CACtF,IAAI,OAAO,UAAU,UAAU;EAC7B,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG,MAAM,IAAI,2BAA2B,4CAA4C;EAC9G,OAAO;CACT;CACA,IAAI,SAAS,kBAAmB,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAC/E,MAAM,IAAI,2BAA2B,oCAAoC;CAE3E,IAAI,KAAK,IAAI,KAAe,GAAG,MAAM,IAAI,2BAA2B,wCAAwC;CAC5G,KAAK,IAAI,KAAe;CACxB,IAAI;EACF,IAAI,MAAM,QAAQ,KAAK,GAAG;GACxB,IAAI,MAAM,SAAS,0BACjB,MAAM,IAAI,2BAA2B,0CAA0C;GAEjF,OAAO,MAAM,KAAI,UAAS,8BAA8B,OAAO,QAAQ,GAAG,IAAI,CAAC;EACjF;EACA,IAAI,CAAC,cAAc,KAAK,GAAG,MAAM,IAAI,2BAA2B,uCAAuC;EACvG,MAAM,UAAU,OAAO,QAAQ,KAAK;EACpC,IAAI,QAAQ,SAAS,0BACnB,MAAM,IAAI,2BAA2B,yCAAyC;EAEhF,MAAM,YAAkD,CAAC;EACzD,KAAK,MAAM,CAAC,KAAK,UAAU,SAAS;GAClC,MAAM,gBAAgB,cAAc,KAAK,qBAAqB,KAAK,aAAa;GAChF,UAAU,iBAAiB,iBAAiB,KAAK,aAAa,IAC1D,eACA,8BAA8B,OAAO,QAAQ,GAAG,IAAI;EAC1D;EACA,OAAO;CACT,UAAU;EACR,KAAK,OAAO,KAAe;CAC7B;AACF;AAEA,SAAS,iBAAiB,OAAkE;CAC1F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;CAChC,MAAM,YAAY,8BAA8B,KAAK;CACrD,IAAI,CAAC,cAAc,SAAS,GAAG,MAAM,IAAI,2BAA2B,kCAAkC;CACtG,IAAI,KAAK,UAAU,SAAS,CAAC,CAAC,SAAS,0BACrC,MAAM,IAAI,2BAA2B,6BAA6B;CAEpE,OAAO;AACT;AAEA,SAAS,mBAAmB,OAAgB,OAAmD;CAC7F,IAAI,CAAC,cAAc,KAAK,GAAG,MAAM,IAAI,2BAA2B,GAAG,MAAM,oBAAoB;CAC7F,KAAK,MAAM,CAAC,OAAO,YAAY,OAAO,QAAQ,KAAK,GAAG;EACpD,UAAU,OAAO,qBAAqB,GAAG,MAAM,OAAO;EACtD,IAAI,CAAC,cAAc,OAAO,GAAG,MAAM,IAAI,2BAA2B,GAAG,MAAM,GAAG,MAAM,oBAAoB;EACxG,KAAK,MAAM,CAAC,QAAQ,SAAS,OAAO,QAAQ,OAAO,GAAG;GACpD,UAAU,QAAQ,sBAAsB,GAAG,MAAM,GAAG,MAAM,QAAQ;GAClE,IAAI,CAAC,cAAc,IAAI,GAAG,MAAM,IAAI,2BAA2B,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,oBAAoB;GAC/G,gBAAgB,MAAM,CAAC,WAAW,QAAQ,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ;GAC1E,KAAK,MAAM,WAAW,OAAO,OAAO,IAAI,GACtC,IAAI,YAAY,KAAA,KAAa,OAAO,YAAY,YAC9C,MAAM,IAAI,2BAA2B,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,6BAA6B;EAGpG;CACF;AACF;AAEA,SAAgB,mBAAmB,OAA+C;CAChF,IAAI,CAAC,cAAc,KAAK,GAAG,MAAM,IAAI,2BAA2B,kCAAkC;CAClG,gBAAgB,OAAO;EAAC;EAAW;EAAQ;EAAU;EAAS;EAAU;CAAQ,GAAG,eAAe;CAClG,cAAc,MAAM,SAAS,wBAAwB,kBAAkB;CACvE,mBAAmB,MAAM,MAAM,oBAAoB;CACnD,mBAAmB,MAAM,QAAQ,sBAAsB;CAEvD,IAAI,CAAC,cAAc,MAAM,KAAK,GAAG,MAAM,IAAI,2BAA2B,wCAAwC;CAC9G,KAAK,MAAM,CAAC,UAAU,SAAS,OAAO,QAAQ,MAAM,KAAK,GAAG;EAC1D,cAAc,UAAU,qBAAqB,KAAK,aAAa;EAC/D,IAAI,CAAC,cAAc,IAAI,GACrB,MAAM,IAAI,2BAA2B,uBAAuB,SAAS,oBAAoB;EAC3F,gBAAgB,MAAM,CAAC,UAAU,GAAG,uBAAuB,UAAU;EACrE,IAAI,KAAK,aAAa,KAAA,KAAa,OAAO,KAAK,aAAa,YAC1D,MAAM,IAAI,2BAA2B,uBAAuB,SAAS,8BAA8B;CAEvG;CAEA,IAAI,CAAC,cAAc,MAAM,MAAM,GAAG,MAAM,IAAI,2BAA2B,yCAAyC;CAChH,KAAK,MAAM,CAAC,OAAO,SAAS,OAAO,QAAQ,MAAM,MAAM,GAAG;EACxD,UAAU,OAAO,uBAAuB,sBAAsB;EAC9D,IAAI,CAAC,cAAc,IAAI,GAAG,MAAM,IAAI,2BAA2B,wBAAwB,MAAM,oBAAoB;EACjH,gBAAgB,MAAM,CAAC,SAAS,GAAG,wBAAwB,OAAO;EAClE,IAAI,KAAK,YAAY,KAAA,KAAa,OAAO,KAAK,YAAY,YACxD,MAAM,IAAI,2BAA2B,wBAAwB,MAAM,6BAA6B;CAEpG;CAEA,IAAI,CAAC,cAAc,MAAM,MAAM,GAAG,MAAM,IAAI,2BAA2B,yCAAyC;CAChH,KAAK,MAAM,CAAC,OAAO,SAAS,OAAO,QAAQ,MAAM,MAAM,GAAG;EACxD,UAAU,OAAO,uBAAuB,sBAAsB;EAC9D,IAAI,CAAC,cAAc,IAAI,GAAG,MAAM,IAAI,2BAA2B,wBAAwB,MAAM,oBAAoB;EACjH,gBAAgB,MAAM,CAAC,SAAS,GAAG,wBAAwB,OAAO;EAClE,IAAI,KAAK,YAAY,KAAA,KAAa,OAAO,KAAK,YAAY,YACxD,MAAM,IAAI,2BAA2B,wBAAwB,MAAM,6BAA6B;CAEpG;AACF;AAEA,SAAS,mBAAmB,OAA4D;CACtF,OAAO,EACL,gBAAgB,cAAc,MAAM,gBAAgB,mCAAmC,0BAA0B,EACnH;AACF;AAEA,SAAgB,4BAA4B,OAAgB,cAAc,GAAwB;CAChG,IAAI,cAAA,GACF,MAAM,IAAI,2BAA2B,qCAAqC;CAE5E,IAAI,CAAC,cAAc,KAAK,GAAG,MAAM,IAAI,2BAA2B,0CAA0C;CAC1G,MAAM,OAAO,MAAM;CACnB,IAAI,OAAO,SAAS,UAAU,MAAM,IAAI,2BAA2B,yCAAyC;CAE5G,QAAQ,MAAR;EACE,KAAK;GACH,gBAAgB,OAAO;IAAC;IAAQ;IAAQ;GAAQ,GAAG,yBAAyB;GAC5E,OAAO;IACL;IACA,MAAM,UAAU,MAAM,MAAM,iBAAiB,wBAAwB;IACrE,QAAQ,cAAc,MAAM,QAAQ,4BAA4B,iBAAiB;GACnF;EAEF,KAAK,cAAc;GACjB,gBACE,OACA;IAAC;IAAQ;IAAkB;IAAS;IAAS;IAAW;GAAS,GACjE,6BACF;GACA,IAAI,MAAM,YAAY,KAAA,KAAa,OAAO,MAAM,YAAY,WAC1D,MAAM,IAAI,2BAA2B,+CAA+C;GAEtF,IAAI;GACJ,IAAI,MAAM,YAAY,KAAA,GAAW;IAC/B,IAAI,CAAC,cAAc,MAAM,OAAO,GAC9B,MAAM,IAAI,2BAA2B,+CAA+C;IAEtF,gBAAgB,MAAM,SAAS,CAAC,QAAQ,MAAM,GAAG,4BAA4B;IAC7E,MAAM,OACJ,MAAM,QAAQ,SAAS,KAAA,IACnB,KAAA,IACA,cAAc,MAAM,QAAQ,MAAM,mCAAmC,iBAAiB,aAAa;IACzG,UAAU;KACR,MAAM,cAAc,MAAM,QAAQ,MAAM,mCAAmC,kBAAkB;KAC7F,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;IACzB;GACF;GACA,OAAO;IACL;IACA,GAAG,mBAAmB,KAAK;IAC3B,OAAO,UAAU,MAAM,OAAO,qBAAqB,0BAA0B;IAC7E,OAAO,UAAU,MAAM,OAAO,qBAAqB,0BAA0B;IAC7E,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;IAC7B,GAAI,MAAM,YAAY,OAAO,EAAE,SAAS,KAAK,IAAI,CAAC;GACpD;EACF;EACA,KAAK,wBAAwB;GAC3B,gBACE,OACA;IAAC;IAAQ;IAAkB;IAAS;IAAU;IAAa;IAAS;IAAO;IAAS;GAAU,GAC9F,mCACF;GACA,MAAM,MAAM,MAAM;GAClB,IAAI,QAAQ,SAAS,OAAO,QAAQ,YAAY,IAAI,SAAS,kBAAkB,CAAC,eAAe,KAAK,GAAG,IACrG,MAAM,IAAI,2BAA2B,0CAA0C;GAEjF,MAAM,WAAW,iBAAiB,MAAM,QAAQ;GAChD,OAAO;IACL;IACA,GAAG,mBAAmB,KAAK;IAC3B,OAAO,UAAU,MAAM,OAAO,qBAAqB,gCAAgC;IACnF,QAAQ,UAAU,MAAM,QAAQ,mBAAmB,iCAAiC;IACpF,WAAW,cAAc,MAAM,WAAW,sCAAsC,qBAAqB;IACrG,OAAO,cAAc,MAAM,OAAO,kCAAkC,gBAAgB;IACpF;IACA,OAAO,UAAU,MAAM,OAAO,qBAAqB,gCAAgC;IACnF,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;GACjC;EACF;EACA,KAAK,eAAe;GAClB,gBACE,OACA;IAAC;IAAQ;IAAkB;IAAQ;IAAa;IAAU;IAAa;IAAoB;GAAgB,GAC3G,+BACF;GAIA,IAAK,MAAM,cAAc,KAAA,OAAgB,MAAM,WAAW,KAAA,IACxD,MAAM,IAAI,2BAA2B,oEAAoE;GAE3G,MAAM,OAAO,sBAAsB,MAAM,WAAW,2BAA2B,oBAAoB;GACnG,MAAM,mBAAmB,sBACvB,MAAM,kBACN,mCACA,kBACF;GACA,IAAI,MAAM,mBAAmB,KAAA,KAAa,OAAO,MAAM,mBAAmB,WACxE,MAAM,IAAI,2BAA2B,iDAAiD;GAExF,OAAO;IACL;IACA,GAAG,mBAAmB,KAAK;IAC3B,MAAM,cAAc,MAAM,MAAM,sBAAsB,iBAAiB,aAAa;IACpF,GAAI,MAAM,cAAc,KAAA,IACpB,EAAE,QAAQ,cAAc,MAAM,QAAQ,wBAAwB,kBAAkB,EAAE,IAClF,EAAE,WAAW,cAAc,MAAM,WAAW,sBAAsB,uBAAuB,aAAa,EAAE;IAC5G,GAAI,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAClC,GAAI,mBAAmB,EAAE,iBAAiB,IAAI,CAAC;IAC/C,GAAI,MAAM,mBAAmB,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;GAClE;EACF;EACA,KAAK,eAAe;GAClB,gBACE,OACA;IAAC;IAAQ;IAAkB;IAAQ;IAAW;IAAY;IAAgB;GAAgB,GAC1F,+BACF;GACA,MAAM,WACJ,MAAM,aAAa,KAAA,IACf,KAAA,IACA,UAAU,MAAM,UAAU;IAAC;IAAU;IAAQ;GAAQ,GAAY,0BAA0B;GACjG,MAAM,eACJ,MAAM,iBAAiB,KAAA,IACnB,KAAA,IACA,UAAU,MAAM,cAAc,CAAC,WAAW,MAAM,GAAY,+BAA+B;GACjG,IAAI,MAAM,mBAAmB,KAAA,KAAa,OAAO,MAAM,mBAAmB,WACxE,MAAM,IAAI,2BAA2B,mDAAmD;GAE1F,OAAO;IACL;IACA,GAAG,mBAAmB,KAAK;IAC3B,MAAM,cAAc,MAAM,MAAM,wBAAwB,iBAAiB,aAAa;IACtF,SAAS,cAAc,MAAM,SAAS,mBAAmB,kBAAkB;IAC3E,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;IAC/B,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;IACvC,GAAI,MAAM,mBAAmB,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;GAClE;EACF;EACA,KAAK,UAAU;GACb,gBAAgB,OAAO;IAAC;IAAQ;IAAkB;IAAS;IAAQ;GAAO,GAAG,yBAAyB;GACtG,MAAM,OAAO,sBAAsB,MAAM,MAAM,6BAA6B,kBAAkB;GAC9F,MAAM,QACJ,MAAM,UAAU,KAAA,IACZ,KAAA,IACA,UAAU,MAAM,OAAO;IAAC;IAAQ;IAAW;GAAO,GAAY,4BAA4B;GAChG,OAAO;IACL;IACA,GAAG,mBAAmB,KAAK;IAC3B,OAAO,cAAc,MAAM,OAAO,8BAA8B,gBAAgB;IAChF,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;IACvB,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;GAC3B;EACF;EACA,SACE,MAAM,IAAI,2BAA2B,4CAA4C;CACrF;AACF;AAEA,SAAgB,6BAA6B,QAA4B,cAAc,GAA4B;CACjH,IAAI,OAAO,SAAS,0BAClB,MAAM,IAAI,2BAA2B,2CAA2C;CAElF,MAAM,YAAqC,CAAC;CAC5C,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,4BAA4B,OAAO,WAAW;EAC/D,IAAI,SAAS,SAAS,UACpB,MAAM,IAAI,2BAA2B,wDAAwD;EAE/F,UAAU,KAAK,QAAQ;CACzB;CACA,MAAM,OAAO,UAAU,KAAI,aAAY,SAAS,cAAc;CAC9D,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC,SAAS,KAAK,QAC9B,MAAM,IAAI,2BAA2B,oDAAoD;CAE3F,OAAO;AACT"}
1
+ {"version":3,"file":"validation.js","names":[],"sources":["../../src/rules/validation.ts"],"sourcesContent":["import {\n FACTORY_GITHUB_EVENTS,\n FACTORY_LINEAR_EVENTS,\n FACTORY_RULE_BOARDS,\n FACTORY_RULE_SOURCES,\n FACTORY_RULE_STAGES,\n} from './types.js';\nimport type {\n FactoryBoardRules,\n FactoryCommitDecision,\n FactoryRuleDecision,\n FactoryRuleJsonValue,\n FactoryRules,\n FactoryRuleRejectionCode,\n WorkItemSource,\n} from './types.js';\n\nexport const MAX_FACTORY_RULE_CAUSAL_DEPTH = 8;\n\nconst MAX_VERSION_LENGTH = 128;\nconst MAX_IDEMPOTENCY_KEY_LENGTH = 256;\nconst MAX_REASON_LENGTH = 512;\nconst MAX_TITLE_LENGTH = 512;\nconst MAX_MESSAGE_LENGTH = 8_192;\nconst MAX_ARGUMENTS_LENGTH = 4_096;\nconst MAX_ROLE_LENGTH = 32;\nconst MAX_SKILL_NAME_LENGTH = 128;\nconst MAX_SOURCE_KEY_LENGTH = 256;\nconst MAX_URL_LENGTH = 2_048;\nconst MAX_METADATA_JSON_LENGTH = 16_384;\nconst MAX_JSON_DEPTH = 8;\nconst MAX_JSON_COLLECTION_SIZE = 100;\n\nconst IDENTIFIER_RE = /^[a-z0-9][a-z0-9_-]*$/i;\nconst SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nconst SENSITIVE_KEY_RE = /(?:authorization|cookie|credential|password|secret|token)/i;\nconst WORK_ITEM_SOURCES: readonly WorkItemSource[] = ['github-issue', 'github-pr', 'linear-issue', 'manual'];\nconst REJECTION_CODES: readonly FactoryRuleRejectionCode[] = [\n 'forbidden',\n 'invalid_transition',\n 'missing_binding',\n 'stale',\n 'timeout',\n 'rule_error',\n 'causal_depth_exceeded',\n 'repeated_transition',\n];\n\nexport class FactoryRuleValidationError extends Error {\n readonly code = 'invalid_factory_rule';\n\n constructor(message: string) {\n super(message);\n this.name = 'FactoryRuleValidationError';\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nfunction assertExactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {\n const allowed = new Set(keys);\n if (Object.keys(value).some(key => !allowed.has(key))) {\n throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);\n }\n}\n\nfunction boundedString(value: unknown, label: string, max: number, pattern?: RegExp): string {\n if (typeof value !== 'string') throw new FactoryRuleValidationError(`${label} must be a string.`);\n const normalized = value.trim();\n if (normalized.length === 0 || normalized.length > max || (pattern && !pattern.test(normalized))) {\n throw new FactoryRuleValidationError(`${label} is invalid.`);\n }\n return normalized;\n}\n\nfunction optionalBoundedString(value: unknown, label: string, max: number): string | undefined {\n if (value === undefined) return undefined;\n return boundedString(value, label, max);\n}\n\nfunction enumValue<T extends string>(value: unknown, allowed: readonly T[], label: string): T {\n if (typeof value !== 'string' || !allowed.includes(value as T)) {\n throw new FactoryRuleValidationError(`${label} is invalid.`);\n }\n return value as T;\n}\n\nexport function normalizeFactoryRuleJsonValue(\n value: unknown,\n depth = 0,\n seen = new Set<object>(),\n): FactoryRuleJsonValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new FactoryRuleValidationError('Rule metadata must contain finite numbers.');\n return value;\n }\n if (depth >= MAX_JSON_DEPTH || (typeof value !== 'object' && !Array.isArray(value))) {\n throw new FactoryRuleValidationError('Rule metadata is not bounded JSON.');\n }\n if (seen.has(value as object)) throw new FactoryRuleValidationError('Rule metadata must not contain cycles.');\n seen.add(value as object);\n try {\n if (Array.isArray(value)) {\n if (value.length > MAX_JSON_COLLECTION_SIZE) {\n throw new FactoryRuleValidationError('Rule metadata contains too many entries.');\n }\n return value.map(entry => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));\n }\n if (!isPlainObject(value)) throw new FactoryRuleValidationError('Rule metadata must use plain objects.');\n const entries = Object.entries(value);\n if (entries.length > MAX_JSON_COLLECTION_SIZE) {\n throw new FactoryRuleValidationError('Rule metadata contains too many fields.');\n }\n const sanitized: Record<string, FactoryRuleJsonValue> = {};\n for (const [key, entry] of entries) {\n const normalizedKey = boundedString(key, 'Rule metadata key', 128, IDENTIFIER_RE);\n sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey)\n ? '[REDACTED]'\n : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);\n }\n return sanitized;\n } finally {\n seen.delete(value as object);\n }\n}\n\nfunction sanitizeMetadata(value: unknown): Record<string, FactoryRuleJsonValue> | undefined {\n if (value === undefined) return undefined;\n const sanitized = normalizeFactoryRuleJsonValue(value);\n if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError('Rule metadata must be an object.');\n if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {\n throw new FactoryRuleValidationError('Rule metadata is too large.');\n }\n return sanitized;\n}\n\nfunction validateBoardRules(rules: unknown, label: string): asserts rules is FactoryBoardRules {\n if (!isPlainObject(rules)) throw new FactoryRuleValidationError(`${label} must be an object.`);\n for (const [stage, sources] of Object.entries(rules)) {\n enumValue(stage, FACTORY_RULE_STAGES, `${label} stage`);\n if (!isPlainObject(sources)) throw new FactoryRuleValidationError(`${label}.${stage} must be an object.`);\n for (const [source, leaf] of Object.entries(sources)) {\n enumValue(source, FACTORY_RULE_SOURCES, `${label}.${stage} source`);\n if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`${label}.${stage}.${source} must be an object.`);\n assertExactKeys(leaf, ['onEnter', 'onExit'], `${label}.${stage}.${source}`);\n for (const handler of Object.values(leaf)) {\n if (handler !== undefined && typeof handler !== 'function') {\n throw new FactoryRuleValidationError(`${label}.${stage}.${source} handlers must be functions.`);\n }\n }\n }\n }\n}\n\nexport function assertFactoryRules(rules: unknown): asserts rules is FactoryRules {\n if (!isPlainObject(rules)) throw new FactoryRuleValidationError('Factory rules must be an object.');\n assertExactKeys(rules, ['version', 'work', 'review', 'tools', 'github', 'linear'], 'Factory rules');\n boundedString(rules.version, 'Factory rule version', MAX_VERSION_LENGTH);\n validateBoardRules(rules.work, 'Factory rules.work');\n validateBoardRules(rules.review, 'Factory rules.review');\n\n if (!isPlainObject(rules.tools)) throw new FactoryRuleValidationError('Factory rules.tools must be an object.');\n for (const [toolName, leaf] of Object.entries(rules.tools)) {\n boundedString(toolName, 'Factory tool name', 128, IDENTIFIER_RE);\n if (!isPlainObject(leaf))\n throw new FactoryRuleValidationError(`Factory rules.tools.${toolName} must be an object.`);\n assertExactKeys(leaf, ['onResult'], `Factory rules.tools.${toolName}`);\n if (leaf.onResult !== undefined && typeof leaf.onResult !== 'function') {\n throw new FactoryRuleValidationError(`Factory rules.tools.${toolName}.onResult must be a function.`);\n }\n }\n\n if (!isPlainObject(rules.github)) throw new FactoryRuleValidationError('Factory rules.github must be an object.');\n for (const [event, leaf] of Object.entries(rules.github)) {\n enumValue(event, FACTORY_GITHUB_EVENTS, 'Factory GitHub event');\n if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`Factory rules.github.${event} must be an object.`);\n assertExactKeys(leaf, ['onEvent'], `Factory rules.github.${event}`);\n if (leaf.onEvent !== undefined && typeof leaf.onEvent !== 'function') {\n throw new FactoryRuleValidationError(`Factory rules.github.${event}.onEvent must be a function.`);\n }\n }\n\n if (!isPlainObject(rules.linear)) throw new FactoryRuleValidationError('Factory rules.linear must be an object.');\n for (const [event, leaf] of Object.entries(rules.linear)) {\n enumValue(event, FACTORY_LINEAR_EVENTS, 'Factory Linear event');\n if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`Factory rules.linear.${event} must be an object.`);\n assertExactKeys(leaf, ['onEvent'], `Factory rules.linear.${event}`);\n if (leaf.onEvent !== undefined && typeof leaf.onEvent !== 'function') {\n throw new FactoryRuleValidationError(`Factory rules.linear.${event}.onEvent must be a function.`);\n }\n }\n}\n\nfunction commonCommitFields(value: Record<string, unknown>): { idempotencyKey: string } {\n return {\n idempotencyKey: boundedString(value.idempotencyKey, 'Factory decision idempotencyKey', MAX_IDEMPOTENCY_KEY_LENGTH),\n };\n}\n\nexport function validateFactoryRuleDecision(value: unknown, causalDepth = 0): FactoryRuleDecision {\n if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {\n throw new FactoryRuleValidationError('Factory rule causal depth exceeded.');\n }\n if (!isPlainObject(value)) throw new FactoryRuleValidationError('Factory rule decision must be an object.');\n const type = value.type;\n if (typeof type !== 'string') throw new FactoryRuleValidationError('Factory rule decision type is required.');\n\n switch (type) {\n case 'reject': {\n assertExactKeys(value, ['type', 'code', 'reason'], 'Factory reject decision');\n return {\n type,\n code: enumValue(value.code, REJECTION_CODES, 'Factory rejection code'),\n reason: boundedString(value.reason, 'Factory rejection reason', MAX_REASON_LENGTH),\n };\n }\n case 'transition': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'board', 'stage', 'message', 'reenter'],\n 'Factory transition decision',\n );\n if (value.reenter !== undefined && typeof value.reenter !== 'boolean') {\n throw new FactoryRuleValidationError('Factory transition reenter must be a boolean.');\n }\n let message: { text: string; role?: string } | undefined;\n if (value.message !== undefined) {\n if (!isPlainObject(value.message)) {\n throw new FactoryRuleValidationError('Factory transition message must be an object.');\n }\n assertExactKeys(value.message, ['text', 'role'], 'Factory transition message');\n const role =\n value.message.role === undefined\n ? undefined\n : boundedString(value.message.role, 'Factory transition message role', MAX_ROLE_LENGTH, IDENTIFIER_RE);\n message = {\n text: boundedString(value.message.text, 'Factory transition message text', MAX_MESSAGE_LENGTH),\n ...(role ? { role } : {}),\n };\n }\n return {\n type,\n ...commonCommitFields(value),\n board: enumValue(value.board, FACTORY_RULE_BOARDS, 'Factory transition board'),\n stage: enumValue(value.stage, FACTORY_RULE_STAGES, 'Factory transition stage'),\n ...(message ? { message } : {}),\n ...(value.reenter === true ? { reenter: true } : {}),\n };\n }\n case 'upsertLinkedWorkItem': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'board', 'source', 'sourceKey', 'title', 'url', 'stage', 'metadata'],\n 'Factory linked work item decision',\n );\n const url = value.url;\n if (url !== null && (typeof url !== 'string' || url.length > MAX_URL_LENGTH || !/^https?:\\/\\//.test(url))) {\n throw new FactoryRuleValidationError('Factory linked work item URL is invalid.');\n }\n const metadata = sanitizeMetadata(value.metadata);\n return {\n type,\n ...commonCommitFields(value),\n board: enumValue(value.board, FACTORY_RULE_BOARDS, 'Factory linked work item board'),\n source: enumValue(value.source, WORK_ITEM_SOURCES, 'Factory linked work item source'),\n sourceKey: boundedString(value.sourceKey, 'Factory linked work item sourceKey', MAX_SOURCE_KEY_LENGTH),\n title: boundedString(value.title, 'Factory linked work item title', MAX_TITLE_LENGTH),\n url,\n stage: enumValue(value.stage, FACTORY_RULE_STAGES, 'Factory linked work item stage'),\n ...(metadata ? { metadata } : {}),\n };\n }\n case 'invokeSkill': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'role', 'skillName', 'prompt', 'arguments', 'precedingMessage', 'cancelInFlight'],\n 'Factory invoke skill decision',\n );\n // A run activates a skill or carries a prompt, never both: they are two\n // ways to author the same kickoff message, so accepting both would leave\n // the dispatcher picking a winner.\n if ((value.skillName === undefined) === (value.prompt === undefined)) {\n throw new FactoryRuleValidationError('Factory skill invocation needs exactly one of skillName or prompt.');\n }\n const args = optionalBoundedString(value.arguments, 'Factory skill arguments', MAX_ARGUMENTS_LENGTH);\n const precedingMessage = optionalBoundedString(\n value.precedingMessage,\n 'Factory skill preceding message',\n MAX_MESSAGE_LENGTH,\n );\n if (value.cancelInFlight !== undefined && typeof value.cancelInFlight !== 'boolean') {\n throw new FactoryRuleValidationError('Factory skill cancelInFlight must be a boolean.');\n }\n return {\n type,\n ...commonCommitFields(value),\n role: boundedString(value.role, 'Factory skill role', MAX_ROLE_LENGTH, IDENTIFIER_RE),\n ...(value.skillName === undefined\n ? { prompt: boundedString(value.prompt, 'Factory skill prompt', MAX_MESSAGE_LENGTH) }\n : { skillName: boundedString(value.skillName, 'Factory skill name', MAX_SKILL_NAME_LENGTH, SKILL_NAME_RE) }),\n ...(args ? { arguments: args } : {}),\n ...(precedingMessage ? { precedingMessage } : {}),\n ...(value.cancelInFlight === true ? { cancelInFlight: true } : {}),\n };\n }\n case 'sendMessage': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'role', 'message', 'priority', 'idleBehavior', 'prepareBinding'],\n 'Factory send message decision',\n );\n const priority =\n value.priority === undefined\n ? undefined\n : enumValue(value.priority, ['medium', 'high', 'urgent'] as const, 'Factory message priority');\n const idleBehavior =\n value.idleBehavior === undefined\n ? undefined\n : enumValue(value.idleBehavior, ['persist', 'wake'] as const, 'Factory message idle behavior');\n if (value.prepareBinding !== undefined && typeof value.prepareBinding !== 'boolean') {\n throw new FactoryRuleValidationError('Factory message prepareBinding must be a boolean.');\n }\n if (value.prepareBinding === true && value.role === undefined) {\n throw new FactoryRuleValidationError('Factory message prepareBinding requires a role.');\n }\n const role =\n value.role === undefined\n ? undefined\n : boundedString(value.role, 'Factory message role', MAX_ROLE_LENGTH, IDENTIFIER_RE);\n return {\n type,\n ...commonCommitFields(value),\n ...(role ? { role } : {}),\n message: boundedString(value.message, 'Factory message', MAX_MESSAGE_LENGTH),\n ...(priority ? { priority } : {}),\n ...(idleBehavior ? { idleBehavior } : {}),\n ...(value.prepareBinding === true ? { prepareBinding: true } : {}),\n };\n }\n case 'notify': {\n assertExactKeys(value, ['type', 'idempotencyKey', 'title', 'body', 'level'], 'Factory notify decision');\n const body = optionalBoundedString(value.body, 'Factory notification body', MAX_MESSAGE_LENGTH);\n const level =\n value.level === undefined\n ? undefined\n : enumValue(value.level, ['info', 'warning', 'error'] as const, 'Factory notification level');\n return {\n type,\n ...commonCommitFields(value),\n title: boundedString(value.title, 'Factory notification title', MAX_TITLE_LENGTH),\n ...(body ? { body } : {}),\n ...(level ? { level } : {}),\n };\n }\n default:\n throw new FactoryRuleValidationError('Factory rule decision type is unsupported.');\n }\n}\n\nexport function validateFactoryRuleDecisions(values: readonly unknown[], causalDepth = 0): FactoryCommitDecision[] {\n if (values.length > MAX_JSON_COLLECTION_SIZE) {\n throw new FactoryRuleValidationError('Factory rule produced too many decisions.');\n }\n const decisions: FactoryCommitDecision[] = [];\n for (const value of values) {\n const decision = validateFactoryRuleDecision(value, causalDepth);\n if (decision.type === 'reject') {\n throw new FactoryRuleValidationError('A rejection cannot be persisted with commit decisions.');\n }\n decisions.push(decision);\n }\n const keys = decisions.map(decision => decision.idempotencyKey);\n if (new Set(keys).size !== keys.length) {\n throw new FactoryRuleValidationError('Factory decisions require unique idempotency keys.');\n }\n return decisions;\n}\n"],"mappings":";;AAiBA,MAAa,gCAAgC;AAE7C,MAAM,qBAAqB;AAC3B,MAAM,6BAA6B;AACnC,MAAM,oBAAoB;AAC1B,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;AAC3B,MAAM,uBAAuB;AAC7B,MAAM,kBAAkB;AACxB,MAAM,wBAAwB;AAC9B,MAAM,wBAAwB;AAC9B,MAAM,iBAAiB;AACvB,MAAM,2BAA2B;AACjC,MAAM,iBAAiB;AACvB,MAAM,2BAA2B;AAEjC,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;AACtB,MAAM,mBAAmB;AACzB,MAAM,oBAA+C;CAAC;CAAgB;CAAa;CAAgB;AAAQ;AAC3G,MAAM,kBAAuD;CAC3D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,IAAa,6BAAb,cAAgD,MAAM;CACpD,OAAgB;CAEhB,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAS,cAAc,OAAkD;CACvE,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG,OAAO;CAChF,MAAM,YAAY,OAAO,eAAe,KAAK;CAC7C,OAAO,cAAc,OAAO,aAAa,cAAc;AACzD;AAEA,SAAS,gBAAgB,OAAgC,MAAyB,OAAqB;CACrG,MAAM,UAAU,IAAI,IAAI,IAAI;CAC5B,IAAI,OAAO,KAAK,KAAK,CAAC,CAAC,MAAK,QAAO,CAAC,QAAQ,IAAI,GAAG,CAAC,GAClD,MAAM,IAAI,2BAA2B,GAAG,MAAM,gCAAgC;AAElF;AAEA,SAAS,cAAc,OAAgB,OAAe,KAAa,SAA0B;CAC3F,IAAI,OAAO,UAAU,UAAU,MAAM,IAAI,2BAA2B,GAAG,MAAM,mBAAmB;CAChG,MAAM,aAAa,MAAM,KAAK;CAC9B,IAAI,WAAW,WAAW,KAAK,WAAW,SAAS,OAAQ,WAAW,CAAC,QAAQ,KAAK,UAAU,GAC5F,MAAM,IAAI,2BAA2B,GAAG,MAAM,aAAa;CAE7D,OAAO;AACT;AAEA,SAAS,sBAAsB,OAAgB,OAAe,KAAiC;CAC7F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;CAChC,OAAO,cAAc,OAAO,OAAO,GAAG;AACxC;AAEA,SAAS,UAA4B,OAAgB,SAAuB,OAAkB;CAC5F,IAAI,OAAO,UAAU,YAAY,CAAC,QAAQ,SAAS,KAAU,GAC3D,MAAM,IAAI,2BAA2B,GAAG,MAAM,aAAa;CAE7D,OAAO;AACT;AAEA,SAAgB,8BACd,OACA,QAAQ,GACR,uBAAO,IAAI,IAAY,GACD;CACtB,IAAI,UAAU,QAAQ,OAAO,UAAU,aAAa,OAAO,UAAU,UAAU,OAAO;CACtF,IAAI,OAAO,UAAU,UAAU;EAC7B,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG,MAAM,IAAI,2BAA2B,4CAA4C;EAC9G,OAAO;CACT;CACA,IAAI,SAAS,kBAAmB,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAC/E,MAAM,IAAI,2BAA2B,oCAAoC;CAE3E,IAAI,KAAK,IAAI,KAAe,GAAG,MAAM,IAAI,2BAA2B,wCAAwC;CAC5G,KAAK,IAAI,KAAe;CACxB,IAAI;EACF,IAAI,MAAM,QAAQ,KAAK,GAAG;GACxB,IAAI,MAAM,SAAS,0BACjB,MAAM,IAAI,2BAA2B,0CAA0C;GAEjF,OAAO,MAAM,KAAI,UAAS,8BAA8B,OAAO,QAAQ,GAAG,IAAI,CAAC;EACjF;EACA,IAAI,CAAC,cAAc,KAAK,GAAG,MAAM,IAAI,2BAA2B,uCAAuC;EACvG,MAAM,UAAU,OAAO,QAAQ,KAAK;EACpC,IAAI,QAAQ,SAAS,0BACnB,MAAM,IAAI,2BAA2B,yCAAyC;EAEhF,MAAM,YAAkD,CAAC;EACzD,KAAK,MAAM,CAAC,KAAK,UAAU,SAAS;GAClC,MAAM,gBAAgB,cAAc,KAAK,qBAAqB,KAAK,aAAa;GAChF,UAAU,iBAAiB,iBAAiB,KAAK,aAAa,IAC1D,eACA,8BAA8B,OAAO,QAAQ,GAAG,IAAI;EAC1D;EACA,OAAO;CACT,UAAU;EACR,KAAK,OAAO,KAAe;CAC7B;AACF;AAEA,SAAS,iBAAiB,OAAkE;CAC1F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;CAChC,MAAM,YAAY,8BAA8B,KAAK;CACrD,IAAI,CAAC,cAAc,SAAS,GAAG,MAAM,IAAI,2BAA2B,kCAAkC;CACtG,IAAI,KAAK,UAAU,SAAS,CAAC,CAAC,SAAS,0BACrC,MAAM,IAAI,2BAA2B,6BAA6B;CAEpE,OAAO;AACT;AAEA,SAAS,mBAAmB,OAAgB,OAAmD;CAC7F,IAAI,CAAC,cAAc,KAAK,GAAG,MAAM,IAAI,2BAA2B,GAAG,MAAM,oBAAoB;CAC7F,KAAK,MAAM,CAAC,OAAO,YAAY,OAAO,QAAQ,KAAK,GAAG;EACpD,UAAU,OAAO,qBAAqB,GAAG,MAAM,OAAO;EACtD,IAAI,CAAC,cAAc,OAAO,GAAG,MAAM,IAAI,2BAA2B,GAAG,MAAM,GAAG,MAAM,oBAAoB;EACxG,KAAK,MAAM,CAAC,QAAQ,SAAS,OAAO,QAAQ,OAAO,GAAG;GACpD,UAAU,QAAQ,sBAAsB,GAAG,MAAM,GAAG,MAAM,QAAQ;GAClE,IAAI,CAAC,cAAc,IAAI,GAAG,MAAM,IAAI,2BAA2B,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,oBAAoB;GAC/G,gBAAgB,MAAM,CAAC,WAAW,QAAQ,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ;GAC1E,KAAK,MAAM,WAAW,OAAO,OAAO,IAAI,GACtC,IAAI,YAAY,KAAA,KAAa,OAAO,YAAY,YAC9C,MAAM,IAAI,2BAA2B,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,6BAA6B;EAGpG;CACF;AACF;AAEA,SAAgB,mBAAmB,OAA+C;CAChF,IAAI,CAAC,cAAc,KAAK,GAAG,MAAM,IAAI,2BAA2B,kCAAkC;CAClG,gBAAgB,OAAO;EAAC;EAAW;EAAQ;EAAU;EAAS;EAAU;CAAQ,GAAG,eAAe;CAClG,cAAc,MAAM,SAAS,wBAAwB,kBAAkB;CACvE,mBAAmB,MAAM,MAAM,oBAAoB;CACnD,mBAAmB,MAAM,QAAQ,sBAAsB;CAEvD,IAAI,CAAC,cAAc,MAAM,KAAK,GAAG,MAAM,IAAI,2BAA2B,wCAAwC;CAC9G,KAAK,MAAM,CAAC,UAAU,SAAS,OAAO,QAAQ,MAAM,KAAK,GAAG;EAC1D,cAAc,UAAU,qBAAqB,KAAK,aAAa;EAC/D,IAAI,CAAC,cAAc,IAAI,GACrB,MAAM,IAAI,2BAA2B,uBAAuB,SAAS,oBAAoB;EAC3F,gBAAgB,MAAM,CAAC,UAAU,GAAG,uBAAuB,UAAU;EACrE,IAAI,KAAK,aAAa,KAAA,KAAa,OAAO,KAAK,aAAa,YAC1D,MAAM,IAAI,2BAA2B,uBAAuB,SAAS,8BAA8B;CAEvG;CAEA,IAAI,CAAC,cAAc,MAAM,MAAM,GAAG,MAAM,IAAI,2BAA2B,yCAAyC;CAChH,KAAK,MAAM,CAAC,OAAO,SAAS,OAAO,QAAQ,MAAM,MAAM,GAAG;EACxD,UAAU,OAAO,uBAAuB,sBAAsB;EAC9D,IAAI,CAAC,cAAc,IAAI,GAAG,MAAM,IAAI,2BAA2B,wBAAwB,MAAM,oBAAoB;EACjH,gBAAgB,MAAM,CAAC,SAAS,GAAG,wBAAwB,OAAO;EAClE,IAAI,KAAK,YAAY,KAAA,KAAa,OAAO,KAAK,YAAY,YACxD,MAAM,IAAI,2BAA2B,wBAAwB,MAAM,6BAA6B;CAEpG;CAEA,IAAI,CAAC,cAAc,MAAM,MAAM,GAAG,MAAM,IAAI,2BAA2B,yCAAyC;CAChH,KAAK,MAAM,CAAC,OAAO,SAAS,OAAO,QAAQ,MAAM,MAAM,GAAG;EACxD,UAAU,OAAO,uBAAuB,sBAAsB;EAC9D,IAAI,CAAC,cAAc,IAAI,GAAG,MAAM,IAAI,2BAA2B,wBAAwB,MAAM,oBAAoB;EACjH,gBAAgB,MAAM,CAAC,SAAS,GAAG,wBAAwB,OAAO;EAClE,IAAI,KAAK,YAAY,KAAA,KAAa,OAAO,KAAK,YAAY,YACxD,MAAM,IAAI,2BAA2B,wBAAwB,MAAM,6BAA6B;CAEpG;AACF;AAEA,SAAS,mBAAmB,OAA4D;CACtF,OAAO,EACL,gBAAgB,cAAc,MAAM,gBAAgB,mCAAmC,0BAA0B,EACnH;AACF;AAEA,SAAgB,4BAA4B,OAAgB,cAAc,GAAwB;CAChG,IAAI,cAAA,GACF,MAAM,IAAI,2BAA2B,qCAAqC;CAE5E,IAAI,CAAC,cAAc,KAAK,GAAG,MAAM,IAAI,2BAA2B,0CAA0C;CAC1G,MAAM,OAAO,MAAM;CACnB,IAAI,OAAO,SAAS,UAAU,MAAM,IAAI,2BAA2B,yCAAyC;CAE5G,QAAQ,MAAR;EACE,KAAK;GACH,gBAAgB,OAAO;IAAC;IAAQ;IAAQ;GAAQ,GAAG,yBAAyB;GAC5E,OAAO;IACL;IACA,MAAM,UAAU,MAAM,MAAM,iBAAiB,wBAAwB;IACrE,QAAQ,cAAc,MAAM,QAAQ,4BAA4B,iBAAiB;GACnF;EAEF,KAAK,cAAc;GACjB,gBACE,OACA;IAAC;IAAQ;IAAkB;IAAS;IAAS;IAAW;GAAS,GACjE,6BACF;GACA,IAAI,MAAM,YAAY,KAAA,KAAa,OAAO,MAAM,YAAY,WAC1D,MAAM,IAAI,2BAA2B,+CAA+C;GAEtF,IAAI;GACJ,IAAI,MAAM,YAAY,KAAA,GAAW;IAC/B,IAAI,CAAC,cAAc,MAAM,OAAO,GAC9B,MAAM,IAAI,2BAA2B,+CAA+C;IAEtF,gBAAgB,MAAM,SAAS,CAAC,QAAQ,MAAM,GAAG,4BAA4B;IAC7E,MAAM,OACJ,MAAM,QAAQ,SAAS,KAAA,IACnB,KAAA,IACA,cAAc,MAAM,QAAQ,MAAM,mCAAmC,iBAAiB,aAAa;IACzG,UAAU;KACR,MAAM,cAAc,MAAM,QAAQ,MAAM,mCAAmC,kBAAkB;KAC7F,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;IACzB;GACF;GACA,OAAO;IACL;IACA,GAAG,mBAAmB,KAAK;IAC3B,OAAO,UAAU,MAAM,OAAO,qBAAqB,0BAA0B;IAC7E,OAAO,UAAU,MAAM,OAAO,qBAAqB,0BAA0B;IAC7E,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;IAC7B,GAAI,MAAM,YAAY,OAAO,EAAE,SAAS,KAAK,IAAI,CAAC;GACpD;EACF;EACA,KAAK,wBAAwB;GAC3B,gBACE,OACA;IAAC;IAAQ;IAAkB;IAAS;IAAU;IAAa;IAAS;IAAO;IAAS;GAAU,GAC9F,mCACF;GACA,MAAM,MAAM,MAAM;GAClB,IAAI,QAAQ,SAAS,OAAO,QAAQ,YAAY,IAAI,SAAS,kBAAkB,CAAC,eAAe,KAAK,GAAG,IACrG,MAAM,IAAI,2BAA2B,0CAA0C;GAEjF,MAAM,WAAW,iBAAiB,MAAM,QAAQ;GAChD,OAAO;IACL;IACA,GAAG,mBAAmB,KAAK;IAC3B,OAAO,UAAU,MAAM,OAAO,qBAAqB,gCAAgC;IACnF,QAAQ,UAAU,MAAM,QAAQ,mBAAmB,iCAAiC;IACpF,WAAW,cAAc,MAAM,WAAW,sCAAsC,qBAAqB;IACrG,OAAO,cAAc,MAAM,OAAO,kCAAkC,gBAAgB;IACpF;IACA,OAAO,UAAU,MAAM,OAAO,qBAAqB,gCAAgC;IACnF,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;GACjC;EACF;EACA,KAAK,eAAe;GAClB,gBACE,OACA;IAAC;IAAQ;IAAkB;IAAQ;IAAa;IAAU;IAAa;IAAoB;GAAgB,GAC3G,+BACF;GAIA,IAAK,MAAM,cAAc,KAAA,OAAgB,MAAM,WAAW,KAAA,IACxD,MAAM,IAAI,2BAA2B,oEAAoE;GAE3G,MAAM,OAAO,sBAAsB,MAAM,WAAW,2BAA2B,oBAAoB;GACnG,MAAM,mBAAmB,sBACvB,MAAM,kBACN,mCACA,kBACF;GACA,IAAI,MAAM,mBAAmB,KAAA,KAAa,OAAO,MAAM,mBAAmB,WACxE,MAAM,IAAI,2BAA2B,iDAAiD;GAExF,OAAO;IACL;IACA,GAAG,mBAAmB,KAAK;IAC3B,MAAM,cAAc,MAAM,MAAM,sBAAsB,iBAAiB,aAAa;IACpF,GAAI,MAAM,cAAc,KAAA,IACpB,EAAE,QAAQ,cAAc,MAAM,QAAQ,wBAAwB,kBAAkB,EAAE,IAClF,EAAE,WAAW,cAAc,MAAM,WAAW,sBAAsB,uBAAuB,aAAa,EAAE;IAC5G,GAAI,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAClC,GAAI,mBAAmB,EAAE,iBAAiB,IAAI,CAAC;IAC/C,GAAI,MAAM,mBAAmB,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;GAClE;EACF;EACA,KAAK,eAAe;GAClB,gBACE,OACA;IAAC;IAAQ;IAAkB;IAAQ;IAAW;IAAY;IAAgB;GAAgB,GAC1F,+BACF;GACA,MAAM,WACJ,MAAM,aAAa,KAAA,IACf,KAAA,IACA,UAAU,MAAM,UAAU;IAAC;IAAU;IAAQ;GAAQ,GAAY,0BAA0B;GACjG,MAAM,eACJ,MAAM,iBAAiB,KAAA,IACnB,KAAA,IACA,UAAU,MAAM,cAAc,CAAC,WAAW,MAAM,GAAY,+BAA+B;GACjG,IAAI,MAAM,mBAAmB,KAAA,KAAa,OAAO,MAAM,mBAAmB,WACxE,MAAM,IAAI,2BAA2B,mDAAmD;GAE1F,IAAI,MAAM,mBAAmB,QAAQ,MAAM,SAAS,KAAA,GAClD,MAAM,IAAI,2BAA2B,iDAAiD;GAExF,MAAM,OACJ,MAAM,SAAS,KAAA,IACX,KAAA,IACA,cAAc,MAAM,MAAM,wBAAwB,iBAAiB,aAAa;GACtF,OAAO;IACL;IACA,GAAG,mBAAmB,KAAK;IAC3B,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;IACvB,SAAS,cAAc,MAAM,SAAS,mBAAmB,kBAAkB;IAC3E,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;IAC/B,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;IACvC,GAAI,MAAM,mBAAmB,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;GAClE;EACF;EACA,KAAK,UAAU;GACb,gBAAgB,OAAO;IAAC;IAAQ;IAAkB;IAAS;IAAQ;GAAO,GAAG,yBAAyB;GACtG,MAAM,OAAO,sBAAsB,MAAM,MAAM,6BAA6B,kBAAkB;GAC9F,MAAM,QACJ,MAAM,UAAU,KAAA,IACZ,KAAA,IACA,UAAU,MAAM,OAAO;IAAC;IAAQ;IAAW;GAAO,GAAY,4BAA4B;GAChG,OAAO;IACL;IACA,GAAG,mBAAmB,KAAK;IAC3B,OAAO,cAAc,MAAM,OAAO,8BAA8B,gBAAgB;IAChF,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;IACvB,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;GAC3B;EACF;EACA,SACE,MAAM,IAAI,2BAA2B,4CAA4C;CACrF;AACF;AAEA,SAAgB,6BAA6B,QAA4B,cAAc,GAA4B;CACjH,IAAI,OAAO,SAAS,0BAClB,MAAM,IAAI,2BAA2B,2CAA2C;CAElF,MAAM,YAAqC,CAAC;CAC5C,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,4BAA4B,OAAO,WAAW;EAC/D,IAAI,SAAS,SAAS,UACpB,MAAM,IAAI,2BAA2B,wDAAwD;EAE/F,UAAU,KAAK,QAAQ;CACzB;CACA,MAAM,OAAO,UAAU,KAAI,aAAY,SAAS,cAAc;CAC9D,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC,SAAS,KAAK,QAC9B,MAAM,IAAI,2BAA2B,oDAAoD;CAE3F,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/comments/base.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/comments/base.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { WORK_ITEM_ACTIVITY_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA } from "./schema.js";
2
+ import { externalSourceKey } from "../work-items/base.js";
2
3
  import { fanOutWorkItemActivity, listActivityForUser } from "./activity.js";
3
4
  import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage";
4
5
  //#region src/storage/domains/comments/base.ts
@@ -57,10 +58,7 @@ function decodeCommentCursor(cursor) {
57
58
  };
58
59
  }
59
60
  function commentSourceKey(input) {
60
- if (input.externalSource) {
61
- const source = input.externalSource;
62
- return `${source.integrationId}:${source.type}:${source.externalId}`;
63
- }
61
+ if (input.externalSource) return externalSourceKey(input.externalSource);
64
62
  if (input.clientToken) return `local:comment:${input.clientToken}`;
65
63
  return null;
66
64
  }
@@ -1 +1 @@
1
- {"version":3,"file":"base.js","names":["#insertWithMentions","#writeMentionRows","#fanOutActivity","#listAround","#listMentionRows"],"sources":["../../../../src/storage/domains/comments/base.ts"],"sourcesContent":["/**\n * Work-item comments domain — one message list per work item, materialized\n * from every source. Local creates and platform mirrors (Slack, Linear,\n * GitHub) share one idempotency story: `external_source` json + derived\n * `source_key` under a partial unique index, a local retry token being just\n * another source (`local:comment:<uuid>`).\n *\n * Ordering is caller-settable `occurred_at`, never insert time — ingest\n * backdates to the platform timestamp and retries arrive out of order.\n * Deletes are soft: the tombstone holds the ordering and its kept\n * `source_key` stops a redelivery from resurrecting the row.\n */\n\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\n\nimport type { ExternalWorkItemSource } from '../work-items/base.js';\nimport { fanOutWorkItemActivity, listActivityForUser, PARTICIPANT_SCAN_LIMIT } from './activity.js';\nimport type { WorkItemActivityRow } from './activity.js';\nimport type { FactoryActorExternalIdentity, FactoryActorRef } from './actor.js';\nimport { WORK_ITEM_ACTIVITY_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA } from './schema.js';\n\nexport type { WorkItemActivityRow } from './activity.js';\nexport { WORK_ITEM_ACTIVITY_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA } from './schema.js';\n\nexport type WorkItemCommentKind = 'comment';\n\nexport interface FactoryMentionRef {\n kind: 'user';\n id: string;\n}\n\nexport interface WorkItemCommentReplyRef {\n commentId: string;\n quote?: string;\n authorId?: string;\n authorName?: string;\n}\n\nexport interface WorkItemCommentRow {\n id: string;\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n kind: WorkItemCommentKind;\n body: string;\n bodyFormat: string;\n author: FactoryActorRef;\n replyTo: WorkItemCommentReplyRef | null;\n mentions: FactoryMentionRef[];\n externalSource: ExternalWorkItemSource | null;\n sourceKey: string | null;\n occurredAt: Date;\n editedAt: Date | null;\n deletedAt: Date | null;\n deletedBy: string | null;\n revision: number;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateWorkItemCommentInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n author: FactoryActorRef;\n body: string;\n bodyFormat?: string;\n replyTo?: WorkItemCommentReplyRef;\n mentions?: FactoryMentionRef[];\n externalSource?: ExternalWorkItemSource;\n /** Local idempotent-retry token; mutually exclusive with `externalSource`. */\n clientToken?: string;\n occurredAt?: Date;\n}\n\nexport interface EditWorkItemCommentInput {\n orgId: string;\n commentId: string;\n body: string;\n mentions?: FactoryMentionRef[];\n /** The acting user; their own handle never becomes a mention row. */\n editorId?: string;\n /** When set, the edit only lands if the row is still at this revision. */\n expectedRevision?: number;\n now?: Date;\n}\n\nexport interface EditWorkItemCommentResult {\n comment: WorkItemCommentRow;\n addedMentions: FactoryMentionRef[];\n removedMentions: FactoryMentionRef[];\n}\n\nexport interface ListWorkItemCommentsInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n before?: string;\n limit?: number;\n /**\n * Anchor the first page on this comment: it and every comment newer than it,\n * so a deep link opens on the target instead of paging back to find it.\n * Ignored when the comment is gone, or sits further back than one page holds.\n */\n around?: string;\n}\n\n/** The only two work-item columns the feed refresh reads or writes. */\ninterface WorkItemFeedColumns extends Record<string, unknown> {\n comment_count: number;\n feed_activity_at: Date | null;\n}\n\n/** A feed snapshot: how many comments the item had, and when it last moved. */\nexport interface FeedActivitySnapshot {\n commentCount: number;\n feedActivityAt: Date;\n}\n\n/**\n * The refresh reads its snapshot outside the write transaction, so two of them\n * can interleave and the loser can write last. This is the one rule that keeps\n * that from undoing a newer refresh: feed activity never moves backwards, and\n * on the same stamp the fuller count wins.\n *\n * Known ceiling: a soft delete sharing a millisecond with a create leaves the\n * count one high until the next feed mutation recounts. Closing that needs the\n * aggregate read inside the write transaction, which the ops layer cannot do\n * without checking out a second pool connection per open transaction.\n */\nexport function supersedesFeedActivity(next: FeedActivitySnapshot, stored: FeedActivitySnapshot): boolean {\n const drift = next.feedActivityAt.getTime() - stored.feedActivityAt.getTime();\n return drift > 0 || (drift === 0 && next.commentCount >= stored.commentCount);\n}\n\nexport interface WorkItemCommentPage {\n comments: WorkItemCommentRow[];\n nextCursor?: string;\n}\n\nexport interface WorkItemMentionRow {\n id: string;\n commentId: string;\n mentionedKind: 'user';\n mentionedId: string;\n authorId: string;\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n occurredAt: Date;\n}\n\nexport const MAX_COMMENT_BODY_LENGTH = 16_000;\nexport const MAX_COMMENT_QUOTE_LENGTH = 500;\nexport const MAX_COMMENT_MENTIONS = 20;\n\n/** The one body policy: HTTP parsing and the service both reject on it. */\nexport function commentBodyError(body: string): string | undefined {\n if (!body.trim()) return 'Comment body must not be empty.';\n if (body.length > MAX_COMMENT_BODY_LENGTH)\n return `Comment body must be at most ${MAX_COMMENT_BODY_LENGTH} characters.`;\n return undefined;\n}\n\nconst DEFAULT_PAGE_SIZE = 30;\nconst MAX_PAGE_SIZE = 100;\n\nexport function clampCommentLimit(limit: number | undefined): number {\n const normalized = typeof limit === 'number' && Number.isFinite(limit) ? Math.trunc(limit) : DEFAULT_PAGE_SIZE;\n return Math.min(Math.max(normalized, 1), MAX_PAGE_SIZE);\n}\n\nexport function encodeCommentCursor(row: WorkItemCommentRow): string {\n return `${row.occurredAt.toISOString()}_${row.id}`;\n}\n\nexport function decodeCommentCursor(cursor: string): { occurredAt: Date; id: string } | undefined {\n const sep = cursor.lastIndexOf('_');\n if (sep <= 0) return undefined;\n const occurredAt = new Date(cursor.slice(0, sep));\n const id = cursor.slice(sep + 1);\n if (Number.isNaN(occurredAt.getTime()) || !id) return undefined;\n return { occurredAt, id };\n}\n\nexport function commentSourceKey(input: {\n externalSource?: ExternalWorkItemSource;\n clientToken?: string;\n}): string | null {\n if (input.externalSource) {\n const source = input.externalSource;\n return `${source.integrationId}:${source.type}:${source.externalId}`;\n }\n if (input.clientToken) return `local:comment:${input.clientToken}`;\n return null;\n}\n\n/** A `clientToken` replay that resolved to a different work item or author. */\nexport class CommentTokenConflictError extends Error {\n constructor() {\n super('Client token already used by a different comment.');\n this.name = 'CommentTokenConflictError';\n }\n}\n\ninterface WorkItemCommentDbRow extends Record<string, unknown> {\n id: string;\n org_id: string;\n factory_project_id: string;\n work_item_id: string;\n kind: WorkItemCommentKind;\n body: string;\n body_format: string;\n author_kind: FactoryActorRef['kind'];\n author_id: string;\n author_display_name: string | null;\n author_avatar_url: string | null;\n author_external: FactoryActorExternalIdentity | null;\n reply_to_comment_id: string | null;\n reply_quote: string | null;\n reply_to_author_id: string | null;\n reply_to_author_name: string | null;\n mentions: FactoryMentionRef[];\n external_source: ExternalWorkItemSource | null;\n source_key: string | null;\n occurred_at: Date;\n edited_at: Date | null;\n deleted_at: Date | null;\n deleted_by: string | null;\n revision: number;\n created_at: Date;\n updated_at: Date;\n}\n\ninterface WorkItemMentionDbRow extends Record<string, unknown> {\n id: string;\n comment_id: string;\n mentioned_kind: 'user';\n mentioned_id: string;\n author_id: string;\n org_id: string;\n factory_project_id: string;\n work_item_id: string;\n occurred_at: Date;\n}\n\nfunction toComment(row: WorkItemCommentDbRow): WorkItemCommentRow {\n return {\n id: row.id,\n orgId: row.org_id,\n factoryProjectId: row.factory_project_id,\n workItemId: row.work_item_id,\n kind: row.kind,\n body: row.body,\n bodyFormat: row.body_format,\n author: {\n kind: row.author_kind,\n id: row.author_id,\n ...(row.author_display_name ? { displayName: row.author_display_name } : {}),\n ...(row.author_avatar_url ? { avatarUrl: row.author_avatar_url } : {}),\n ...(row.author_external ? { external: row.author_external } : {}),\n },\n replyTo: row.reply_to_comment_id\n ? {\n commentId: row.reply_to_comment_id,\n ...(row.reply_quote ? { quote: row.reply_quote } : {}),\n ...(row.reply_to_author_id ? { authorId: row.reply_to_author_id } : {}),\n ...(row.reply_to_author_name ? { authorName: row.reply_to_author_name } : {}),\n }\n : null,\n mentions: row.mentions,\n externalSource: row.external_source,\n sourceKey: row.source_key,\n occurredAt: row.occurred_at,\n editedAt: row.edited_at,\n deletedAt: row.deleted_at,\n deletedBy: row.deleted_by,\n revision: row.revision,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nfunction toMention(row: WorkItemMentionDbRow): WorkItemMentionRow {\n return {\n id: row.id,\n commentId: row.comment_id,\n mentionedKind: row.mentioned_kind,\n mentionedId: row.mentioned_id,\n authorId: row.author_id,\n orgId: row.org_id,\n factoryProjectId: row.factory_project_id,\n workItemId: row.work_item_id,\n occurredAt: row.occurred_at,\n };\n}\n\nfunction toMentionRef(row: WorkItemMentionRow): FactoryMentionRef {\n return { kind: row.mentionedKind, id: row.mentionedId };\n}\n\nfunction mentionKey(mention: { kind: string; id: string }): string {\n return `${mention.kind}\\0${mention.id}`;\n}\n\nfunction dedupeMentions(mentions: FactoryMentionRef[] | undefined): FactoryMentionRef[] {\n if (!mentions?.length) return [];\n return [...new Map(mentions.map(mention => [mentionKey(mention), mention])).values()];\n}\n\nexport class WorkItemCommentsStorage extends FactoryStorageDomain {\n constructor() {\n super('work-item-comments');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([\n WORK_ITEM_COMMENTS_SCHEMA,\n WORK_ITEM_COMMENT_MENTIONS_SCHEMA,\n WORK_ITEM_ACTIVITY_SCHEMA,\n ]);\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('work_item_activity', {});\n await this.ops.deleteMany('work_item_comment_mentions', {});\n await this.ops.deleteMany('work_item_comments', {});\n }\n\n async create(input: CreateWorkItemCommentInput): Promise<WorkItemCommentRow> {\n const now = new Date();\n const occurredAt = input.occurredAt ?? now;\n const sourceKey = commentSourceKey(input);\n const author = input.author;\n const row: Partial<WorkItemCommentDbRow> = {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n work_item_id: input.workItemId,\n kind: 'comment',\n body: input.body,\n body_format: input.bodyFormat ?? 'markdown',\n author_kind: author.kind,\n author_id: author.id,\n author_display_name: author.displayName ?? null,\n author_avatar_url: author.avatarUrl ?? null,\n author_external: author.external ?? null,\n reply_to_comment_id: input.replyTo?.commentId ?? null,\n reply_quote: input.replyTo?.quote ?? null,\n reply_to_author_id: input.replyTo?.authorId ?? null,\n reply_to_author_name: input.replyTo?.authorName ?? null,\n mentions: dedupeMentions(input.mentions),\n external_source: input.externalSource ?? null,\n source_key: sourceKey,\n occurred_at: occurredAt,\n edited_at: null,\n deleted_at: null,\n deleted_by: null,\n revision: 1,\n created_at: now,\n updated_at: now,\n };\n\n // Insert-or-recover, never upsert: a replayed create must return the\n // existing row untouched (an upsert would clobber a later edit's body).\n // A local token resolving to another item or author is a conflict, not a\n // recovery. External keys stay lenient: a redelivery after a thread\n // re-link maps to a new item id and must still no-op.\n if (sourceKey) {\n const sourceWhere = {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n source_key: sourceKey,\n };\n const recover = (found: WorkItemCommentDbRow): WorkItemCommentRow => {\n if (!input.externalSource && (found.work_item_id !== input.workItemId || found.author_id !== author.id)) {\n throw new CommentTokenConflictError();\n }\n return toComment(found);\n };\n const existing = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', sourceWhere);\n if (existing) return recover(existing);\n try {\n return await this.#insertWithMentions(row);\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n const raced = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', sourceWhere);\n if (!raced) throw error;\n return recover(raced);\n }\n }\n\n return this.#insertWithMentions(row);\n }\n\n /**\n * The one insert path a genuinely new comment takes — `create()`'s recover\n * branches never reach it, so a replay writes no mention or activity rows.\n */\n async #insertWithMentions(row: Partial<WorkItemCommentDbRow>): Promise<WorkItemCommentRow> {\n const comment = toComment(await this.ops.insertOne<WorkItemCommentDbRow>('work_item_comments', row));\n await this.#writeMentionRows(comment, comment.mentions);\n await this.#fanOutActivity(comment);\n return comment;\n }\n\n /**\n * Participants come off the item's recent authors, so the scan happens here\n * rather than in the fan-out, which must stay free of its own queries.\n */\n async #fanOutActivity(comment: WorkItemCommentRow): Promise<void> {\n const item = await this.ops.findOne<{ created_by: string } & Record<string, unknown>>('work_items', {\n id: comment.workItemId,\n org_id: comment.orgId,\n factory_project_id: comment.factoryProjectId,\n });\n if (!item) return;\n const recent = await this.listRecent({\n orgId: comment.orgId,\n factoryProjectId: comment.factoryProjectId,\n workItemId: comment.workItemId,\n limit: PARTICIPANT_SCAN_LIMIT,\n });\n await fanOutWorkItemActivity(this.ops, {\n comment,\n recentAuthors: recent.filter(row => row.author.kind === 'user').map(row => row.author.id),\n createdBy: item.created_by,\n mentions: comment.mentions,\n });\n }\n\n async listActivityForUser(args: {\n orgId: string;\n factoryProjectId: string;\n userId: string;\n before?: { occurredAt: Date; id: string };\n limit: number;\n }): Promise<WorkItemActivityRow[]> {\n return listActivityForUser(this.ops, args);\n }\n\n /**\n * Idempotent counter refresh on the parent work item: both columns are read\n * back off the comments (never incremented or stamped with the wall clock —\n * replays and races double an increment, and a replayed create would move a\n * work item in the feed without adding a comment; a read-back can't drift).\n * Read BEFORE `updateAtomic`: its mutator runs inside an open transaction\n * holding a pool connection, and a query in there checks out a second one —\n * concurrent posts would exhaust the pool. Reading outside means two\n * refreshes can interleave, so the write goes through\n * {@link supersedesFeedActivity} rather than landing whatever it read.\n * Touches ONLY the counter columns: `revision`/`updated_at` are the\n * stage-transition concurrency token.\n */\n async refreshWorkItemFeedActivity({\n orgId,\n factoryProjectId,\n workItemId,\n now = new Date(),\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n now?: Date;\n }): Promise<void> {\n const commentCount = await this.countForWorkItem({ orgId, factoryProjectId, workItemId });\n // Every feed mutation stamps `updated_at`, edits and tombstones included,\n // so the newest one is when this feed last moved.\n const [latest] = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId, work_item_id: workItemId },\n { orderBy: [['updated_at', 'desc']], limit: 1 },\n );\n const feedActivityAt = latest?.updated_at ?? now;\n await this.ops.updateAtomic<WorkItemFeedColumns>(\n 'work_items',\n { id: workItemId, org_id: orgId, factory_project_id: factoryProjectId },\n row => {\n const next = { commentCount, feedActivityAt };\n const stored = row.feed_activity_at;\n if (stored && !supersedesFeedActivity(next, { commentCount: row.comment_count, feedActivityAt: stored })) {\n return null;\n }\n return { comment_count: commentCount, feed_activity_at: feedActivityAt };\n },\n );\n }\n\n async get({ orgId, commentId }: { orgId: string; commentId: string }): Promise<WorkItemCommentRow | null> {\n const row = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', {\n id: commentId,\n org_id: orgId,\n });\n return row ? toComment(row) : null;\n }\n\n async listByIds({ orgId, ids }: { orgId: string; ids: string[] }): Promise<WorkItemCommentRow[]> {\n if (ids.length === 0) return [];\n const rows = await this.ops.findMany<WorkItemCommentDbRow>('work_item_comments', {\n org_id: orgId,\n id: { in: ids },\n });\n return rows.map(toComment);\n }\n\n async list(input: ListWorkItemCommentsInput): Promise<WorkItemCommentPage> {\n const anchored = input.around ? await this.#listAround(input, input.around) : undefined;\n if (anchored) return anchored;\n const limit = clampCommentLimit(input.limit);\n const cursor = input.before ? decodeCommentCursor(input.before) : undefined;\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n work_item_id: input.workItemId,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit: limit + 1,\n ...(cursor ? { cursor: { values: [cursor.occurredAt, cursor.id] } } : {}),\n },\n );\n const comments = rows.slice(0, limit).map(toComment);\n const hasMore = rows.length > limit;\n const last = comments[comments.length - 1];\n return {\n comments,\n ...(hasMore && last ? { nextCursor: encodeCommentCursor(last) } : {}),\n };\n }\n\n async #listAround(\n { orgId, factoryProjectId, workItemId }: ListWorkItemCommentsInput,\n commentId: string,\n ): Promise<WorkItemCommentPage | undefined> {\n const target = await this.get({ orgId, commentId });\n if (!target || target.factoryProjectId !== factoryProjectId || target.workItemId !== workItemId) return undefined;\n\n const newer = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId, work_item_id: workItemId },\n {\n orderBy: [\n ['occurred_at', 'asc'],\n ['id', 'asc'],\n ],\n limit: MAX_PAGE_SIZE + 1,\n cursor: { values: [target.occurredAt, target.id] },\n },\n );\n if (newer.length > MAX_PAGE_SIZE) return undefined;\n\n const before = encodeCommentCursor(target);\n const older = await this.list({ orgId, factoryProjectId, workItemId, before, limit: 1 });\n return {\n comments: [...newer.map(toComment).reverse(), target],\n ...(older.comments.length > 0 ? { nextCursor: before } : {}),\n };\n }\n\n /**\n * Newest non-deleted comments for run-context injection, newest-first (the\n * caller reverses for display order).\n */\n async listRecent({\n orgId,\n factoryProjectId,\n workItemId,\n limit,\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n limit: number;\n }): Promise<WorkItemCommentRow[]> {\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n work_item_id: workItemId,\n deleted_at: null,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n },\n );\n return rows.map(toComment);\n }\n\n async edit(input: EditWorkItemCommentInput): Promise<EditWorkItemCommentResult | 'conflict' | null> {\n const now = input.now ?? new Date();\n let rejection: 'deleted' | 'conflict' | undefined;\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: input.commentId, org_id: input.orgId },\n current => {\n if (current.deleted_at) {\n rejection = 'deleted';\n return null;\n }\n if (input.expectedRevision !== undefined && Number(current.revision) !== input.expectedRevision) {\n rejection = 'conflict';\n return null;\n }\n return {\n body: input.body,\n mentions: dedupeMentions(input.mentions ?? current.mentions),\n edited_at: now,\n revision: Number(current.revision) + 1,\n updated_at: now,\n };\n },\n );\n if (rejection === 'conflict') return 'conflict';\n if (!updated || rejection) return null;\n const comment = toComment(updated);\n\n const existing = await this.#listMentionRows(comment.id);\n const existingKeys = new Set(existing.map(mention => mentionKey(toMentionRef(mention))));\n const nextKeys = new Set(comment.mentions.map(mentionKey));\n // Self-mentions (author, or the acting editor) never become rows, so they\n // must not report as \"added\" either — they would re-report on every edit.\n const skippedIds = new Set([comment.author.id, ...(input.editorId ? [input.editorId] : [])]);\n const addedMentions = comment.mentions.filter(\n mention => !existingKeys.has(mentionKey(mention)) && !skippedIds.has(mention.id),\n );\n const removedMentions = existing.map(toMentionRef).filter(mention => !nextKeys.has(mentionKey(mention)));\n\n // Stamped with the edit time, not the comment's creation time: the inbox\n // is a keyset on `occurred_at`, and a backdated row lands buried under\n // everything the user already saw.\n await this.#writeMentionRows(comment, addedMentions, now);\n for (const mention of removedMentions) {\n await this.ops.deleteMany('work_item_comment_mentions', {\n comment_id: comment.id,\n mentioned_kind: mention.kind,\n mentioned_id: mention.id,\n });\n }\n return { comment, addedMentions, removedMentions };\n }\n\n async softDelete({\n orgId,\n commentId,\n deletedBy,\n now = new Date(),\n }: {\n orgId: string;\n commentId: string;\n deletedBy: string;\n now?: Date;\n }): Promise<WorkItemCommentRow | null> {\n let alreadyDeleted = false;\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: commentId, org_id: orgId },\n current => {\n if (current.deleted_at) {\n alreadyDeleted = true;\n return null;\n }\n return {\n body: '',\n mentions: [],\n deleted_at: now,\n deleted_by: deletedBy,\n updated_at: now,\n };\n },\n );\n if (!updated || alreadyDeleted) return null;\n await this.ops.deleteMany('work_item_comment_mentions', { comment_id: commentId });\n return toComment(updated);\n }\n\n /**\n * Provenance write-back after an outbound publish. First platform wins, and\n * an existing `source_key` is kept: replacing a `local:comment:<token>` one\n * would let a client retry duplicate the row. So a web-born comment is never\n * key-deduped against its own platform echo — the host's bot-sender check is\n * the only echo layer for those rows (COR-1174).\n */\n async attachExternalSource({\n orgId,\n commentId,\n source,\n }: {\n orgId: string;\n commentId: string;\n source: ExternalWorkItemSource;\n }): Promise<WorkItemCommentRow | null> {\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: commentId, org_id: orgId },\n current => {\n if (current.external_source) return null;\n return {\n external_source: source,\n source_key: current.source_key ?? commentSourceKey({ externalSource: source }),\n updated_at: new Date(),\n };\n },\n );\n return updated ? toComment(updated) : null;\n }\n\n async listMentionsForComment(commentId: string): Promise<WorkItemMentionRow[]> {\n return this.#listMentionRows(commentId);\n }\n\n /** Keyset inbox read for the mention attention provider, newest-first. */\n async listMentionsForUser({\n orgId,\n factoryProjectId,\n userId,\n before,\n limit,\n }: {\n orgId: string;\n factoryProjectId: string;\n userId: string;\n before?: { occurredAt: Date; id: string };\n limit: number;\n }): Promise<WorkItemMentionRow[]> {\n const rows = await this.ops.findMany<WorkItemMentionDbRow>(\n 'work_item_comment_mentions',\n {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n mentioned_id: userId,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n ...(before ? { cursor: { values: [before.occurredAt, before.id] } } : {}),\n },\n );\n return rows.map(toMention);\n }\n\n async countForWorkItem({\n orgId,\n factoryProjectId,\n workItemId,\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n }): Promise<number> {\n if (!this.ops.count) {\n throw new Error('[WorkItemCommentsStorage] storage backend does not support collection counts.');\n }\n return this.ops.count('work_item_comments', {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n work_item_id: workItemId,\n deleted_at: null,\n });\n }\n\n /** Recent distinct comment authors of a project, for the roster fallback. */\n async listRecentAuthors({\n orgId,\n factoryProjectId,\n limit = 200,\n }: {\n orgId: string;\n factoryProjectId: string;\n limit?: number;\n }): Promise<FactoryActorRef[]> {\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n },\n );\n const authors = new Map<string, FactoryActorRef>();\n for (const row of rows) {\n if (authors.has(row.author_id)) continue;\n authors.set(row.author_id, toComment(row).author);\n }\n return [...authors.values()];\n }\n\n async #listMentionRows(commentId: string): Promise<WorkItemMentionRow[]> {\n const rows = await this.ops.findMany<WorkItemMentionDbRow>('work_item_comment_mentions', {\n comment_id: commentId,\n });\n return rows.map(toMention);\n }\n\n /**\n * Self-mentions never get rows: the join exists solely for the attention\n * inbox, and `CollectionWhere` cannot express `author_id != userId` at read\n * time, so the filter happens at write time.\n */\n async #writeMentionRows(\n comment: WorkItemCommentRow,\n mentions: FactoryMentionRef[],\n occurredAt = comment.occurredAt,\n ): Promise<void> {\n for (const mention of mentions) {\n if (mention.id === comment.author.id) continue;\n await this.ops.upsertOne<WorkItemMentionDbRow>(\n 'work_item_comment_mentions',\n ['comment_id', 'mentioned_kind', 'mentioned_id'],\n {\n comment_id: comment.id,\n mentioned_kind: mention.kind,\n mentioned_id: mention.id,\n author_id: comment.author.id,\n org_id: comment.orgId,\n factory_project_id: comment.factoryProjectId,\n work_item_id: comment.workItemId,\n occurred_at: occurredAt,\n },\n );\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"base.js","names":["#insertWithMentions","#writeMentionRows","#fanOutActivity","#listAround","#listMentionRows"],"sources":["../../../../src/storage/domains/comments/base.ts"],"sourcesContent":["/**\n * Work-item comments domain — one message list per work item, materialized\n * from every source. Local creates and platform mirrors (Slack, Linear,\n * GitHub) share one idempotency story: `external_source` json + derived\n * `source_key` under a partial unique index, a local retry token being just\n * another source (`local:comment:<uuid>`).\n *\n * Ordering is caller-settable `occurred_at`, never insert time — ingest\n * backdates to the platform timestamp and retries arrive out of order.\n * Deletes are soft: the tombstone holds the ordering and its kept\n * `source_key` stops a redelivery from resurrecting the row.\n */\n\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\n\nimport { externalSourceKey } from '../work-items/base.js';\nimport type { ExternalWorkItemSource } from '../work-items/base.js';\nimport { fanOutWorkItemActivity, listActivityForUser, PARTICIPANT_SCAN_LIMIT } from './activity.js';\nimport type { WorkItemActivityRow } from './activity.js';\nimport type { FactoryActorExternalIdentity, FactoryActorRef } from './actor.js';\nimport { WORK_ITEM_ACTIVITY_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA } from './schema.js';\n\nexport type { WorkItemActivityRow } from './activity.js';\nexport { WORK_ITEM_ACTIVITY_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA } from './schema.js';\n\nexport type WorkItemCommentKind = 'comment';\n\nexport interface FactoryMentionRef {\n kind: 'user';\n id: string;\n}\n\nexport interface WorkItemCommentReplyRef {\n commentId: string;\n quote?: string;\n authorId?: string;\n authorName?: string;\n}\n\nexport interface WorkItemCommentRow {\n id: string;\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n kind: WorkItemCommentKind;\n body: string;\n bodyFormat: string;\n author: FactoryActorRef;\n replyTo: WorkItemCommentReplyRef | null;\n mentions: FactoryMentionRef[];\n externalSource: ExternalWorkItemSource | null;\n sourceKey: string | null;\n occurredAt: Date;\n editedAt: Date | null;\n deletedAt: Date | null;\n deletedBy: string | null;\n revision: number;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateWorkItemCommentInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n author: FactoryActorRef;\n body: string;\n bodyFormat?: string;\n replyTo?: WorkItemCommentReplyRef;\n mentions?: FactoryMentionRef[];\n externalSource?: ExternalWorkItemSource;\n /** Local idempotent-retry token; mutually exclusive with `externalSource`. */\n clientToken?: string;\n occurredAt?: Date;\n}\n\nexport interface EditWorkItemCommentInput {\n orgId: string;\n commentId: string;\n body: string;\n mentions?: FactoryMentionRef[];\n /** The acting user; their own handle never becomes a mention row. */\n editorId?: string;\n /** When set, the edit only lands if the row is still at this revision. */\n expectedRevision?: number;\n now?: Date;\n}\n\nexport interface EditWorkItemCommentResult {\n comment: WorkItemCommentRow;\n addedMentions: FactoryMentionRef[];\n removedMentions: FactoryMentionRef[];\n}\n\nexport interface ListWorkItemCommentsInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n before?: string;\n limit?: number;\n /**\n * Anchor the first page on this comment: it and every comment newer than it,\n * so a deep link opens on the target instead of paging back to find it.\n * Ignored when the comment is gone, or sits further back than one page holds.\n */\n around?: string;\n}\n\n/** The only two work-item columns the feed refresh reads or writes. */\ninterface WorkItemFeedColumns extends Record<string, unknown> {\n comment_count: number;\n feed_activity_at: Date | null;\n}\n\n/** A feed snapshot: how many comments the item had, and when it last moved. */\nexport interface FeedActivitySnapshot {\n commentCount: number;\n feedActivityAt: Date;\n}\n\n/**\n * The refresh reads its snapshot outside the write transaction, so two of them\n * can interleave and the loser can write last. This is the one rule that keeps\n * that from undoing a newer refresh: feed activity never moves backwards, and\n * on the same stamp the fuller count wins.\n *\n * Known ceiling: a soft delete sharing a millisecond with a create leaves the\n * count one high until the next feed mutation recounts. Closing that needs the\n * aggregate read inside the write transaction, which the ops layer cannot do\n * without checking out a second pool connection per open transaction.\n */\nexport function supersedesFeedActivity(next: FeedActivitySnapshot, stored: FeedActivitySnapshot): boolean {\n const drift = next.feedActivityAt.getTime() - stored.feedActivityAt.getTime();\n return drift > 0 || (drift === 0 && next.commentCount >= stored.commentCount);\n}\n\nexport interface WorkItemCommentPage {\n comments: WorkItemCommentRow[];\n nextCursor?: string;\n}\n\nexport interface WorkItemMentionRow {\n id: string;\n commentId: string;\n mentionedKind: 'user';\n mentionedId: string;\n authorId: string;\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n occurredAt: Date;\n}\n\nexport const MAX_COMMENT_BODY_LENGTH = 16_000;\nexport const MAX_COMMENT_QUOTE_LENGTH = 500;\nexport const MAX_COMMENT_MENTIONS = 20;\n\n/** The one body policy: HTTP parsing and the service both reject on it. */\nexport function commentBodyError(body: string): string | undefined {\n if (!body.trim()) return 'Comment body must not be empty.';\n if (body.length > MAX_COMMENT_BODY_LENGTH)\n return `Comment body must be at most ${MAX_COMMENT_BODY_LENGTH} characters.`;\n return undefined;\n}\n\nconst DEFAULT_PAGE_SIZE = 30;\nconst MAX_PAGE_SIZE = 100;\n\nexport function clampCommentLimit(limit: number | undefined): number {\n const normalized = typeof limit === 'number' && Number.isFinite(limit) ? Math.trunc(limit) : DEFAULT_PAGE_SIZE;\n return Math.min(Math.max(normalized, 1), MAX_PAGE_SIZE);\n}\n\nexport function encodeCommentCursor(row: WorkItemCommentRow): string {\n return `${row.occurredAt.toISOString()}_${row.id}`;\n}\n\nexport function decodeCommentCursor(cursor: string): { occurredAt: Date; id: string } | undefined {\n const sep = cursor.lastIndexOf('_');\n if (sep <= 0) return undefined;\n const occurredAt = new Date(cursor.slice(0, sep));\n const id = cursor.slice(sep + 1);\n if (Number.isNaN(occurredAt.getTime()) || !id) return undefined;\n return { occurredAt, id };\n}\n\nexport function commentSourceKey(input: {\n externalSource?: ExternalWorkItemSource;\n clientToken?: string;\n}): string | null {\n if (input.externalSource) return externalSourceKey(input.externalSource);\n if (input.clientToken) return `local:comment:${input.clientToken}`;\n return null;\n}\n\n/** A `clientToken` replay that resolved to a different work item or author. */\nexport class CommentTokenConflictError extends Error {\n constructor() {\n super('Client token already used by a different comment.');\n this.name = 'CommentTokenConflictError';\n }\n}\n\ninterface WorkItemCommentDbRow extends Record<string, unknown> {\n id: string;\n org_id: string;\n factory_project_id: string;\n work_item_id: string;\n kind: WorkItemCommentKind;\n body: string;\n body_format: string;\n author_kind: FactoryActorRef['kind'];\n author_id: string;\n author_display_name: string | null;\n author_avatar_url: string | null;\n author_external: FactoryActorExternalIdentity | null;\n reply_to_comment_id: string | null;\n reply_quote: string | null;\n reply_to_author_id: string | null;\n reply_to_author_name: string | null;\n mentions: FactoryMentionRef[];\n external_source: ExternalWorkItemSource | null;\n source_key: string | null;\n occurred_at: Date;\n edited_at: Date | null;\n deleted_at: Date | null;\n deleted_by: string | null;\n revision: number;\n created_at: Date;\n updated_at: Date;\n}\n\ninterface WorkItemMentionDbRow extends Record<string, unknown> {\n id: string;\n comment_id: string;\n mentioned_kind: 'user';\n mentioned_id: string;\n author_id: string;\n org_id: string;\n factory_project_id: string;\n work_item_id: string;\n occurred_at: Date;\n}\n\nfunction toComment(row: WorkItemCommentDbRow): WorkItemCommentRow {\n return {\n id: row.id,\n orgId: row.org_id,\n factoryProjectId: row.factory_project_id,\n workItemId: row.work_item_id,\n kind: row.kind,\n body: row.body,\n bodyFormat: row.body_format,\n author: {\n kind: row.author_kind,\n id: row.author_id,\n ...(row.author_display_name ? { displayName: row.author_display_name } : {}),\n ...(row.author_avatar_url ? { avatarUrl: row.author_avatar_url } : {}),\n ...(row.author_external ? { external: row.author_external } : {}),\n },\n replyTo: row.reply_to_comment_id\n ? {\n commentId: row.reply_to_comment_id,\n ...(row.reply_quote ? { quote: row.reply_quote } : {}),\n ...(row.reply_to_author_id ? { authorId: row.reply_to_author_id } : {}),\n ...(row.reply_to_author_name ? { authorName: row.reply_to_author_name } : {}),\n }\n : null,\n mentions: row.mentions,\n externalSource: row.external_source,\n sourceKey: row.source_key,\n occurredAt: row.occurred_at,\n editedAt: row.edited_at,\n deletedAt: row.deleted_at,\n deletedBy: row.deleted_by,\n revision: row.revision,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nfunction toMention(row: WorkItemMentionDbRow): WorkItemMentionRow {\n return {\n id: row.id,\n commentId: row.comment_id,\n mentionedKind: row.mentioned_kind,\n mentionedId: row.mentioned_id,\n authorId: row.author_id,\n orgId: row.org_id,\n factoryProjectId: row.factory_project_id,\n workItemId: row.work_item_id,\n occurredAt: row.occurred_at,\n };\n}\n\nfunction toMentionRef(row: WorkItemMentionRow): FactoryMentionRef {\n return { kind: row.mentionedKind, id: row.mentionedId };\n}\n\nfunction mentionKey(mention: { kind: string; id: string }): string {\n return `${mention.kind}\\0${mention.id}`;\n}\n\nfunction dedupeMentions(mentions: FactoryMentionRef[] | undefined): FactoryMentionRef[] {\n if (!mentions?.length) return [];\n return [...new Map(mentions.map(mention => [mentionKey(mention), mention])).values()];\n}\n\nexport class WorkItemCommentsStorage extends FactoryStorageDomain {\n constructor() {\n super('work-item-comments');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([\n WORK_ITEM_COMMENTS_SCHEMA,\n WORK_ITEM_COMMENT_MENTIONS_SCHEMA,\n WORK_ITEM_ACTIVITY_SCHEMA,\n ]);\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('work_item_activity', {});\n await this.ops.deleteMany('work_item_comment_mentions', {});\n await this.ops.deleteMany('work_item_comments', {});\n }\n\n async create(input: CreateWorkItemCommentInput): Promise<WorkItemCommentRow> {\n const now = new Date();\n const occurredAt = input.occurredAt ?? now;\n const sourceKey = commentSourceKey(input);\n const author = input.author;\n const row: Partial<WorkItemCommentDbRow> = {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n work_item_id: input.workItemId,\n kind: 'comment',\n body: input.body,\n body_format: input.bodyFormat ?? 'markdown',\n author_kind: author.kind,\n author_id: author.id,\n author_display_name: author.displayName ?? null,\n author_avatar_url: author.avatarUrl ?? null,\n author_external: author.external ?? null,\n reply_to_comment_id: input.replyTo?.commentId ?? null,\n reply_quote: input.replyTo?.quote ?? null,\n reply_to_author_id: input.replyTo?.authorId ?? null,\n reply_to_author_name: input.replyTo?.authorName ?? null,\n mentions: dedupeMentions(input.mentions),\n external_source: input.externalSource ?? null,\n source_key: sourceKey,\n occurred_at: occurredAt,\n edited_at: null,\n deleted_at: null,\n deleted_by: null,\n revision: 1,\n created_at: now,\n updated_at: now,\n };\n\n // Insert-or-recover, never upsert: a replayed create must return the\n // existing row untouched (an upsert would clobber a later edit's body).\n // A local token resolving to another item or author is a conflict, not a\n // recovery. External keys stay lenient: a redelivery after a thread\n // re-link maps to a new item id and must still no-op.\n if (sourceKey) {\n const sourceWhere = {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n source_key: sourceKey,\n };\n const recover = (found: WorkItemCommentDbRow): WorkItemCommentRow => {\n if (!input.externalSource && (found.work_item_id !== input.workItemId || found.author_id !== author.id)) {\n throw new CommentTokenConflictError();\n }\n return toComment(found);\n };\n const existing = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', sourceWhere);\n if (existing) return recover(existing);\n try {\n return await this.#insertWithMentions(row);\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n const raced = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', sourceWhere);\n if (!raced) throw error;\n return recover(raced);\n }\n }\n\n return this.#insertWithMentions(row);\n }\n\n /**\n * The one insert path a genuinely new comment takes — `create()`'s recover\n * branches never reach it, so a replay writes no mention or activity rows.\n */\n async #insertWithMentions(row: Partial<WorkItemCommentDbRow>): Promise<WorkItemCommentRow> {\n const comment = toComment(await this.ops.insertOne<WorkItemCommentDbRow>('work_item_comments', row));\n await this.#writeMentionRows(comment, comment.mentions);\n await this.#fanOutActivity(comment);\n return comment;\n }\n\n /**\n * Participants come off the item's recent authors, so the scan happens here\n * rather than in the fan-out, which must stay free of its own queries.\n */\n async #fanOutActivity(comment: WorkItemCommentRow): Promise<void> {\n const item = await this.ops.findOne<{ created_by: string } & Record<string, unknown>>('work_items', {\n id: comment.workItemId,\n org_id: comment.orgId,\n factory_project_id: comment.factoryProjectId,\n });\n if (!item) return;\n const recent = await this.listRecent({\n orgId: comment.orgId,\n factoryProjectId: comment.factoryProjectId,\n workItemId: comment.workItemId,\n limit: PARTICIPANT_SCAN_LIMIT,\n });\n await fanOutWorkItemActivity(this.ops, {\n comment,\n recentAuthors: recent.filter(row => row.author.kind === 'user').map(row => row.author.id),\n createdBy: item.created_by,\n mentions: comment.mentions,\n });\n }\n\n async listActivityForUser(args: {\n orgId: string;\n factoryProjectId: string;\n userId: string;\n before?: { occurredAt: Date; id: string };\n limit: number;\n }): Promise<WorkItemActivityRow[]> {\n return listActivityForUser(this.ops, args);\n }\n\n /**\n * Idempotent counter refresh on the parent work item: both columns are read\n * back off the comments (never incremented or stamped with the wall clock —\n * replays and races double an increment, and a replayed create would move a\n * work item in the feed without adding a comment; a read-back can't drift).\n * Read BEFORE `updateAtomic`: its mutator runs inside an open transaction\n * holding a pool connection, and a query in there checks out a second one —\n * concurrent posts would exhaust the pool. Reading outside means two\n * refreshes can interleave, so the write goes through\n * {@link supersedesFeedActivity} rather than landing whatever it read.\n * Touches ONLY the counter columns: `revision`/`updated_at` are the\n * stage-transition concurrency token.\n */\n async refreshWorkItemFeedActivity({\n orgId,\n factoryProjectId,\n workItemId,\n now = new Date(),\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n now?: Date;\n }): Promise<void> {\n const commentCount = await this.countForWorkItem({ orgId, factoryProjectId, workItemId });\n // Every feed mutation stamps `updated_at`, edits and tombstones included,\n // so the newest one is when this feed last moved.\n const [latest] = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId, work_item_id: workItemId },\n { orderBy: [['updated_at', 'desc']], limit: 1 },\n );\n const feedActivityAt = latest?.updated_at ?? now;\n await this.ops.updateAtomic<WorkItemFeedColumns>(\n 'work_items',\n { id: workItemId, org_id: orgId, factory_project_id: factoryProjectId },\n row => {\n const next = { commentCount, feedActivityAt };\n const stored = row.feed_activity_at;\n if (stored && !supersedesFeedActivity(next, { commentCount: row.comment_count, feedActivityAt: stored })) {\n return null;\n }\n return { comment_count: commentCount, feed_activity_at: feedActivityAt };\n },\n );\n }\n\n async get({ orgId, commentId }: { orgId: string; commentId: string }): Promise<WorkItemCommentRow | null> {\n const row = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', {\n id: commentId,\n org_id: orgId,\n });\n return row ? toComment(row) : null;\n }\n\n async listByIds({ orgId, ids }: { orgId: string; ids: string[] }): Promise<WorkItemCommentRow[]> {\n if (ids.length === 0) return [];\n const rows = await this.ops.findMany<WorkItemCommentDbRow>('work_item_comments', {\n org_id: orgId,\n id: { in: ids },\n });\n return rows.map(toComment);\n }\n\n async list(input: ListWorkItemCommentsInput): Promise<WorkItemCommentPage> {\n const anchored = input.around ? await this.#listAround(input, input.around) : undefined;\n if (anchored) return anchored;\n const limit = clampCommentLimit(input.limit);\n const cursor = input.before ? decodeCommentCursor(input.before) : undefined;\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n work_item_id: input.workItemId,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit: limit + 1,\n ...(cursor ? { cursor: { values: [cursor.occurredAt, cursor.id] } } : {}),\n },\n );\n const comments = rows.slice(0, limit).map(toComment);\n const hasMore = rows.length > limit;\n const last = comments[comments.length - 1];\n return {\n comments,\n ...(hasMore && last ? { nextCursor: encodeCommentCursor(last) } : {}),\n };\n }\n\n async #listAround(\n { orgId, factoryProjectId, workItemId }: ListWorkItemCommentsInput,\n commentId: string,\n ): Promise<WorkItemCommentPage | undefined> {\n const target = await this.get({ orgId, commentId });\n if (!target || target.factoryProjectId !== factoryProjectId || target.workItemId !== workItemId) return undefined;\n\n const newer = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId, work_item_id: workItemId },\n {\n orderBy: [\n ['occurred_at', 'asc'],\n ['id', 'asc'],\n ],\n limit: MAX_PAGE_SIZE + 1,\n cursor: { values: [target.occurredAt, target.id] },\n },\n );\n if (newer.length > MAX_PAGE_SIZE) return undefined;\n\n const before = encodeCommentCursor(target);\n const older = await this.list({ orgId, factoryProjectId, workItemId, before, limit: 1 });\n return {\n comments: [...newer.map(toComment).reverse(), target],\n ...(older.comments.length > 0 ? { nextCursor: before } : {}),\n };\n }\n\n /**\n * Newest non-deleted comments for run-context injection, newest-first (the\n * caller reverses for display order).\n */\n async listRecent({\n orgId,\n factoryProjectId,\n workItemId,\n limit,\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n limit: number;\n }): Promise<WorkItemCommentRow[]> {\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n work_item_id: workItemId,\n deleted_at: null,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n },\n );\n return rows.map(toComment);\n }\n\n async edit(input: EditWorkItemCommentInput): Promise<EditWorkItemCommentResult | 'conflict' | null> {\n const now = input.now ?? new Date();\n let rejection: 'deleted' | 'conflict' | undefined;\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: input.commentId, org_id: input.orgId },\n current => {\n if (current.deleted_at) {\n rejection = 'deleted';\n return null;\n }\n if (input.expectedRevision !== undefined && Number(current.revision) !== input.expectedRevision) {\n rejection = 'conflict';\n return null;\n }\n return {\n body: input.body,\n mentions: dedupeMentions(input.mentions ?? current.mentions),\n edited_at: now,\n revision: Number(current.revision) + 1,\n updated_at: now,\n };\n },\n );\n if (rejection === 'conflict') return 'conflict';\n if (!updated || rejection) return null;\n const comment = toComment(updated);\n\n const existing = await this.#listMentionRows(comment.id);\n const existingKeys = new Set(existing.map(mention => mentionKey(toMentionRef(mention))));\n const nextKeys = new Set(comment.mentions.map(mentionKey));\n // Self-mentions (author, or the acting editor) never become rows, so they\n // must not report as \"added\" either — they would re-report on every edit.\n const skippedIds = new Set([comment.author.id, ...(input.editorId ? [input.editorId] : [])]);\n const addedMentions = comment.mentions.filter(\n mention => !existingKeys.has(mentionKey(mention)) && !skippedIds.has(mention.id),\n );\n const removedMentions = existing.map(toMentionRef).filter(mention => !nextKeys.has(mentionKey(mention)));\n\n // Stamped with the edit time, not the comment's creation time: the inbox\n // is a keyset on `occurred_at`, and a backdated row lands buried under\n // everything the user already saw.\n await this.#writeMentionRows(comment, addedMentions, now);\n for (const mention of removedMentions) {\n await this.ops.deleteMany('work_item_comment_mentions', {\n comment_id: comment.id,\n mentioned_kind: mention.kind,\n mentioned_id: mention.id,\n });\n }\n return { comment, addedMentions, removedMentions };\n }\n\n async softDelete({\n orgId,\n commentId,\n deletedBy,\n now = new Date(),\n }: {\n orgId: string;\n commentId: string;\n deletedBy: string;\n now?: Date;\n }): Promise<WorkItemCommentRow | null> {\n let alreadyDeleted = false;\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: commentId, org_id: orgId },\n current => {\n if (current.deleted_at) {\n alreadyDeleted = true;\n return null;\n }\n return {\n body: '',\n mentions: [],\n deleted_at: now,\n deleted_by: deletedBy,\n updated_at: now,\n };\n },\n );\n if (!updated || alreadyDeleted) return null;\n await this.ops.deleteMany('work_item_comment_mentions', { comment_id: commentId });\n return toComment(updated);\n }\n\n /**\n * Provenance write-back after an outbound publish. First platform wins, and\n * an existing `source_key` is kept: replacing a `local:comment:<token>` one\n * would let a client retry duplicate the row. So a web-born comment is never\n * key-deduped against its own platform echo — the host's bot-sender check is\n * the only echo layer for those rows (COR-1174).\n */\n async attachExternalSource({\n orgId,\n commentId,\n source,\n }: {\n orgId: string;\n commentId: string;\n source: ExternalWorkItemSource;\n }): Promise<WorkItemCommentRow | null> {\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: commentId, org_id: orgId },\n current => {\n if (current.external_source) return null;\n return {\n external_source: source,\n source_key: current.source_key ?? commentSourceKey({ externalSource: source }),\n updated_at: new Date(),\n };\n },\n );\n return updated ? toComment(updated) : null;\n }\n\n async listMentionsForComment(commentId: string): Promise<WorkItemMentionRow[]> {\n return this.#listMentionRows(commentId);\n }\n\n /** Keyset inbox read for the mention attention provider, newest-first. */\n async listMentionsForUser({\n orgId,\n factoryProjectId,\n userId,\n before,\n limit,\n }: {\n orgId: string;\n factoryProjectId: string;\n userId: string;\n before?: { occurredAt: Date; id: string };\n limit: number;\n }): Promise<WorkItemMentionRow[]> {\n const rows = await this.ops.findMany<WorkItemMentionDbRow>(\n 'work_item_comment_mentions',\n {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n mentioned_id: userId,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n ...(before ? { cursor: { values: [before.occurredAt, before.id] } } : {}),\n },\n );\n return rows.map(toMention);\n }\n\n async countForWorkItem({\n orgId,\n factoryProjectId,\n workItemId,\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n }): Promise<number> {\n if (!this.ops.count) {\n throw new Error('[WorkItemCommentsStorage] storage backend does not support collection counts.');\n }\n return this.ops.count('work_item_comments', {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n work_item_id: workItemId,\n deleted_at: null,\n });\n }\n\n /** Recent distinct comment authors of a project, for the roster fallback. */\n async listRecentAuthors({\n orgId,\n factoryProjectId,\n limit = 200,\n }: {\n orgId: string;\n factoryProjectId: string;\n limit?: number;\n }): Promise<FactoryActorRef[]> {\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n },\n );\n const authors = new Map<string, FactoryActorRef>();\n for (const row of rows) {\n if (authors.has(row.author_id)) continue;\n authors.set(row.author_id, toComment(row).author);\n }\n return [...authors.values()];\n }\n\n async #listMentionRows(commentId: string): Promise<WorkItemMentionRow[]> {\n const rows = await this.ops.findMany<WorkItemMentionDbRow>('work_item_comment_mentions', {\n comment_id: commentId,\n });\n return rows.map(toMention);\n }\n\n /**\n * Self-mentions never get rows: the join exists solely for the attention\n * inbox, and `CollectionWhere` cannot express `author_id != userId` at read\n * time, so the filter happens at write time.\n */\n async #writeMentionRows(\n comment: WorkItemCommentRow,\n mentions: FactoryMentionRef[],\n occurredAt = comment.occurredAt,\n ): Promise<void> {\n for (const mention of mentions) {\n if (mention.id === comment.author.id) continue;\n await this.ops.upsertOne<WorkItemMentionDbRow>(\n 'work_item_comment_mentions',\n ['comment_id', 'mentioned_kind', 'mentioned_id'],\n {\n comment_id: comment.id,\n mentioned_kind: mention.kind,\n mentioned_id: mention.id,\n author_id: comment.author.id,\n org_id: comment.orgId,\n factory_project_id: comment.factoryProjectId,\n work_item_id: comment.workItemId,\n occurred_at: occurredAt,\n },\n );\n }\n }\n}\n"],"mappings":"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"}
@@ -11,7 +11,7 @@ import type { RouteAuth } from '../../../routes/route.js';
11
11
  import type { AuditEmitter } from '../audit/domain.js';
12
12
  import type { ChannelIdentityStorage } from '../channel-identity/base.js';
13
13
  import type { FactoryProjectsStorage } from '../projects/base.js';
14
- import type { WorkItemRow, WorkItemsStorage } from '../work-items/base.js';
14
+ import type { ExternalWorkItemSource, WorkItemRow, WorkItemsStorage } from '../work-items/base.js';
15
15
  import type { FactoryActorRef } from './actor.js';
16
16
  import type { EditWorkItemCommentResult, FactoryMentionRef, WorkItemCommentRow, WorkItemCommentsStorage } from './base.js';
17
17
  import type { WorkItemFeedPublisher } from './feed-sync.js';
@@ -48,11 +48,18 @@ export interface CreateCommentServiceInput {
48
48
  };
49
49
  mentions?: FactoryMentionRef[];
50
50
  clientToken?: string;
51
+ /** Set when the comment is a platform message being ingested, never by the web UI. */
52
+ externalSource?: ExternalWorkItemSource;
53
+ /** The platform's own timestamp; defaults to now for locally authored comments. */
54
+ occurredAt?: Date;
51
55
  }
52
- export type CreateCommentServiceResult = {
56
+ export type CreateCommentServiceResult =
57
+ /** `mirrored` settles when the platforms have been posted to; nothing in the response waits on it. */
58
+ {
53
59
  status: 'created';
54
60
  comment: WorkItemCommentRow;
55
61
  workItem: WorkItemRow;
62
+ mirrored: Promise<void>;
56
63
  } | {
57
64
  status: 'work_item_not_found';
58
65
  } | {
@@ -1 +1 @@
1
- {"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/comments/domain.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/comments/domain.ts"],"names":[],"mappings":"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"}
@@ -29,10 +29,7 @@ var CommentsDomain = class {
29
29
  this.#publishers = publishers ?? [];
30
30
  this.#pubsub = pubsub;
31
31
  }
32
- /**
33
- * The one seam every feed mutation routes through — future
34
- * `WorkItemFeedIngest` impls included.
35
- */
32
+ /** The one seam every feed mutation routes through, platform ingest included. */
36
33
  async #touchFeed(scope) {
37
34
  await this.#comments.refreshWorkItemFeedActivity(scope);
38
35
  touchFeed(this.#pubsub, scope, scope.workItemId);
@@ -81,7 +78,9 @@ var CommentsDomain = class {
81
78
  body: input.body,
82
79
  ...replyTo ? { replyTo } : {},
83
80
  ...input.mentions ? { mentions: input.mentions } : {},
84
- ...input.clientToken ? { clientToken: input.clientToken } : {}
81
+ ...input.clientToken ? { clientToken: input.clientToken } : {},
82
+ ...input.externalSource ? { externalSource: input.externalSource } : {},
83
+ ...input.occurredAt ? { occurredAt: input.occurredAt } : {}
85
84
  });
86
85
  } catch (error) {
87
86
  if (error instanceof CommentTokenConflictError) return { status: "token_conflict" };
@@ -92,36 +91,40 @@ var CommentsDomain = class {
92
91
  factoryProjectId: workItem.factoryProjectId,
93
92
  workItemId: input.workItemId
94
93
  });
95
- await this.#mirrorComment(comment, workItem);
96
94
  return {
97
95
  status: "created",
98
96
  comment,
99
- workItem
97
+ workItem,
98
+ mirrored: this.#mirrorComment(comment, workItem)
100
99
  };
101
100
  }
102
101
  /**
103
- * Best-effort: a failed publish never fails the create. The write-back is
104
- * the replay guard — a replayed create sees its own platform on the row and
105
- * skips it. Known ceiling, single publisher only: `external_source` is
106
- * single-valued, so with two publishers a replayed create re-publishes to
107
- * the one that is not recorded, and nothing persists a failed attempt for a
108
- * later pass to retry. Both need an outbox if a second publisher ever lands.
102
+ * Runs past the response — the comment is stored, the feed frame is already
103
+ * out, and nobody is waiting on Slack. A failed publish never fails the
104
+ * create. The write-back is the replay guard: a replayed create sees its own
105
+ * platform on the row and skips it. Known ceiling, single publisher only:
106
+ * `external_source` is single-valued, so with two publishers a replayed
107
+ * create re-publishes to the one that is not recorded, and a process that
108
+ * restarts mid-flight drops the post. Both need an outbox.
109
109
  */
110
110
  async #mirrorComment(comment, workItem) {
111
111
  let current = comment;
112
112
  for (const publisher of this.#publishers) {
113
113
  if (current.externalSource?.integrationId === publisher.id) continue;
114
114
  try {
115
- const { source } = await publisher.publish(current, workItem);
115
+ const published = await publisher.publish(current, workItem);
116
+ if (!published) continue;
116
117
  current = await this.#comments.attachExternalSource({
117
118
  orgId: current.orgId,
118
119
  commentId: current.id,
119
- source
120
+ source: published.source
120
121
  }) ?? current;
121
122
  } catch (err) {
122
123
  console.warn("[Comments] Failed to mirror a comment to a platform", {
123
124
  publisherId: publisher.id,
124
125
  commentId: current.id,
126
+ orgId: current.orgId,
127
+ workItemId: workItem.id,
125
128
  error: err instanceof Error ? err.message : String(err)
126
129
  });
127
130
  }