@shipfox/api-definitions 12.3.0 → 14.0.0

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 (223) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +45 -0
  3. package/dist/core/entities/index.d.ts +1 -1
  4. package/dist/core/entities/index.d.ts.map +1 -1
  5. package/dist/core/entities/index.js +1 -1
  6. package/dist/core/entities/index.js.map +1 -1
  7. package/dist/core/entities/integration-context.d.ts +4 -0
  8. package/dist/core/entities/integration-context.d.ts.map +1 -1
  9. package/dist/core/entities/integration-context.js.map +1 -1
  10. package/dist/core/entities/sync-state.d.ts +10 -4
  11. package/dist/core/entities/sync-state.d.ts.map +1 -1
  12. package/dist/core/entities/sync-state.js +26 -7
  13. package/dist/core/entities/sync-state.js.map +1 -1
  14. package/dist/core/entities/validation-diagnostic.d.ts +8 -0
  15. package/dist/core/entities/validation-diagnostic.d.ts.map +1 -0
  16. package/dist/core/entities/validation-diagnostic.js +3 -0
  17. package/dist/core/entities/validation-diagnostic.js.map +1 -0
  18. package/dist/core/entities/workflow-definition.d.ts +2 -0
  19. package/dist/core/entities/workflow-definition.d.ts.map +1 -1
  20. package/dist/core/entities/workflow-definition.js.map +1 -1
  21. package/dist/core/errors.d.ts +10 -0
  22. package/dist/core/errors.d.ts.map +1 -1
  23. package/dist/core/errors.js +9 -0
  24. package/dist/core/errors.js.map +1 -1
  25. package/dist/core/index.d.ts +2 -1
  26. package/dist/core/index.d.ts.map +1 -1
  27. package/dist/core/index.js +2 -1
  28. package/dist/core/index.js.map +1 -1
  29. package/dist/core/integrations.d.ts +3 -1
  30. package/dist/core/integrations.d.ts.map +1 -1
  31. package/dist/core/integrations.js +10 -2
  32. package/dist/core/integrations.js.map +1 -1
  33. package/dist/core/parse-definition.d.ts +4 -4
  34. package/dist/core/parse-definition.d.ts.map +1 -1
  35. package/dist/core/parse-definition.js +5 -5
  36. package/dist/core/parse-definition.js.map +1 -1
  37. package/dist/core/resolve-definition-at-ref.d.ts +66 -0
  38. package/dist/core/resolve-definition-at-ref.d.ts.map +1 -0
  39. package/dist/core/resolve-definition-at-ref.js +374 -0
  40. package/dist/core/resolve-definition-at-ref.js.map +1 -0
  41. package/dist/core/sync-definitions.d.ts +2 -2
  42. package/dist/core/sync-definitions.d.ts.map +1 -1
  43. package/dist/core/sync-definitions.js +5 -5
  44. package/dist/core/sync-definitions.js.map +1 -1
  45. package/dist/core/validate-definition.d.ts +3 -3
  46. package/dist/core/validate-definition.d.ts.map +1 -1
  47. package/dist/core/validate-definition.js +17 -12
  48. package/dist/core/validate-definition.js.map +1 -1
  49. package/dist/core/workflow-model/cron-trigger.d.ts +1 -1
  50. package/dist/core/workflow-model/cron-trigger.d.ts.map +1 -1
  51. package/dist/core/workflow-model/cron-trigger.js +9 -5
  52. package/dist/core/workflow-model/cron-trigger.js.map +1 -1
  53. package/dist/core/workflow-model/index.d.ts +1 -1
  54. package/dist/core/workflow-model/index.d.ts.map +1 -1
  55. package/dist/core/workflow-model/index.js.map +1 -1
  56. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts +4 -2
  57. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts.map +1 -1
  58. package/dist/core/workflow-model/invalid-workflow-model-error.js.map +1 -1
  59. package/dist/core/workflow-model/normalize-job-listening.d.ts.map +1 -1
  60. package/dist/core/workflow-model/normalize-job-listening.js +51 -30
  61. package/dist/core/workflow-model/normalize-job-listening.js.map +1 -1
  62. package/dist/core/workflow-model/normalize-triggers.d.ts +1 -1
  63. package/dist/core/workflow-model/normalize-triggers.d.ts.map +1 -1
  64. package/dist/core/workflow-model/normalize-triggers.js +48 -18
  65. package/dist/core/workflow-model/normalize-triggers.js.map +1 -1
  66. package/dist/core/workflow-model/normalize-workflow-document.d.ts +1 -1
  67. package/dist/core/workflow-model/normalize-workflow-document.d.ts.map +1 -1
  68. package/dist/core/workflow-model/normalize-workflow-document.js +3 -3
  69. package/dist/core/workflow-model/normalize-workflow-document.js.map +1 -1
  70. package/dist/core/workflow-model/parse-interpolation-field.d.ts.map +1 -1
  71. package/dist/core/workflow-model/parse-interpolation-field.js +1 -15
  72. package/dist/core/workflow-model/parse-interpolation-field.js.map +1 -1
  73. package/dist/core/workflow-model/validate-predicate-expression.d.ts +3 -1
  74. package/dist/core/workflow-model/validate-predicate-expression.d.ts.map +1 -1
  75. package/dist/core/workflow-model/validate-predicate-expression.js +3 -0
  76. package/dist/core/workflow-model/validate-predicate-expression.js.map +1 -1
  77. package/dist/core/workflow-model/validation-issue.d.ts +2 -1
  78. package/dist/core/workflow-model/validation-issue.d.ts.map +1 -1
  79. package/dist/core/workflow-model/validation-issue.js +12 -15
  80. package/dist/core/workflow-model/validation-issue.js.map +1 -1
  81. package/dist/db/db.d.ts +196 -4
  82. package/dist/db/db.d.ts.map +1 -1
  83. package/dist/db/db.js +3 -1
  84. package/dist/db/db.js.map +1 -1
  85. package/dist/db/definition-triggers.js +3 -1
  86. package/dist/db/definition-triggers.js.map +1 -1
  87. package/dist/db/definitions.d.ts +17 -0
  88. package/dist/db/definitions.d.ts.map +1 -1
  89. package/dist/db/definitions.js +190 -23
  90. package/dist/db/definitions.js.map +1 -1
  91. package/dist/db/index.d.ts +2 -1
  92. package/dist/db/index.d.ts.map +1 -1
  93. package/dist/db/index.js +2 -1
  94. package/dist/db/index.js.map +1 -1
  95. package/dist/db/schema/definitions.d.ts +17 -0
  96. package/dist/db/schema/definitions.d.ts.map +1 -1
  97. package/dist/db/schema/definitions.js +8 -0
  98. package/dist/db/schema/definitions.js.map +1 -1
  99. package/dist/db/schema/sync-states.d.ts +7 -3
  100. package/dist/db/schema/sync-states.d.ts.map +1 -1
  101. package/dist/db/schema/sync-states.js +5 -1
  102. package/dist/db/schema/sync-states.js.map +1 -1
  103. package/dist/db/schema/workflows.d.ts +78 -0
  104. package/dist/db/schema/workflows.d.ts.map +1 -0
  105. package/dist/db/schema/workflows.js +17 -0
  106. package/dist/db/schema/workflows.js.map +1 -0
  107. package/dist/db/sync-states.d.ts +2 -2
  108. package/dist/db/sync-states.d.ts.map +1 -1
  109. package/dist/db/sync-states.js +4 -4
  110. package/dist/db/sync-states.js.map +1 -1
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +5 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/metrics/index.d.ts +2 -0
  115. package/dist/metrics/index.d.ts.map +1 -0
  116. package/dist/metrics/index.js +3 -0
  117. package/dist/metrics/index.js.map +1 -0
  118. package/dist/metrics/instance.d.ts +4 -0
  119. package/dist/metrics/instance.d.ts.map +1 -0
  120. package/dist/metrics/instance.js +19 -0
  121. package/dist/metrics/instance.js.map +1 -0
  122. package/dist/presentation/dto/definition.d.ts.map +1 -1
  123. package/dist/presentation/dto/definition.js +11 -4
  124. package/dist/presentation/dto/definition.js.map +1 -1
  125. package/dist/presentation/inter-module.d.ts +9 -1
  126. package/dist/presentation/inter-module.d.ts.map +1 -1
  127. package/dist/presentation/inter-module.js +66 -2
  128. package/dist/presentation/inter-module.js.map +1 -1
  129. package/dist/presentation/routes/create-definition.d.ts.map +1 -1
  130. package/dist/presentation/routes/create-definition.js +16 -3
  131. package/dist/presentation/routes/create-definition.js.map +1 -1
  132. package/dist/presentation/routes/get-definition.d.ts.map +1 -1
  133. package/dist/presentation/routes/get-definition.js +18 -4
  134. package/dist/presentation/routes/get-definition.js.map +1 -1
  135. package/dist/presentation/routes/project-access.d.ts +1 -0
  136. package/dist/presentation/routes/project-access.d.ts.map +1 -1
  137. package/dist/presentation/routes/validate-definition.js +3 -3
  138. package/dist/presentation/routes/validate-definition.js.map +1 -1
  139. package/dist/temporal/activities/sync-activities.d.ts +3 -3
  140. package/dist/temporal/activities/sync-activities.d.ts.map +1 -1
  141. package/dist/temporal/activities/sync-activities.js +7 -4
  142. package/dist/temporal/activities/sync-activities.js.map +1 -1
  143. package/dist/temporal/workflows/definition-sync.js +1 -1
  144. package/dist/temporal/workflows/definition-sync.js.map +1 -1
  145. package/dist/temporal/workflows/index.bundle.js +50 -22
  146. package/dist/tsconfig.test.tsbuildinfo +1 -1
  147. package/drizzle/0003_nebulous_nemesis.sql +11 -0
  148. package/drizzle/0004_workflow_ref_lookup.sql +1 -0
  149. package/drizzle/meta/0003_snapshot.json +627 -0
  150. package/drizzle/meta/0004_snapshot.json +649 -0
  151. package/drizzle/meta/_journal.json +14 -0
  152. package/drizzle.config.ts +1 -0
  153. package/package.json +7 -7
  154. package/src/core/entities/index.ts +1 -1
  155. package/src/core/entities/integration-context.ts +4 -0
  156. package/src/core/entities/sync-state.test.ts +49 -0
  157. package/src/core/entities/sync-state.ts +35 -12
  158. package/src/core/entities/validation-diagnostic.ts +8 -0
  159. package/src/core/entities/workflow-definition.ts +2 -0
  160. package/src/core/errors.ts +26 -0
  161. package/src/core/index.ts +12 -0
  162. package/src/core/integrations.test.ts +38 -0
  163. package/src/core/integrations.ts +10 -1
  164. package/src/core/parse-definition-diagnostics.test.ts +47 -0
  165. package/src/core/parse-definition.test.ts +29 -5
  166. package/src/core/parse-definition.ts +7 -7
  167. package/src/core/resolve-definition-at-ref.test.ts +792 -0
  168. package/src/core/resolve-definition-at-ref.ts +551 -0
  169. package/src/core/sync-definitions.test.ts +61 -2
  170. package/src/core/sync-definitions.ts +7 -7
  171. package/src/core/validate-definition.test.ts +31 -22
  172. package/src/core/validate-definition.ts +21 -13
  173. package/src/core/workflow-model/cron-trigger.ts +6 -2
  174. package/src/core/workflow-model/index.ts +1 -0
  175. package/src/core/workflow-model/invalid-workflow-model-error.ts +5 -1
  176. package/src/core/workflow-model/normalize-job-listening.ts +55 -23
  177. package/src/core/workflow-model/normalize-triggers.ts +44 -15
  178. package/src/core/workflow-model/normalize-workflow-document.test.ts +494 -36
  179. package/src/core/workflow-model/normalize-workflow-document.ts +8 -4
  180. package/src/core/workflow-model/parse-interpolation-field.ts +1 -23
  181. package/src/core/workflow-model/validate-predicate-expression.ts +6 -0
  182. package/src/core/workflow-model/validation-issue.ts +8 -13
  183. package/src/db/db.ts +7 -1
  184. package/src/db/definition-triggers.test.ts +44 -0
  185. package/src/db/definition-triggers.ts +1 -1
  186. package/src/db/definitions.test.ts +300 -1
  187. package/src/db/definitions.ts +282 -28
  188. package/src/db/index.ts +2 -0
  189. package/src/db/schema/definitions.test.ts +1 -0
  190. package/src/db/schema/definitions.ts +11 -0
  191. package/src/db/schema/sync-states.ts +12 -3
  192. package/src/db/schema/workflows.ts +23 -0
  193. package/src/db/sync-states.test.ts +87 -18
  194. package/src/db/sync-states.ts +6 -6
  195. package/src/db/workflow-lineage-migration.test.ts +158 -0
  196. package/src/index.ts +3 -1
  197. package/src/metrics/index.ts +1 -0
  198. package/src/metrics/instance.test.ts +50 -0
  199. package/src/metrics/instance.ts +23 -0
  200. package/src/presentation/dto/definition.test.ts +89 -1
  201. package/src/presentation/dto/definition.ts +8 -5
  202. package/src/presentation/inter-module.test.ts +177 -0
  203. package/src/presentation/inter-module.ts +77 -4
  204. package/src/presentation/routes/create-definition.test.ts +38 -0
  205. package/src/presentation/routes/create-definition.ts +20 -3
  206. package/src/presentation/routes/get-definition.test.ts +125 -11
  207. package/src/presentation/routes/get-definition.ts +21 -4
  208. package/src/presentation/routes/list-definitions.test.ts +1 -1
  209. package/src/presentation/routes/validate-definition.test.ts +1 -1
  210. package/src/presentation/routes/validate-definition.ts +3 -3
  211. package/src/presentation/subscribers/on-integration-connection-available.test.ts +3 -0
  212. package/src/temporal/activities/sync-activities.test.ts +46 -2
  213. package/src/temporal/activities/sync-activities.ts +9 -7
  214. package/src/temporal/workflows/definition-sync.ts +1 -1
  215. package/test/factories/definition.ts +1 -0
  216. package/test/globalSetup.ts +2 -0
  217. package/tsconfig.build.tsbuildinfo +1 -1
  218. package/dist/core/entities/validation-warning.d.ts +0 -6
  219. package/dist/core/entities/validation-warning.d.ts.map +0 -1
  220. package/dist/core/entities/validation-warning.js +0 -3
  221. package/dist/core/entities/validation-warning.js.map +0 -1
  222. package/src/core/entities/validation-warning.ts +0 -5
  223. package/src/core/parse-definition-warnings.test.ts +0 -33
@@ -1,7 +1,12 @@
1
+ import type {AgentValidationCatalog} from '@shipfox/api-agent-dto/inter-module';
1
2
  import type {WorkflowDocument} from '@shipfox/workflow-document';
2
3
  import {agentValidationCatalog} from '#test/agent-validation-catalog.js';
3
4
  import type {IntegrationValidationContext} from '../entities/integration-context.js';
4
- import {InvalidWorkflowModelError} from './invalid-workflow-model-error.js';
5
+ import type {WorkflowModel} from '../entities/workflow-model.js';
6
+ import {
7
+ InvalidWorkflowModelError,
8
+ type WorkflowModelValidationIssue,
9
+ } from './invalid-workflow-model-error.js';
5
10
  import {DEFAULT_JOB_CHECKOUT} from './normalize-job-checkout.js';
6
11
  import {normalizeWorkflowDocument as normalizeWorkflowDocumentBase} from './normalize-workflow-document.js';
7
12
 
@@ -33,6 +38,16 @@ function expectInvalid(
33
38
  }
34
39
  }
35
40
 
41
+ function normalizeWithDiagnostics(
42
+ document: WorkflowDocument,
43
+ options?: Omit<Parameters<typeof normalizeWorkflowDocumentBase>[1], 'agentValidationCatalog'> &
44
+ Partial<Pick<Parameters<typeof normalizeWorkflowDocumentBase>[1], 'agentValidationCatalog'>>,
45
+ ): {model: WorkflowModel; diagnostics: WorkflowModelValidationIssue[]} {
46
+ const diagnostics: WorkflowModelValidationIssue[] = [];
47
+ const model = normalizeWorkflowDocument(document, {...options, diagnostics});
48
+ return {model, diagnostics};
49
+ }
50
+
36
51
  function interpolation(source: string): string {
37
52
  return '$'.concat('{{ ', source, ' }}');
38
53
  }
@@ -85,6 +100,13 @@ const integrationValidationContext = {
85
100
  ['linear-main', {id: 'conn_3', provider: 'linear', capabilities: ['agent_tools']}],
86
101
  ['jira-main', {id: 'conn_4', provider: 'jira', capabilities: ['agent_tools']}],
87
102
  ]),
103
+ eventCatalogs: new Map([
104
+ ['github', new Set(['push', 'pull_request.opened'])],
105
+ ['sentry', new Set(['issue.created'])],
106
+ ['linear', new Set(['Issue'])],
107
+ ['jira', new Set(['jira:issue_created'])],
108
+ ]),
109
+ fixedEventProviders: new Set(['webhook']),
88
110
  defaultConnectionSlug: 'github-main',
89
111
  } satisfies IntegrationValidationContext;
90
112
 
@@ -778,6 +800,8 @@ describe('normalizeWorkflowDocument', () => {
778
800
  message: 'Provider "github-copilot" is not supported.',
779
801
  path: ['jobs', 'fix', 'steps', 0, 'provider'],
780
802
  details: {provider: 'github-copilot'},
803
+ severity: 'error',
804
+ scope: 'definition',
781
805
  },
782
806
  ]);
783
807
  });
@@ -828,6 +852,8 @@ describe('normalizeWorkflowDocument', () => {
828
852
  'Agent model "not-a-model" is not available for harness "pi" and provider "anthropic".',
829
853
  path: ['jobs', 'fix', 'steps', 0, 'model'],
830
854
  details: {harness: 'pi', provider: 'anthropic', model: 'not-a-model'},
855
+ severity: 'error',
856
+ scope: 'definition',
831
857
  },
832
858
  ]);
833
859
  });
@@ -901,6 +927,8 @@ describe('normalizeWorkflowDocument', () => {
901
927
  'Harness "claude" does not support provider: openai. Supported providers: anthropic.',
902
928
  path: ['jobs', 'fix', 'steps', 0, 'provider'],
903
929
  details: {harness: 'claude', provider: 'openai', supportedProviders: ['anthropic']},
930
+ severity: 'error',
931
+ scope: 'definition',
904
932
  },
905
933
  ]);
906
934
  });
@@ -939,6 +967,8 @@ describe('normalizeWorkflowDocument', () => {
939
967
  'get_search_content',
940
968
  ],
941
969
  },
970
+ severity: 'error',
971
+ scope: 'definition',
942
972
  },
943
973
  ]);
944
974
  });
@@ -981,6 +1011,8 @@ describe('normalizeWorkflowDocument', () => {
981
1011
  'Agent step tools require an explicit harness because tool names are harness-specific.',
982
1012
  path: ['jobs', 'fix', 'steps', 0, 'tools'],
983
1013
  details: {tools: ['Read']},
1014
+ severity: 'error',
1015
+ scope: 'definition',
984
1016
  },
985
1017
  ]);
986
1018
  });
@@ -1068,6 +1100,8 @@ describe('normalizeWorkflowDocument', () => {
1068
1100
  thinking: 'off',
1069
1101
  supportedLevels: ['low', 'medium', 'high', 'xhigh', 'max'],
1070
1102
  },
1103
+ severity: 'error',
1104
+ scope: 'definition',
1071
1105
  });
1072
1106
  });
1073
1107
 
@@ -1433,6 +1467,78 @@ describe('normalizeWorkflowDocument', () => {
1433
1467
  ]);
1434
1468
  });
1435
1469
 
1470
+ it('reports missing resolution sources when every until matcher is inert', () => {
1471
+ const document: WorkflowDocument = {
1472
+ name: 'listen forever',
1473
+ jobs: {
1474
+ review: {
1475
+ listening: {
1476
+ on: [{source: 'github', event: 'pull_request_review'}],
1477
+ until: [{source: 'github', event: 'pull_request', filter: 'event.action'}],
1478
+ },
1479
+ steps: [{run: 'echo ok'}],
1480
+ },
1481
+ },
1482
+ };
1483
+
1484
+ const diagnostics: WorkflowModelValidationIssue[] = [];
1485
+ let error: InvalidWorkflowModelError | undefined;
1486
+ try {
1487
+ normalizeWorkflowDocument(document, {diagnostics});
1488
+ expect.fail('Expected InvalidWorkflowModelError');
1489
+ } catch (caught) {
1490
+ expect(caught).toBeInstanceOf(InvalidWorkflowModelError);
1491
+ error = caught as InvalidWorkflowModelError;
1492
+ }
1493
+
1494
+ expect(error?.issues).toEqual([
1495
+ expect.objectContaining({
1496
+ code: 'listening-job-missing-resolution-source',
1497
+ path: ['jobs', 'review', 'listening'],
1498
+ }),
1499
+ ]);
1500
+ expect(diagnostics).toEqual([
1501
+ expect.objectContaining({
1502
+ code: 'invalid-listener-filter',
1503
+ path: ['jobs', 'review', 'listening', 'until', 0, 'filter'],
1504
+ severity: 'error',
1505
+ scope: 'trigger',
1506
+ }),
1507
+ ]);
1508
+ });
1509
+
1510
+ it('drops inert until matchers from the model when a timeout resolves the job', () => {
1511
+ const document: WorkflowDocument = {
1512
+ name: 'listen with timeout',
1513
+ jobs: {
1514
+ review: {
1515
+ listening: {
1516
+ on: [{source: 'github', event: 'pull_request_review'}],
1517
+ until: [{source: 'github', event: 'pull_request', filter: 'event.action'}],
1518
+ timeout: '1h',
1519
+ },
1520
+ steps: [{run: 'echo ok'}],
1521
+ },
1522
+ },
1523
+ };
1524
+
1525
+ const {model, diagnostics} = normalizeWithDiagnostics(document);
1526
+
1527
+ expect(diagnostics).toEqual([
1528
+ expect.objectContaining({
1529
+ code: 'invalid-listener-filter',
1530
+ path: ['jobs', 'review', 'listening', 'until', 0, 'filter'],
1531
+ severity: 'error',
1532
+ scope: 'trigger',
1533
+ }),
1534
+ ]);
1535
+ expect(model.jobs[0]?.listening).toMatchObject({
1536
+ on: [{source: 'github', event: 'pull_request_review'}],
1537
+ timeoutMs: 60 * 60 * 1000,
1538
+ });
1539
+ expect(model.jobs[0]?.listening).not.toHaveProperty('until');
1540
+ });
1541
+
1436
1542
  it('reports listening timeouts above the run timeout', () => {
1437
1543
  const document: WorkflowDocument = {
1438
1544
  name: 'too long',
@@ -1502,7 +1608,6 @@ describe('normalizeWorkflowDocument', () => {
1502
1608
  ['executions root', 'executions.size() > 0', 'context-unavailable-at-predicate-site'],
1503
1609
  ['matrix root', 'matrix.os == "linux"', 'context-unavailable-at-predicate-site'],
1504
1610
  ['runner root', 'runner.os == "linux"', 'runner-context-in-server-predicate'],
1505
- ['non-boolean source', 'event.action', 'invalid-listener-filter'],
1506
1611
  ] as const)('reports invalid listener on filters for %s', (_label, filter, code) => {
1507
1612
  const document: WorkflowDocument = {
1508
1613
  name: 'invalid listener filter',
@@ -1531,6 +1636,70 @@ describe('normalizeWorkflowDocument', () => {
1531
1636
  ]);
1532
1637
  });
1533
1638
 
1639
+ it('rejects a listening job whose only on matcher is inert', () => {
1640
+ const document: WorkflowDocument = {
1641
+ name: 'invalid listener filter',
1642
+ jobs: {
1643
+ review: {
1644
+ listening: {
1645
+ on: [{source: 'github', event: 'pull_request_review', filter: 'event.action'}],
1646
+ max_executions: 1,
1647
+ },
1648
+ steps: [{run: 'echo ok'}],
1649
+ },
1650
+ },
1651
+ };
1652
+
1653
+ const error = expectInvalid(document);
1654
+
1655
+ expect(error.issues).toEqual([
1656
+ expect.objectContaining({
1657
+ code: 'listening-job-no-active-matcher',
1658
+ path: ['jobs', 'review', 'listening', 'on'],
1659
+ }),
1660
+ ]);
1661
+ });
1662
+
1663
+ it('makes an invalid listener on matcher inert and keeps the others active', () => {
1664
+ const document: WorkflowDocument = {
1665
+ name: 'invalid listener filter',
1666
+ jobs: {
1667
+ review: {
1668
+ listening: {
1669
+ on: [
1670
+ {source: 'github', event: 'pull_request_review', filter: 'event.action'},
1671
+ {
1672
+ source: 'github',
1673
+ event: 'pull_request_review',
1674
+ filter: 'event.action == "submitted"',
1675
+ },
1676
+ ],
1677
+ max_executions: 1,
1678
+ },
1679
+ steps: [{run: 'echo ok'}],
1680
+ },
1681
+ },
1682
+ };
1683
+
1684
+ const {model, diagnostics} = normalizeWithDiagnostics(document);
1685
+
1686
+ expect(diagnostics).toEqual([
1687
+ expect.objectContaining({
1688
+ code: 'invalid-listener-filter',
1689
+ path: ['jobs', 'review', 'listening', 'on', 0, 'filter'],
1690
+ details: expect.objectContaining({
1691
+ field: 'listener.on',
1692
+ source: 'event.action',
1693
+ }),
1694
+ severity: 'error',
1695
+ scope: 'trigger',
1696
+ }),
1697
+ ]);
1698
+ expect(model.jobs[0]?.listening?.on).toEqual([
1699
+ {source: 'github', event: 'pull_request_review', filter: 'event.action == "submitted"'},
1700
+ ]);
1701
+ });
1702
+
1534
1703
  it('rejects listener filters that reference jobs without a direct needs edge', () => {
1535
1704
  const document: WorkflowDocument = {
1536
1705
  name: 'listener missing needs',
@@ -3174,6 +3343,8 @@ describe('normalizeWorkflowDocument', () => {
3174
3343
  message: 'Duration must be an integer followed by ms, s, m, h, or d.',
3175
3344
  path: ['jobs', 'build', 'execution_timeout'],
3176
3345
  details: {source: 'ten minutes'},
3346
+ severity: 'error',
3347
+ scope: 'definition',
3177
3348
  },
3178
3349
  ]);
3179
3350
  });
@@ -3197,6 +3368,8 @@ describe('normalizeWorkflowDocument', () => {
3197
3368
  message: 'Duration must be between 1s and 24h.',
3198
3369
  path: ['jobs', 'build', 'execution_timeout'],
3199
3370
  details: {source: '999ms', min_ms: 1000, max_ms: 24 * 60 * 60 * 1000},
3371
+ severity: 'error',
3372
+ scope: 'definition',
3200
3373
  },
3201
3374
  ]);
3202
3375
  });
@@ -3220,6 +3393,8 @@ describe('normalizeWorkflowDocument', () => {
3220
3393
  message: 'Duration must be between 1s and 24h.',
3221
3394
  path: ['jobs', 'build', 'execution_timeout'],
3222
3395
  details: {source: '25h', min_ms: 1000, max_ms: 24 * 60 * 60 * 1000},
3396
+ severity: 'error',
3397
+ scope: 'definition',
3223
3398
  },
3224
3399
  ]);
3225
3400
  });
@@ -3243,6 +3418,8 @@ describe('normalizeWorkflowDocument', () => {
3243
3418
  message: 'Duration must be between 1s and 24h.',
3244
3419
  path: ['jobs', 'build', 'execution_timeout'],
3245
3420
  details: {source: '30d', min_ms: 1000, max_ms: 24 * 60 * 60 * 1000},
3421
+ severity: 'error',
3422
+ scope: 'definition',
3246
3423
  },
3247
3424
  ]);
3248
3425
  });
@@ -3335,6 +3512,8 @@ describe('normalizeWorkflowDocument', () => {
3335
3512
  message: 'Step "build-review" must restart from an earlier keyed step; found "producer".',
3336
3513
  path: ['jobs', 'build', 'steps', 0, 'gate', 'on_failure'],
3337
3514
  details: {stepId: 'build-review', restartFrom: 'producer'},
3515
+ severity: 'error',
3516
+ scope: 'definition',
3338
3517
  },
3339
3518
  ]);
3340
3519
  });
@@ -3363,6 +3542,8 @@ describe('normalizeWorkflowDocument', () => {
3363
3542
  message: 'Step "build-review" must restart from an earlier keyed step; found "review".',
3364
3543
  path: ['jobs', 'build', 'steps', 0, 'gate', 'on_failure'],
3365
3544
  details: {stepId: 'build-review', restartFrom: 'review'},
3545
+ severity: 'error',
3546
+ scope: 'definition',
3366
3547
  },
3367
3548
  ]);
3368
3549
  });
@@ -3392,6 +3573,8 @@ describe('normalizeWorkflowDocument', () => {
3392
3573
  message: 'Step "build-review" must restart from an earlier keyed step; found "producer".',
3393
3574
  path: ['jobs', 'build', 'steps', 0, 'gate', 'on_failure'],
3394
3575
  details: {stepId: 'build-review', restartFrom: 'producer'},
3576
+ severity: 'error',
3577
+ scope: 'definition',
3395
3578
  },
3396
3579
  ]);
3397
3580
  });
@@ -3415,6 +3598,8 @@ describe('normalizeWorkflowDocument', () => {
3415
3598
  message: 'Job "test" depends on unknown job "build".',
3416
3599
  path: ['jobs', 'test', 'needs'],
3417
3600
  details: {job: 'test', dependency: 'build'},
3601
+ severity: 'error',
3602
+ scope: 'definition',
3418
3603
  },
3419
3604
  ]);
3420
3605
  });
@@ -3438,6 +3623,8 @@ describe('normalizeWorkflowDocument', () => {
3438
3623
  message: 'Job "test" depends on itself.',
3439
3624
  path: ['jobs', 'test', 'needs'],
3440
3625
  details: {job: 'test'},
3626
+ severity: 'error',
3627
+ scope: 'definition',
3441
3628
  },
3442
3629
  ]);
3443
3630
  });
@@ -3465,6 +3652,8 @@ describe('normalizeWorkflowDocument', () => {
3465
3652
  message: 'Circular dependency detected among jobs: build, test.',
3466
3653
  path: ['jobs'],
3467
3654
  details: {cycleSourceNames: ['build', 'test'], cycleJobIds: ['build', 'test']},
3655
+ severity: 'error',
3656
+ scope: 'definition',
3468
3657
  },
3469
3658
  ]);
3470
3659
  });
@@ -3496,6 +3685,8 @@ describe('normalizeWorkflowDocument', () => {
3496
3685
  message: 'Circular dependency detected among jobs: build, test.',
3497
3686
  path: ['jobs'],
3498
3687
  details: {cycleSourceNames: ['build', 'test'], cycleJobIds: ['build', 'test']},
3688
+ severity: 'error',
3689
+ scope: 'definition',
3499
3690
  },
3500
3691
  ]);
3501
3692
  });
@@ -3521,6 +3712,8 @@ describe('normalizeWorkflowDocument', () => {
3521
3712
  message: 'Job keys "build app" and "build-app" resolve to the same stable id "build-app".',
3522
3713
  path: ['jobs', 'build-app'],
3523
3714
  details: {id: 'build-app', sourceKeys: ['build app', 'build-app']},
3715
+ severity: 'error',
3716
+ scope: 'definition',
3524
3717
  },
3525
3718
  ]);
3526
3719
  });
@@ -3546,7 +3739,7 @@ describe('normalizeWorkflowDocument', () => {
3546
3739
  ]);
3547
3740
  });
3548
3741
 
3549
- it('reports stable trigger id collisions', () => {
3742
+ it('reports stable trigger id collisions with the later trigger inert', () => {
3550
3743
  const document: WorkflowDocument = {
3551
3744
  name: 'trigger collision',
3552
3745
  triggers: {
@@ -3560,17 +3753,20 @@ describe('normalizeWorkflowDocument', () => {
3560
3753
  },
3561
3754
  };
3562
3755
 
3563
- const error = expectInvalid(document);
3756
+ const {model, diagnostics} = normalizeWithDiagnostics(document);
3564
3757
 
3565
- expect(error.issues).toEqual([
3758
+ expect(diagnostics).toEqual([
3566
3759
  {
3567
3760
  code: 'duplicate-trigger-id',
3568
3761
  message:
3569
3762
  'Trigger keys "main_push" and "main push" resolve to the same stable id "main-push".',
3570
3763
  path: ['triggers', 'main push'],
3571
3764
  details: {id: 'main-push', sourceKeys: ['main_push', 'main push']},
3765
+ severity: 'error',
3766
+ scope: 'trigger',
3572
3767
  },
3573
3768
  ]);
3769
+ expect(model.triggers.map((trigger) => trigger.key)).toEqual(['main_push']);
3574
3770
  });
3575
3771
 
3576
3772
  it('reports stable step id collisions inside a job', () => {
@@ -3591,6 +3787,8 @@ describe('normalizeWorkflowDocument', () => {
3591
3787
  message: 'Steps 0 and 1 in job "build" resolve to the same stable id "build-step-1".',
3592
3788
  path: ['jobs', 'build', 'steps', 1],
3593
3789
  details: {id: 'build-step-1', indexes: [0, 1]},
3790
+ severity: 'error',
3791
+ scope: 'definition',
3594
3792
  },
3595
3793
  ]);
3596
3794
  });
@@ -3674,17 +3872,6 @@ describe('normalizeWorkflowDocument', () => {
3674
3872
  site: 'ingest',
3675
3873
  },
3676
3874
  ],
3677
- [
3678
- 'non-boolean shape',
3679
- 'event.ref',
3680
- 'invalid-trigger-filter',
3681
- {
3682
- field: 'trigger.filter',
3683
- source: 'event.ref',
3684
- contextRoots: ['event'],
3685
- reason: 'Predicate source must be boolean-shaped.',
3686
- },
3687
- ],
3688
3875
  ] as const)('rejects trigger filters with %s', (_label, filter, code, details) => {
3689
3876
  const document: WorkflowDocument = {
3690
3877
  name: 'invalid trigger filter',
@@ -3710,11 +3897,50 @@ describe('normalizeWorkflowDocument', () => {
3710
3897
  message: expect.any(String),
3711
3898
  path: ['triggers', 'main', 'filter'],
3712
3899
  details,
3900
+ severity: 'error',
3901
+ scope: 'definition',
3713
3902
  },
3714
3903
  ]);
3715
3904
  });
3716
3905
 
3717
- it.each(['manual', 'cron'] as const)('rejects filters on %s triggers', (source) => {
3906
+ it('makes a trigger with a non-boolean filter inert', () => {
3907
+ const document: WorkflowDocument = {
3908
+ name: 'invalid trigger filter',
3909
+ triggers: {
3910
+ main: {
3911
+ source: 'github',
3912
+ event: 'push',
3913
+ filter: 'event.ref',
3914
+ },
3915
+ },
3916
+ jobs: {
3917
+ build: {
3918
+ steps: [{run: 'npm run build'}],
3919
+ },
3920
+ },
3921
+ };
3922
+
3923
+ const {model, diagnostics} = normalizeWithDiagnostics(document);
3924
+
3925
+ expect(diagnostics).toEqual([
3926
+ {
3927
+ code: 'invalid-trigger-filter',
3928
+ message: expect.any(String),
3929
+ path: ['triggers', 'main', 'filter'],
3930
+ details: {
3931
+ field: 'trigger.filter',
3932
+ source: 'event.ref',
3933
+ contextRoots: ['event'],
3934
+ reason: 'Predicate source must be boolean-shaped.',
3935
+ },
3936
+ severity: 'error',
3937
+ scope: 'trigger',
3938
+ },
3939
+ ]);
3940
+ expect(model.triggers).toEqual([]);
3941
+ });
3942
+
3943
+ it.each(['manual', 'cron'] as const)('makes a %s trigger with a filter inert', (source) => {
3718
3944
  const document: WorkflowDocument = {
3719
3945
  name: 'unsupported trigger filter',
3720
3946
  triggers: {
@@ -3732,16 +3958,19 @@ describe('normalizeWorkflowDocument', () => {
3732
3958
  },
3733
3959
  };
3734
3960
 
3735
- const error = expectInvalid(document);
3961
+ const {model, diagnostics} = normalizeWithDiagnostics(document);
3736
3962
 
3737
- expect(error.issues).toEqual([
3963
+ expect(diagnostics).toEqual([
3738
3964
  {
3739
3965
  code: 'invalid-trigger-filter',
3740
3966
  message: `A ${source} trigger cannot define a filter because it does not receive an event payload.`,
3741
3967
  path: ['triggers', 'main', 'filter'],
3742
3968
  details: {source: 'event.ref == "refs/heads/main"', triggerSource: source},
3969
+ severity: 'error',
3970
+ scope: 'trigger',
3743
3971
  },
3744
3972
  ]);
3973
+ expect(model.triggers).toEqual([]);
3745
3974
  });
3746
3975
 
3747
3976
  it('maps trigger with values to model inputs', () => {
@@ -3843,7 +4072,64 @@ describe('normalizeWorkflowDocument', () => {
3843
4072
  ]);
3844
4073
  });
3845
4074
 
3846
- it('reports a cron trigger with a non-tick event', () => {
4075
+ it('materializes fire for a manual trigger with the event omitted', () => {
4076
+ const document: WorkflowDocument = {
4077
+ name: 'manual trigger',
4078
+ triggers: {
4079
+ manual: {source: 'manual'},
4080
+ },
4081
+ jobs: {
4082
+ build: {
4083
+ steps: [{run: 'npm run build'}],
4084
+ },
4085
+ },
4086
+ };
4087
+
4088
+ const model = normalizeWorkflowDocument(document);
4089
+
4090
+ expect(model.triggers).toEqual([
4091
+ {
4092
+ id: 'manual',
4093
+ key: 'manual',
4094
+ source: 'manual',
4095
+ event: 'fire',
4096
+ },
4097
+ ]);
4098
+ });
4099
+
4100
+ it('materializes tick for a cron trigger with the event omitted', () => {
4101
+ const document: WorkflowDocument = {
4102
+ name: 'nightly trigger',
4103
+ triggers: {
4104
+ nightly: {
4105
+ source: 'cron',
4106
+ config: {schedule: '0 2 * * *'},
4107
+ },
4108
+ },
4109
+ jobs: {
4110
+ build: {
4111
+ steps: [{run: 'npm run build'}],
4112
+ },
4113
+ },
4114
+ };
4115
+
4116
+ const model = normalizeWorkflowDocument(document);
4117
+
4118
+ expect(model.triggers).toEqual([
4119
+ {
4120
+ id: 'nightly',
4121
+ key: 'nightly',
4122
+ source: 'cron',
4123
+ event: 'tick',
4124
+ config: {
4125
+ schedule: '0 2 * * *',
4126
+ timezone: 'UTC',
4127
+ },
4128
+ },
4129
+ ]);
4130
+ });
4131
+
4132
+ it('makes a cron trigger with a non-tick event inert', () => {
3847
4133
  const document: WorkflowDocument = {
3848
4134
  name: 'nightly trigger',
3849
4135
  triggers: {
@@ -3860,19 +4146,58 @@ describe('normalizeWorkflowDocument', () => {
3860
4146
  },
3861
4147
  };
3862
4148
 
3863
- const error = expectInvalid(document);
4149
+ const {model, diagnostics} = normalizeWithDiagnostics(document);
3864
4150
 
3865
- expect(error.issues).toEqual([
4151
+ expect(diagnostics).toEqual([
3866
4152
  {
3867
4153
  code: 'invalid-cron-event',
3868
4154
  message: 'A cron trigger must use event "tick"; found "push".',
3869
4155
  path: ['triggers', 'nightly', 'event'],
3870
4156
  details: {event: 'push'},
4157
+ severity: 'error',
4158
+ scope: 'trigger',
4159
+ },
4160
+ ]);
4161
+ expect(model.triggers).toEqual([]);
4162
+ });
4163
+
4164
+ it('keeps the event absent for integration triggers with the event omitted', () => {
4165
+ const document: WorkflowDocument = {
4166
+ name: 'source subscriptions',
4167
+ triggers: {
4168
+ on_any_deploy: {
4169
+ source: 'deploy_hook',
4170
+ },
4171
+ on_any_github_event: {
4172
+ source: 'github_acme',
4173
+ },
4174
+ },
4175
+ jobs: {
4176
+ build: {
4177
+ steps: [{run: 'npm run build'}],
4178
+ },
4179
+ },
4180
+ };
4181
+
4182
+ const model = normalizeWorkflowDocument(document);
4183
+
4184
+ expect(model.triggers).toEqual([
4185
+ {
4186
+ id: 'on-any-deploy',
4187
+ key: 'on_any_deploy',
4188
+ source: 'deploy_hook',
4189
+ },
4190
+ {
4191
+ id: 'on-any-github-event',
4192
+ key: 'on_any_github_event',
4193
+ source: 'github_acme',
3871
4194
  },
3872
4195
  ]);
4196
+ expect(model.triggers[0]).not.toHaveProperty('event');
4197
+ expect(model.triggers[1]).not.toHaveProperty('event');
3873
4198
  });
3874
4199
 
3875
- it('reports a cron trigger without a schedule', () => {
4200
+ it('makes a cron trigger without a schedule inert', () => {
3876
4201
  const document: WorkflowDocument = {
3877
4202
  name: 'nightly trigger',
3878
4203
  triggers: {
@@ -3888,22 +4213,25 @@ describe('normalizeWorkflowDocument', () => {
3888
4213
  },
3889
4214
  };
3890
4215
 
3891
- const error = expectInvalid(document);
4216
+ const {model, diagnostics} = normalizeWithDiagnostics(document);
3892
4217
 
3893
- expect(error.issues).toEqual([
4218
+ expect(diagnostics).toEqual([
3894
4219
  {
3895
4220
  code: 'missing-cron-schedule',
3896
4221
  message: 'A cron trigger requires a schedule.',
3897
4222
  path: ['triggers', 'nightly', 'config', 'schedule'],
4223
+ severity: 'error',
4224
+ scope: 'trigger',
3898
4225
  },
3899
4226
  ]);
4227
+ expect(model.triggers).toEqual([]);
3900
4228
  });
3901
4229
 
3902
4230
  it.each([
3903
4231
  ['malformed', 'not a cron'],
3904
4232
  ['6-field', '0 0 2 * * *'],
3905
4233
  ['preset', '@daily'],
3906
- ])('reports an invalid %s cron schedule', (_label, schedule) => {
4234
+ ])('makes a cron trigger with an invalid %s schedule inert', (_label, schedule) => {
3907
4235
  const document: WorkflowDocument = {
3908
4236
  name: 'nightly trigger',
3909
4237
  triggers: {
@@ -3920,19 +4248,22 @@ describe('normalizeWorkflowDocument', () => {
3920
4248
  },
3921
4249
  };
3922
4250
 
3923
- const error = expectInvalid(document);
4251
+ const {model, diagnostics} = normalizeWithDiagnostics(document);
3924
4252
 
3925
- expect(error.issues).toEqual([
4253
+ expect(diagnostics).toEqual([
3926
4254
  {
3927
4255
  code: 'invalid-cron-schedule',
3928
4256
  message: 'Cron trigger schedule must be a valid 5-field cron expression.',
3929
4257
  path: ['triggers', 'nightly', 'config', 'schedule'],
3930
4258
  details: {schedule},
4259
+ severity: 'error',
4260
+ scope: 'trigger',
3931
4261
  },
3932
4262
  ]);
4263
+ expect(model.triggers).toEqual([]);
3933
4264
  });
3934
4265
 
3935
- it('reports an invalid cron timezone', () => {
4266
+ it('makes a cron trigger with an invalid timezone inert', () => {
3936
4267
  const document: WorkflowDocument = {
3937
4268
  name: 'nightly trigger',
3938
4269
  triggers: {
@@ -3952,16 +4283,19 @@ describe('normalizeWorkflowDocument', () => {
3952
4283
  },
3953
4284
  };
3954
4285
 
3955
- const error = expectInvalid(document);
4286
+ const {model, diagnostics} = normalizeWithDiagnostics(document);
3956
4287
 
3957
- expect(error.issues).toEqual([
4288
+ expect(diagnostics).toEqual([
3958
4289
  {
3959
4290
  code: 'invalid-cron-timezone',
3960
4291
  message: 'Cron trigger timezone must be a valid IANA time zone.',
3961
4292
  path: ['triggers', 'nightly', 'config', 'timezone'],
3962
4293
  details: {timezone: 'Not/A/Zone'},
4294
+ severity: 'error',
4295
+ scope: 'trigger',
3963
4296
  },
3964
4297
  ]);
4298
+ expect(model.triggers).toEqual([]);
3965
4299
  });
3966
4300
 
3967
4301
  it('allows multiple cron triggers', () => {
@@ -4009,7 +4343,47 @@ describe('normalizeWorkflowDocument', () => {
4009
4343
  expect(model.triggers).toMatchObject([{id: 'manual', source: 'manual', event: 'fire'}]);
4010
4344
  });
4011
4345
 
4012
- it('reports multiple manual triggers as a semantic rule', () => {
4346
+ it('keeps other triggers active when a cron trigger is inert', () => {
4347
+ const document: WorkflowDocument = {
4348
+ name: 'partially broken triggers',
4349
+ triggers: {
4350
+ nightly: {
4351
+ source: 'cron',
4352
+ event: 'tick',
4353
+ config: {schedule: 'not a cron'},
4354
+ },
4355
+ on_demand: {source: 'manual', event: 'fire'},
4356
+ },
4357
+ jobs: {
4358
+ build: {
4359
+ steps: [{run: 'npm run build'}],
4360
+ },
4361
+ },
4362
+ };
4363
+
4364
+ const {model, diagnostics} = normalizeWithDiagnostics(document);
4365
+
4366
+ expect(diagnostics).toEqual([
4367
+ {
4368
+ code: 'invalid-cron-schedule',
4369
+ message: 'Cron trigger schedule must be a valid 5-field cron expression.',
4370
+ path: ['triggers', 'nightly', 'config', 'schedule'],
4371
+ details: {schedule: 'not a cron'},
4372
+ severity: 'error',
4373
+ scope: 'trigger',
4374
+ },
4375
+ ]);
4376
+ expect(model.triggers).toEqual([
4377
+ {
4378
+ id: 'on-demand',
4379
+ key: 'on_demand',
4380
+ source: 'manual',
4381
+ event: 'fire',
4382
+ },
4383
+ ]);
4384
+ });
4385
+
4386
+ it('keeps the first manual trigger active and makes later ones inert', () => {
4013
4387
  const document: WorkflowDocument = {
4014
4388
  name: 'manual triggers',
4015
4389
  triggers: {
@@ -4023,16 +4397,51 @@ describe('normalizeWorkflowDocument', () => {
4023
4397
  },
4024
4398
  };
4025
4399
 
4026
- const error = expectInvalid(document);
4400
+ const {model, diagnostics} = normalizeWithDiagnostics(document);
4027
4401
 
4028
- expect(error.issues).toEqual([
4402
+ expect(diagnostics).toEqual([
4029
4403
  {
4030
4404
  code: 'multiple-manual-triggers',
4031
- message: 'A workflow may declare at most one manual trigger; found 2: one, two.',
4032
- path: ['triggers'],
4405
+ message:
4406
+ 'A workflow may declare at most one manual trigger; found 2: one, two. This trigger is inert because it is not the first manual trigger in document order.',
4407
+ path: ['triggers', 'two'],
4033
4408
  details: {manualTriggerKeys: ['one', 'two']},
4409
+ severity: 'error',
4410
+ scope: 'trigger',
4034
4411
  },
4035
4412
  ]);
4413
+ expect(model.triggers.map((trigger) => trigger.key)).toEqual(['one']);
4414
+ });
4415
+
4416
+ it('promotes the first valid manual trigger when an earlier one is inert', () => {
4417
+ const document: WorkflowDocument = {
4418
+ name: 'manual triggers with an inert first trigger',
4419
+ triggers: {
4420
+ broken: {
4421
+ source: 'manual',
4422
+ event: 'fire',
4423
+ filter: 'event.ref == "refs/heads/main"',
4424
+ },
4425
+ working: {source: 'manual', event: 'fire'},
4426
+ },
4427
+ jobs: {
4428
+ build: {
4429
+ steps: [{run: 'npm run build'}],
4430
+ },
4431
+ },
4432
+ };
4433
+
4434
+ const {model, diagnostics} = normalizeWithDiagnostics(document);
4435
+
4436
+ expect(diagnostics).toEqual([
4437
+ expect.objectContaining({
4438
+ code: 'invalid-trigger-filter',
4439
+ path: ['triggers', 'broken', 'filter'],
4440
+ severity: 'error',
4441
+ scope: 'trigger',
4442
+ }),
4443
+ ]);
4444
+ expect(model.triggers.map((trigger) => trigger.key)).toEqual(['working']);
4036
4445
  });
4037
4446
 
4038
4447
  it('accumulates independent semantic issues in one pass', () => {
@@ -4985,6 +5394,55 @@ describe('normalizeWorkflowDocument', () => {
4985
5394
  ]);
4986
5395
  });
4987
5396
 
5397
+ it('accepts a managed provider on Claude when the catalog exposes an Anthropic model', () => {
5398
+ const providerId = 'shipfox-managed';
5399
+ const managedCatalog: AgentValidationCatalog = {
5400
+ ...agentValidationCatalog,
5401
+ providers: [
5402
+ ...agentValidationCatalog.providers,
5403
+ {id: providerId, support_status: 'supported'},
5404
+ ],
5405
+ harnesses: agentValidationCatalog.harnesses.map((harness) =>
5406
+ harness.id === 'claude'
5407
+ ? {
5408
+ ...harness,
5409
+ supported_provider_ids: [...harness.supported_provider_ids, providerId],
5410
+ model_ids_by_provider: {
5411
+ ...harness.model_ids_by_provider,
5412
+ [providerId]: ['managed-claude'],
5413
+ },
5414
+ }
5415
+ : harness,
5416
+ ),
5417
+ };
5418
+
5419
+ const model = normalizeWorkflowDocument(
5420
+ {
5421
+ name: 'managed Claude provider',
5422
+ jobs: {
5423
+ fix: {
5424
+ steps: [
5425
+ {
5426
+ harness: 'claude',
5427
+ provider: providerId,
5428
+ model: 'managed-claude',
5429
+ prompt: 'Fix it.',
5430
+ },
5431
+ ],
5432
+ },
5433
+ },
5434
+ },
5435
+ {agentValidationCatalog: managedCatalog},
5436
+ );
5437
+
5438
+ expect(model.jobs[0]?.steps[0]).toMatchObject({
5439
+ kind: 'agent',
5440
+ harness: 'claude',
5441
+ provider: providerId,
5442
+ model: 'managed-claude',
5443
+ });
5444
+ });
5445
+
4988
5446
  it('reports typed interpolation expression errors for trusted known contexts', () => {
4989
5447
  const document: WorkflowDocument = {
4990
5448
  name: 'bad trusted path',