Ctrl K
ComunidadGuías…

docs/specs/FS-20260514-jira-source-implementation-plan.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Jira Source Plugin Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build Jira Cloud as a custom Chartbrew source with connection setup, Jira builder, normalized runtime, templates, and source-owned AI planning.

**Architecture:** Jira is a custom source with `id`, `type`, and `subType` all set to `"jira"`. Backend runtime lives in `server/sources/plugins/jira/`; frontend UI lives in `client/src/sources/jira/`; templates are JSON files loaded through the existing source template system. Jira uses source actions for metadata and source-owned AI tools for planning, while dashboard creation from templates is handled by a generic orchestrator tool.

**Tech Stack:** Node.js, Express, Sequelize, request-promise, React, Redux Toolkit, HeroUI, Vite, existing Chartbrew source plugin registry.

---

## File Structure

Create:

- `server/sources/plugins/jira/jira.plugin.js` - Jira source metadata, capabilities, template registration, backend hooks.
- `server/sources/plugins/jira/jira.connection.js` - Jira Cloud auth, URL normalization, safe request helpers, metadata actions.
- `server/sources/plugins/jira/jira.resources.js` - Supported resources, fields, transforms, template constants, field mapping helpers.
- `server/sources/plugins/jira/jira.protocol.js` - DataRequest defaults, variable application, validation, runtime execution, normalization, caching/audit.
- `server/sources/plugins/jira/ai/jira.ai.js` - Source-owned planner, schema, preview, template recommendation.
- `server/sources/plugins/jira/templates/project-overview.json` - Project overview starter bundle.
- `server/sources/plugins/jira/templates/sprint-health.json` - Sprint health starter bundle.
- `server/sources/plugins/jira/templates/bug-tracking.json` - Bug tracking starter bundle.
- `server/sources/plugins/jira/templates/team-workload.json` - Team workload starter bundle.
- `server/tests/unit/jiraProtocol.test.js` - Protocol validation, JQL building, normalization, pagination, and variable tests.
- `server/tests/unit/jiraTemplates.test.js` - Template validation and variable binding coverage.
- `client/src/sources/jira/jira.source.js` - Frontend source metadata and defaults.
- `client/src/sources/jira/jira-connection-form.jsx` - Jira connection form.
- `client/src/sources/jira/jira-builder.jsx` - Jira dataset builder container following Stripe Official layout.
- `client/src/sources/jira/jira-template-setup.jsx` - Jira template bundle setup.
- `client/src/sources/jira/jira-builder.constants.jsx` - UI resource and option constants.
- `client/src/sources/jira/jira-builder.utils.js` - Builder config normalization and preview utilities.
- `client/src/sources/jira/assets/jira.svg` - Jira logo asset.
- `client/src/sources/jira/components/jira-builder-context.jsx` - Builder state provider.
- `client/src/sources/jira/components/jira-resource-step.jsx` - Resource/category selection.
- `client/src/sources/jira/components/jira-config-step.jsx` - Visual/JQL/advanced configuration.
- `client/src/sources/jira/components/jira-preview-step.jsx` - Preview and normalized output table.
- `client/src/sources/jira/components/jira-summary-sidebar.jsx` - Summary, warnings, variables, suggested charts.
- `server/modules/ai/orchestrator/tools/createDashboardFromTemplate.js` - Generic AI tool for registered chart templates.

Modify:

- `server/sources/index.js` - Register Jira plugin.
- `client/src/sources/definitions.js` - Register Jira source metadata.
- `client/src/sources/index.js` - Register Jira connection form, builder, and template setup.
- `server/modules/ai/orchestrator/tools/index.js` - Export generic dashboard-template tool.
- `server/modules/ai/orchestrator/orchestrator.js` - Add generic dashboard-template tool schema and dispatch.
- `server/tests/unit/sourceRegistry.test.js` - Assert Jira resolution.
- `server/tests/unit/sourcePluginStructure.test.js` - Assert Jira plugin shape.
- `server/tests/integration/connectionRoute.security.test.js` - Assert Jira source-action security.
- `server/tests/unit/sourceAiHarness.test.js` - Add Jira source-owned AI coverage and dashboard-template tool coverage.

---

## Task 1: Backend Plugin Skeleton And Registry

**Files:**
- Create: `server/sources/plugins/jira/jira.plugin.js`
- Create: `server/sources/plugins/jira/jira.protocol.js`
- Create: `server/sources/plugins/jira/jira.connection.js`
- Create: `server/sources/plugins/jira/jira.resources.js`
- Modify: `server/sources/index.js`
- Modify: `server/tests/unit/sourceRegistry.test.js`
- Modify: `server/tests/unit/sourcePluginStructure.test.js`

- [ ] **Step 1: Write failing registry tests**

Add assertions to `server/tests/unit/sourceRegistry.test.js` that load the source registry and verify:

```js
const jira = getSourceById("jira");
expect(jira.id).toBe("jira");
expect(jira.type).toBe("jira");
expect(jira.subType).toBe("jira");
expect(getSourceForConnection({ type: "jira", subType: "jira" }).id).toBe("jira");
```

Add assertions to `server/tests/unit/sourcePluginStructure.test.js` that Jira exposes:

```js
expect(jira.capabilities.connection.supportsTest).toBe(true);
expect(jira.capabilities.connection.authModes).toContain("api_token");
expect(jira.capabilities.data.supportsVariables).toBe(true);
expect(jira.capabilities.templates.charts).toBe(true);
expect(jira.capabilities.ai.hasTools).toBe(true);
expect(jira.backend.runDataRequest).toEqual(expect.any(Function));
```

- [ ] **Step 2: Run failing tests**

Run:

```bash
npm run test:run -- tests/unit/sourceRegistry.test.js tests/unit/sourcePluginStructure.test.js
```

Expected: FAIL because `./plugins/jira/jira.plugin` is not registered.

- [ ] **Step 3: Add minimal Jira resources**

Create `server/sources/plugins/jira/jira.resources.js`:

```js
const DEFAULT_FIELDS = [
  "key",
  "summary",
  "status",
  "assignee",
  "reporter",
  "priority",
  "issuetype",
  "created",
  "updated",
  "resolutiondate",
  "project",
  "parent",
  "fixVersions",
  "labels",
];

const JIRA_RESOURCES = {
  issues: { label: "Issues", endpoint: "/rest/api/3/search", fields: DEFAULT_FIELDS },
  boards: { label: "Boards", endpoint: "/rest/agile/1.0/board" },
  sprints: { label: "Sprints", endpoint: "/rest/agile/1.0/board/{boardId}/sprint" },
  sprint_issues: { label: "Sprint issues", endpoint: "/rest/agile/1.0/sprint/{sprintId}/issue", fields: DEFAULT_FIELDS },
  versions: { label: "Versions", endpoint: "/rest/api/3/project/{projectIdOrKey}/versions" },
  fields: { label: "Fields", endpoint: "/rest/api/3/field" },
  users: { label: "Users", endpoint: "/rest/api/3/user/search" },
};

const FIELD_MAPPING_NAMES = {
  storyPoints: ["story points", "story point estimate", "estimate"],
  severity: ["severity"],
  team: ["team"],
};

module.exports = {
  DEFAULT_FIELDS,
  FIELD_MAPPING_NAMES,
  JIRA_RESOURCES,
};
```

- [ ] **Step 4: Add minimal protocol**

Create `server/sources/plugins/jira/jira.protocol.js`:

```js
const { JIRA_RESOURCES, DEFAULT_FIELDS } = require("./jira.resources");

const DEFAULT_CONFIGURATION = {
  source: "jira",
  resource: "issues",
  mode: "visual",
  jql: "created >= {{start_date}} AND created <= {{end_date}} ORDER BY updated DESC",
  fields: DEFAULT_FIELDS,
  transform: { type: "raw" },
  pagination: { startAt: 0, maxResults: 100, maxRecords: 5000 },
};

function getDefaultDataRequest() {
  return {
    method: "GET",
    route: null,
    headers: {},
    body: null,
    conditions: null,
    configuration: { ...DEFAULT_CONFIGURATION },
    query: null,
    pagination: true,
    items: "issues",
    itemsLimit: 5000,
    offset: "startAt",
    paginationField: null,
    template: "jira",
    useGlobalHeaders: true,
  };
}

function validateConfiguration(configuration = {}) {
  const config = {
    ...DEFAULT_CONFIGURATION,
    ...configuration,
    pagination: {
      ...DEFAULT_CONFIGURATION.pagination,
      ...(configuration.pagination || {}),
    },
  };
  const errors = [];

  if (config.source !== "jira") errors.push("configuration.source must be jira");
  if (!JIRA_RESOURCES[config.resource]) errors.push(`Unsupported Jira resource: ${config.resource || "missing"}`);
  if (!["visual", "jql", "advanced"].includes(config.mode)) errors.push("mode must be visual, jql, or advanced");
  if (config.resource === "issues" && !String(config.jql || "").trim()) errors.push("jql is required for issue searches");
  if (Number(config.pagination.maxResults) > 100) errors.push("pagination.maxResults cannot exceed 100");
  if (Number(config.pagination.maxRecords) > 10000) errors.push("pagination.maxRecords cannot exceed 10000");

  return {
    valid: errors.length === 0,
    errors,
    warnings: [],
    configuration: config,
  };
}

function assertValidConfiguration(configuration) {
  const validation = validateConfiguration(configuration);
  if (!validation.valid) throw new Error(validation.errors.join(" "));
  return validation.configuration;
}

function getBuilderMetadata() {
  return Promise.resolve({
    source: "jira",
    resources: JIRA_RESOURCES,
    defaults: DEFAULT_CONFIGURATION,
  });
}

async function testConnection({ connection }) {
  const jiraConnection = require("./jira.connection"); // eslint-disable-line global-require
  const myself = await jiraConnection.getMyself(connection);
  return { success: true, metadata: myself };
}

function testUnsavedConnection({ connection }) {
  return testConnection({ connection });
}

async function runDataRequest() {
  throw new Error("Jira runtime is not implemented yet");
}

module.exports = {
  assertValidConfiguration,
  getBuilderMetadata,
  getDefaultDataRequest,
  runDataRequest,
  testConnection,
  testUnsavedConnection,
  validateConfiguration,
};
```

- [ ] **Step 5: Add minimal connection helper**

Create `server/sources/plugins/jira/jira.connection.js`:

```js
const request = require("request-promise");

function normalizeSiteUrl(siteUrl = "") {
  const trimmed = String(siteUrl).trim().replace(/\/+$/, "");
  if (!/^https:\/\/[^/]+\.atlassian\.net$/i.test(trimmed)) {
    throw new Error("Enter a Jira Cloud site URL like https://example.atlassian.net");
  }
  return trimmed;
}

function toPlainObject(value) {
  if (!value) return {};
  if (typeof value.toJSON === "function") return value.toJSON();
  return { ...value };
}

function getCredentials(connection) {
  const plain = toPlainObject(connection);
  const siteUrl = normalizeSiteUrl(plain.host || plain.options?.jira?.siteUrl || plain.authentication?.siteUrl);
  const email = plain.authentication?.email || plain.authentication?.user || plain.username || "";
  const apiToken = plain.authentication?.apiToken || plain.authentication?.token || plain.password || "";
  if (!email) throw new Error("Enter your Jira account email");
  if (!apiToken) throw new Error("Enter your Jira API token");
  return { siteUrl, email, apiToken };
}

function jiraRequest(connection, route, options = {}) {
  const { siteUrl, email, apiToken } = getCredentials(connection);
  return request({
    method: options.method || "GET",
    uri: `${siteUrl}${route}`,
    auth: { user: email, pass: apiToken },
    qs: options.qs || undefined,
    body: options.body || undefined,
    json: true,
    resolveWithFullResponse: false,
  });
}

function getMyself(connection) {
  return jiraRequest(connection, "/rest/api/3/myself");
}

module.exports = {
  getCredentials,
  getMyself,
  jiraRequest,
  normalizeSiteUrl,
};
```

- [ ] **Step 6: Add plugin and registry entry**

Create `server/sources/plugins/jira/jira.plugin.js`:

```js
const path = require("path");
const jiraProtocol = require("./jira.protocol");

module.exports = {
  id: "jira",
  type: "jira",
  subType: "jira",
  name: "Jira",
  category: "productivity",
  description: "Connect to Jira Cloud projects, issues, boards, and sprints.",
  capabilities: {
    connection: {
      supportsTest: true,
      supportsOAuth: false,
      supportsFiles: false,
      authModes: ["api_token"],
    },
    data: {
      supportsQuery: false,
      supportsSchema: true,
      supportsResourcePicker: true,
      supportsPagination: true,
      supportsVariables: true,
      supportsJoins: true,
    },
    templates: {
      datasets: true,
      charts: true,
      dashboards: false,
    },
    ai: {
      canGenerateDatasets: true,
      canGenerateQueries: false,
      hasSourceInstructions: true,
      hasTools: true,
    },
  },
  backend: jiraProtocol,
  templates: {
    directory: path.join(__dirname, "templates"),
    chartTemplates: ["project-overview", "sprint-health", "bug-tracking", "team-workload"],
    defaults: {
      dataRequest: jiraProtocol.getDefaultDataRequest(),
    },
  },
};
```

Modify `server/sources/index.js`:

```js
const jira = require("./plugins/jira/jira.plugin");
```

Add `jira` to the `sources` array near the other custom sources.

- [ ] **Step 7: Run registry tests**

Run:

```bash
npm run test:run -- tests/unit/sourceRegistry.test.js tests/unit/sourcePluginStructure.test.js
```

Expected: PASS for Jira registry and structure assertions.

- [ ] **Step 8: Commit backend skeleton**

```bash
git add server/sources/index.js server/sources/plugins/jira server/tests/unit/sourceRegistry.test.js server/tests/unit/sourcePluginStructure.test.js
git commit -m "feat: add jira source plugin skeleton"
```

---

## Task 2: Jira Connection Actions And Metadata

**Files:**
- Modify: `server/sources/plugins/jira/jira.connection.js`
- Modify: `server/sources/plugins/jira/jira.protocol.js`
- Test: `server/tests/unit/jiraProtocol.test.js`
- Modify: `server/tests/integration/connectionRoute.security.test.js`

- [ ] **Step 1: Write action tests**

Create `server/tests/unit/jiraProtocol.test.js` with tests that stub `jira.connection.jiraRequest` and verify:

```js
expect(await actions.listProjects({ connection })).toEqual([{ id: "10000", key: "CHART", name: "Chartbrew" }]);
expect(await actions.listBoards({ connection, params: { projectKeyOrId: "CHART" } })).toEqual([{ id: 12, name: "CHART board", type: "scrum" }]);
expect(await actions.detectFieldMappings({ connection })).toEqual({
  fieldMappings: { storyPoints: "customfield_10016" },
  candidates: expect.any(Object),
});
```

Add integration assertions that an unknown Jira action returns `400` and allowed actions require a team-scoped connection, matching existing source action security patterns.

- [ ] **Step 2: Run tests to verify failure**

Run:

```bash
npm run test:run -- tests/unit/jiraProtocol.test.js tests/integration/connectionRoute.security.test.js
```

Expected: FAIL because Jira actions are not implemented.

- [ ] **Step 3: Add metadata helpers**

Add to `jira.connection.js`:

```js
function listProjects(connection) {
  return jiraRequest(connection, "/rest/api/3/project/search", { qs: { maxResults: 50 } })
    .then((response) => (response.values || []).map((project) => ({
      id: project.id,
      key: project.key,
      name: project.name,
      projectTypeKey: project.projectTypeKey,
    })));
}

function listBoards(connection, params = {}) {
  return jiraRequest(connection, "/rest/agile/1.0/board", {
    qs: {
      maxResults: 50,
      projectKeyOrId: params.projectKeyOrId || undefined,
      type: params.type || undefined,
    },
  }).then((response) => (response.values || []).map((board) => ({
    id: board.id,
    name: board.name,
    type: board.type,
  })));
}

function listFields(connection) {
  return jiraRequest(connection, "/rest/api/3/field")
    .then((fields) => (fields || []).map((field) => ({
      id: field.id,
      key: field.key,
      name: field.name,
      custom: field.custom,
      schema: field.schema || {},
    })));
}
```

- [ ] **Step 4: Add field mapping detection**

Add to `jira.connection.js`:

```js
const { FIELD_MAPPING_NAMES } = require("./jira.resources");

function normalizeFieldName(name = "") {
  return String(name).trim().toLowerCase();
}

function detectFieldMappingsFromFields(fields = []) {
  const candidates = {};
  const fieldMappings = {};

  Object.entries(FIELD_MAPPING_NAMES).forEach(([semanticName, names]) => {
    const matches = fields.filter((field) => {
      const fieldName = normalizeFieldName(field.name);
      return names.some((name) => fieldName.includes(name));
    });
    candidates[semanticName] = matches.map((field) => ({
      id: field.id,
      name: field.name,
      schema: field.schema,
    }));
    if (matches[0]) fieldMappings[semanticName] = matches[0].id;
  });

  return { fieldMappings, candidates };
}

async function detectFieldMappings(connection) {
  const fields = await listFields(connection);
  return detectFieldMappingsFromFields(fields);
}
```

Export the new helpers.

- [ ] **Step 5: Add source actions**

Add to `jira.protocol.js`:

```js
const jiraConnection = require("./jira.connection");

const actions = {
  listProjects({ connection }) {
    return jiraConnection.listProjects(connection);
  },
  listBoards({ connection, params }) {
    return jiraConnection.listBoards(connection, params);
  },
  listFields({ connection }) {
    return jiraConnection.listFields(connection);
  },
  detectFieldMappings({ connection }) {
    return jiraConnection.detectFieldMappings(connection);
  },
};

module.exports = {
  actions,
  assertValidConfiguration,
  getBuilderMetadata,
  getDefaultDataRequest,
  runDataRequest,
  testConnection,
  testUnsavedConnection,
  validateConfiguration,
};
```

- [ ] **Step 6: Add remaining actions**

Implement `listIssueTypes`, `listStatuses`, `listUsers`, `listSprints`, `listVersions`, `validateJql`, and `previewJql` using these Jira Cloud routes:

```txt
GET /rest/api/3/issuetype
GET /rest/api/3/status
GET /rest/api/3/user/search?query=
GET /rest/agile/1.0/board/{boardId}/sprint
GET /rest/api/3/project/{projectIdOrKey}/versions
POST /rest/api/3/jql/parse?validation=strict
GET /rest/api/3/search
```

Normalize every action response to compact arrays of identifiers and labels.

- [ ] **Step 7: Run action tests**

Run:

```bash
npm run test:run -- tests/unit/jiraProtocol.test.js tests/integration/connectionRoute.security.test.js
```

Expected: PASS.

- [ ] **Step 8: Commit connection actions**

```bash
git add server/sources/plugins/jira server/tests/unit/jiraProtocol.test.js server/tests/integration/connectionRoute.security.test.js
git commit -m "feat: add jira source actions"
```

---

## Task 3: Jira Runtime, Variables, Normalization, And Caching

**Files:**
- Modify: `server/sources/plugins/jira/jira.protocol.js`
- Modify: `server/sources/plugins/jira/jira.resources.js`
- Test: `server/tests/unit/jiraProtocol.test.js`

- [ ] **Step 1: Add failing runtime tests**

Add tests for:

```js
validateConfiguration({ source: "jira", resource: "unknown" }).valid === false
applyVariables replaces {{projects}}, {{start_date}}, and {{end_date}} in configuration.jql
normalizeIssue maps fields.status.statusCategory.name to statusCategory
aggregateRows groups by status and sums storyPoints
runDataRequest caches responseData.data
```

- [ ] **Step 2: Run tests to verify failure**

Run:

```bash
npm run test:run -- tests/unit/jiraProtocol.test.js
```

Expected: FAIL on missing runtime helpers.

- [ ] **Step 3: Implement variable application**

Use `processVariablesInString` from `server/sources/shared/variables/stringVariables.js` and recursively process Jira configuration values:

```js
function processConfigValue(value, dataRequest, variables) {
  if (typeof value === "string") {
    return processVariablesInString({ value, dataRequest, variables });
  }
  if (Array.isArray(value)) return value.map((item) => processConfigValue(item, dataRequest, variables));
  if (value && typeof value === "object") {
    return Object.keys(value).reduce((next, key) => ({
      ...next,
      [key]: processConfigValue(value[key], dataRequest, variables),
    }), {});
  }
  return value;
}

function applyVariables({ dataRequest, variables }) {
  const processedConfiguration = processConfigValue(dataRequest.configuration || {}, dataRequest, variables);
  return {
    dataRequest,
    processedDataRequest: {
      ...dataRequest,
      configuration: processedConfiguration,
      Connection: dataRequest.Connection,
      VariableBindings: dataRequest.VariableBindings,
    },
  };
}
```

- [ ] **Step 4: Implement normalization**

Add:

```js
function normalizeIssue(issue, fieldMappings = {}) {
  const fields = issue.fields || {};
  return {
    key: issue.key,
    summary: fields.summary || "",
    projectKey: fields.project?.key || null,
    issueType: fields.issuetype?.name || null,
    status: fields.status?.name || null,
    statusCategory: fields.status?.statusCategory?.name || null,
    assignee: fields.assignee?.displayName || null,
    reporter: fields.reporter?.displayName || null,
    priority: fields.priority?.name || null,
    createdAt: fields.created || null,
    updatedAt: fields.updated || null,
    resolvedAt: fields.resolutiondate || null,
    storyPoints: Number(fields[fieldMappings.storyPoints] || 0),
    severity: fieldMappings.severity ? fields[fieldMappings.severity]?.value || fields[fieldMappings.severity] || null : null,
    team: fieldMappings.team ? fields[fieldMappings.team]?.value || fields[fieldMappings.team] || null : null,
    labels: fields.labels || [],
    fixVersions: (fields.fixVersions || []).map((version) => version.name),
    epicKey: fields.parent?.key || fields.customfield_epic || null,
  };
}
```

- [ ] **Step 5: Implement transforms**

Support transform types:

```txt
raw
grouped
created_resolved_trend
sprint_summary
stale_table
```

The grouped transform should return:

```js
{ [groupBy]: label, issueCount, storyPoints }
```

The trend transform should return:

```js
{ period, created, resolved, open }
```

- [ ] **Step 6: Implement Jira fetch and pagination**

For issue searches, call:

```txt
GET /rest/api/3/search
```

with `jql`, `fields`, `startAt`, and `maxResults`.

For sprint issues, call:

```txt
GET /rest/agile/1.0/sprint/{sprintId}/issue
```

with the same pagination pattern. Stop when Jira returns fewer rows than `maxResults`, when `isLast` is true, or when `maxRecords` is reached.

- [ ] **Step 7: Implement runDataRequest**

Follow the Stripe Official runtime pattern:

```js
if (getCache) {
  const drCache = await checkAndGetCache(connection.id, dataRequest);
  if (drCache) return drCache;
}
const rows = await fetchJiraRows(savedConnection, config);
const transformedRows = transformRows(rows, config, savedConnection.options?.jira?.fieldMappings || {});
const dataToCache = { dataRequest, responseData: { data: transformedRows }, connection_id: savedConnection.id };
await drCacheController.create(dataRequest.id, dataToCache);
return dataToCache;
```

Include connector audit success/failure with `serializeResponsePreview`.

- [ ] **Step 8: Run runtime tests**

Run:

```bash
npm run test:run -- tests/unit/jiraProtocol.test.js
```

Expected: PASS.

- [ ] **Step 9: Commit runtime**

```bash
git add server/sources/plugins/jira server/tests/unit/jiraProtocol.test.js
git commit -m "feat: add jira runtime"
```

---

## Task 4: Jira Templates

**Files:**
- Create: `server/sources/plugins/jira/templates/project-overview.json`
- Create: `server/sources/plugins/jira/templates/sprint-health.json`
- Create: `server/sources/plugins/jira/templates/bug-tracking.json`
- Create: `server/sources/plugins/jira/templates/team-workload.json`
- Create: `server/tests/unit/jiraTemplates.test.js`

- [ ] **Step 1: Write template validation tests**

Use `loadTemplate("jira", slug)` and assert each Jira template loads, includes datasets, includes charts, and every chart references existing datasets.

Add a focused test that a template dataset can include:

```js
variableBindings: [{
  name: "start_date",
  type: "date",
  default_value: "last_30_days",
  required: false,
}]
```

and that `ChartTemplateController.createFromTemplate(...)` passes this through to `DatasetController.createWithDataRequests(...)`.

- [ ] **Step 2: Run tests to verify failure**

Run:

```bash
npm run test:run -- tests/unit/jiraTemplates.test.js
```

Expected: FAIL because template files do not exist.

- [ ] **Step 3: Add project overview template**

Create `project-overview.json` with:

```json
{
  "id": "jira-project-overview",
  "slug": "project-overview",
  "version": 1,
  "source": "jira",
  "name": "Jira Project Overview",
  "description": "Project-level Jira KPIs, issue trends, status breakdowns, and recent completed work.",
  "requiredConnection": { "type": "jira", "subType": "jira" },
  "datasets": [
    {
      "id": "issues_created_resolved",
      "name": "Jira issues created vs resolved",
      "fieldsSchema": {
        "root[].period": "date",
        "root[].created": "number",
        "root[].resolved": "number",
        "root[].open": "number"
      },
      "dataRequest": {
        "configuration": {
          "source": "jira",
          "resource": "issues",
          "mode": "visual",
          "jql": "project IN ({{projects}}) AND created >= {{start_date}} AND created <= {{end_date}} ORDER BY created ASC",
          "transform": { "type": "created_resolved_trend", "interval": "day" },
          "pagination": { "startAt": 0, "maxResults": 100, "maxRecords": 5000 }
        },
        "variableBindings": [
          { "name": "projects", "type": "string", "default_value": "", "required": true },
          { "name": "start_date", "type": "date", "default_value": "last_30_days", "required": false },
          { "name": "end_date", "type": "date", "default_value": "today", "required": false }
        ]
      }
    }
  ],
  "charts": [
    {
      "id": "created-vs-resolved",
      "name": "Issues created vs resolved",
      "type": "line",
      "requiredDatasetIds": ["issues_created_resolved"],
      "layoutIntent": { "kind": "trend", "priority": 10 },
      "cdcs": [
        { "datasetTemplateId": "issues_created_resolved", "xAxis": "root[].period", "yAxis": "root[].created", "legend": "Created" },
        { "datasetTemplateId": "issues_created_resolved", "xAxis": "root[].period", "yAxis": "root[].resolved", "legend": "Resolved" }
      ]
    }
  ]
}
```

Then expand the file with the remaining project overview datasets/charts from the spec.

- [ ] **Step 4: Add remaining templates**

Create the other three templates using the same structure:

- `sprint-health.json`: sprint completion KPI, story points completed KPI, carryover issues table, work by assignee, sprint status breakdown.
- `bug-tracking.json`: open bugs KPI, bugs by priority, oldest open bugs table, bug trend.
- `team-workload.json`: open issues by assignee, WIP by assignee, stale issues table.

- [ ] **Step 5: Run template tests**

Run:

```bash
npm run test:run -- tests/unit/jiraTemplates.test.js
```

Expected: PASS.

- [ ] **Step 6: Commit templates**

```bash
git add server/sources/plugins/jira/templates server/tests/unit/jiraTemplates.test.js
git commit -m "feat: add jira chart templates"
```

---

## Task 5: Frontend Source Registration And Connection Form

**Files:**
- Create: `client/src/sources/jira/jira.source.js`
- Create: `client/src/sources/jira/jira-connection-form.jsx`
- Create: `client/src/sources/jira/assets/jira.svg`
- Modify: `client/src/sources/definitions.js`
- Modify: `client/src/sources/index.js`

- [ ] **Step 1: Add frontend source metadata**

Create `jira.source.js`:

```js
import jiraLogo from "./assets/jira.svg";

const DEFAULT_CONFIGURATION = {
  source: "jira",
  resource: "issues",
  mode: "visual",
  jql: "created >= {{start_date}} AND created <= {{end_date}} ORDER BY updated DESC",
  fields: ["key", "summary", "status", "assignee", "priority", "issuetype", "created", "updated", "resolutiondate", "project"],
  transform: { type: "raw" },
  pagination: { startAt: 0, maxResults: 100, maxRecords: 5000 },
};

const jiraSource = {
  id: "jira",
  type: "jira",
  subType: "jira",
  name: "Jira",
  category: "productivity",
  capabilities: {
    ai: { canGenerateQueries: false, hasSourceInstructions: true, hasTools: true, canGenerateDatasets: true },
    templates: { charts: true, datasets: true },
    nextSteps: { chartTemplates: true, datasetTemplates: true },
  },
  assets: {
    lightLogo: jiraLogo,
    darkLogo: jiraLogo,
  },
  defaults: {
    dataRequest: {
      method: "GET",
      pagination: true,
      items: "issues",
      itemsLimit: 5000,
      offset: "startAt",
      paginationField: null,
      template: "jira",
      useGlobalHeaders: true,
      configuration: DEFAULT_CONFIGURATION,
    },
  },
  templates: {
    chartTemplates: ["project-overview", "sprint-health", "bug-tracking", "team-workload"],
  },
};

export { DEFAULT_CONFIGURATION };
export default jiraSource;
```

- [ ] **Step 2: Register source**

Modify `client/src/sources/definitions.js` to import `jiraSource` and include it in `SOURCE_DEFINITIONS`.

Modify `client/src/sources/index.js` to import Jira frontend components and register:

```js
jira: {
  ConnectionForm: JiraConnectionForm,
  DataRequestBuilder: JiraBuilder,
  ChartTemplateSetup: JiraTemplateSetup,
}
```

- [ ] **Step 3: Implement connection form**

Create `jira-connection-form.jsx` using existing source form patterns. It should collect:

```txt
name
host
authentication.email
authentication.apiToken
```

The submitted connection data should include:

```js
{
  type: "jira",
  subType: "jira",
  host,
  authentication: {
    type: "api_token",
    email,
    apiToken,
  },
}
```

- [ ] **Step 4: Run frontend build check**

Run:

```bash
npm run build
```

Expected: PASS.

- [ ] **Step 5: Commit frontend registration**

```bash
git add client/src/sources/definitions.js client/src/sources/index.js client/src/sources/jira
git commit -m "feat: add jira frontend source"
```

---

## Task 6: Jira Builder UI

**Files:**
- Create: `client/src/sources/jira/jira-builder.jsx`
- Create: `client/src/sources/jira/jira-builder.constants.jsx`
- Create: `client/src/sources/jira/jira-builder.utils.js`
- Create: `client/src/sources/jira/components/jira-builder-context.jsx`
- Create: `client/src/sources/jira/components/jira-resource-step.jsx`
- Create: `client/src/sources/jira/components/jira-config-step.jsx`
- Create: `client/src/sources/jira/components/jira-preview-step.jsx`
- Create: `client/src/sources/jira/components/jira-summary-sidebar.jsx`

- [ ] **Step 1: Mirror Stripe Official builder structure**

Use `client/src/sources/stripeOfficial/stripeOfficial-builder.jsx` as the layout reference. Jira builder should have:

```txt
resource/category step
configuration step
preview step
summary sidebar
advanced/config preview toggle
variable drawer where supported
DataTransform support
```

- [ ] **Step 2: Add constants**

Create resource options:

```js
export const RESOURCE_OPTIONS = [
  { id: "issues", label: "Issues" },
  { id: "sprint_issues", label: "Sprint issues" },
  { id: "boards", label: "Boards" },
  { id: "sprints", label: "Sprints" },
  { id: "versions", label: "Versions" },
];

export const GROUP_BY_OPTIONS = [
  { value: "status", label: "Status" },
  { value: "statusCategory", label: "Status category" },
  { value: "assignee", label: "Assignee" },
  { value: "priority", label: "Priority" },
  { value: "issueType", label: "Issue type" },
  { value: "projectKey", label: "Project" },
];

export const METRIC_OPTIONS = [
  { value: "count", label: "Count issues" },
  { value: "storyPoints", label: "Sum story points" },
  { value: "averageAge", label: "Average age" },
  { value: "leadTime", label: "Lead time" },
];
```

- [ ] **Step 3: Add config utilities**

Implement:

```js
import { DEFAULT_CONFIGURATION } from "./jira.source";

export function mergeConfiguration(dataRequest) {
  return {
    ...DEFAULT_CONFIGURATION,
    ...(dataRequest?.configuration || {}),
    transform: {
      ...DEFAULT_CONFIGURATION.transform,
      ...(dataRequest?.configuration?.transform || {}),
    },
    pagination: {
      ...DEFAULT_CONFIGURATION.pagination,
      ...(dataRequest?.configuration?.pagination || {}),
    },
  };
}

function quoteJqlValue(value) {
  if (value === undefined || value === null || value === "") return null;
  return `"${String(value).replace(/"/g, "\\\"")}"`;
}

export function buildJqlFromVisualConfig(configuration) {
  const clauses = [];
  if (configuration.projects) clauses.push(`project IN (${configuration.projects})`);
  if (configuration.issueType) clauses.push(`issuetype = ${quoteJqlValue(configuration.issueType)}`);
  if (configuration.statusCategory) clauses.push(`statusCategory = ${quoteJqlValue(configuration.statusCategory)}`);
  if (configuration.assignee) clauses.push(`assignee = ${quoteJqlValue(configuration.assignee)}`);
  if (configuration.priority) clauses.push(`priority = ${quoteJqlValue(configuration.priority)}`);
  if (configuration.fixVersion) clauses.push(`fixVersion = ${quoteJqlValue(configuration.fixVersion)}`);
  if (configuration.dateRange?.start) clauses.push(`${configuration.dateRange.field || "created"} >= ${configuration.dateRange.start}`);
  if (configuration.dateRange?.end) clauses.push(`${configuration.dateRange.field || "created"} <= ${configuration.dateRange.end}`);
  return `${clauses.filter(Boolean).join(" AND ") || "ORDER BY updated DESC"}`;
}

export function getPreviewRows(payload) {
  if (Array.isArray(payload?.data)) return payload.data;
  if (Array.isArray(payload?.response?.data)) return payload.response.data;
  if (Array.isArray(payload?.response?.dataRequest?.responseData?.data)) return payload.response.dataRequest.responseData.data;
  return [];
}

export function getPreviewColumns(payload) {
  const firstRow = getPreviewRows(payload)[0];
  return firstRow ? Object.keys(firstRow) : [];
}

export function getSuggestedCharts(configuration) {
  if (configuration.transform?.type === "grouped") {
    return ["bar", "doughnut", "table"];
  }
  if (configuration.transform?.type === "created_resolved_trend") {
    return ["line", "bar"];
  }
  if (configuration.transform?.type === "sprint_summary") {
    return ["kpi", "bar", "table"];
  }
  return ["table"];
}
```

- [ ] **Step 4: Implement source actions in UI**

Use `runSourceAction` for:

```txt
listProjects
listBoards
listSprints
listFields
detectFieldMappings
validateJql
previewJql
```

Cache action results inside builder state to avoid repeated metadata calls.

- [ ] **Step 5: Implement preview and save flow**

Follow the Stripe Official pattern:

```txt
on configuration change -> update DataRequest with template: "jira"
Preview -> save request if needed -> dispatch runDataRequest
Save -> onSave(jiraRequest)
Delete -> onDelete
Transform -> DataTransform save
```

- [ ] **Step 6: Run client build**

Run:

```bash
npm run build
```

Expected: PASS.

- [ ] **Step 7: Commit builder**

```bash
git add client/src/sources/jira
git commit -m "feat: add jira dataset builder"
```

---

## Task 7: Jira Template Setup UI

**Files:**
- Create: `client/src/sources/jira/jira-template-setup.jsx`
- Modify: `client/src/sources/jira/jira-builder.utils.js`

- [ ] **Step 1: Implement bundle picker**

Use Stripe Official template setup as the reference. Jira setup should let users choose:

```txt
Project overview
Sprint health
Bug tracking
Team workload
```

It should require a connection and target dashboard options.

- [ ] **Step 2: Add Jira setup fields**

Before creating templates, collect or infer:

```txt
project keys
board id, when sprint-health is selected
sprint id or active sprint, when sprint-health is selected
story points field mapping, when story-point charts are selected
```

Use source actions to load projects, boards, sprints, and field mappings.

- [ ] **Step 3: Call chart template APIs**

Use the existing built-in chart-template client flow used by Stripe Official. Send:

```js
{
  connection_id,
  dataset_template_ids,
  chart_template_ids,
  dashboard: { type: "existing", project_id }
}
```

or:

```js
{
  connection_id,
  dashboard: { type: "new", name: "Jira Project Overview" }
}
```

- [ ] **Step 4: Run client build**

Run:

```bash
npm run build
```

Expected: PASS.

- [ ] **Step 5: Commit template setup**

```bash
git add client/src/sources/jira/jira-template-setup.jsx client/src/sources/jira/jira-builder.utils.js
git commit -m "feat: add jira template setup"
```

---

## Task 8: Jira Source-Owned AI Planner

**Files:**
- Create: `server/sources/plugins/jira/ai/jira.ai.js`
- Modify: `server/sources/plugins/jira/jira.plugin.js`
- Modify: `server/tests/unit/sourceAiHarness.test.js`

- [ ] **Step 1: Add failing harness tests**

Add Jira cases for:

```txt
open bugs by priority -> ok, issues grouped by priority, bar chart
stale issues -> ok, issues table
completed story points for active sprint -> needs_more_context when board/sprint missing
sprint health dashboard -> recommends sprint-health template
invalid field -> unsupported or error without raw Jira docs
```

Assert Jira does not expose `generateQuery`.

- [ ] **Step 2: Run harness to verify failure**

Run:

```bash
npm run test:run -- tests/unit/sourceAiHarness.test.js
```

Expected: FAIL because Jira AI is not implemented.

- [ ] **Step 3: Implement capabilities and resources**

Create `jira.ai.js`:

```js
const jiraProtocol = require("../jira.protocol");
const { JIRA_RESOURCES } = require("../jira.resources");

const SOURCE_ID = "jira";

const SOURCE_INSTRUCTIONS = [
  "Jira is configuration-based. Return DataRequest.configuration objects; do not invent Jira REST routes.",
  "Use issues for issue search and sprint_issues for sprint issue analytics.",
  "Ask for project, board, sprint, or field mapping context when required and missing.",
  "Do not create burndown or transition-history charts in v1.",
].join("\n");

function getCapabilities() {
  return {
    source: SOURCE_ID,
    instructions: SOURCE_INSTRUCTIONS,
    supports: {
      modes: ["visual", "jql", "advanced"],
      resources: Object.keys(JIRA_RESOURCES),
      variables: true,
      templates: ["project-overview", "sprint-health", "bug-tracking", "team-workload"],
    },
  };
}

function listResources() {
  return {
    source: SOURCE_ID,
    resources: Object.entries(JIRA_RESOURCES).map(([id, resource]) => ({
      id,
      label: resource.label,
    })),
  };
}
```

- [ ] **Step 4: Implement planner**

Add deterministic planning rules for the v1 supported questions:

```js
function planDataset({ question = "", overrides = {} }) {
  const normalized = question.toLowerCase();
  if (/sprint health/.test(normalized)) {
    return recommendTemplates({ question });
  }
  if (/bug/.test(normalized) && /priority/.test(normalized)) {
    return {
      status: "ok",
      dataRequest: {
        template: "jira",
        configuration: {
          source: "jira",
          resource: "issues",
          mode: "visual",
          jql: "project IN ({{projects}}) AND issuetype = Bug AND created >= {{start_date}} AND created <= {{end_date}}",
          transform: { type: "grouped", metric: "count", groupBy: "priority" },
          pagination: { startAt: 0, maxResults: 100, maxRecords: 5000 },
        },
      },
      chartSpec: { type: "bar", xAxis: "root[].priority", yAxis: "root[].issueCount" },
    };
  }
  return {
    status: "needs_more_context",
    message: "Choose a Jira project or template goal first.",
    requiredContext: ["projects"],
  };
}
```

Expand rules for stale issues, sprint story points, and template recommendations.

- [ ] **Step 5: Implement validation and preview wrappers**

Use:

```js
function validateConfiguration(configuration) {
  return jiraProtocol.validateConfiguration(configuration);
}

function previewConfiguration({ connection, configuration, rowLimit = 10 }) {
  return jiraProtocol.previewDataRequest({
    connection,
    dataRequest: {
      ...jiraProtocol.getDefaultDataRequest(),
      configuration: {
        ...configuration,
        pagination: {
          ...(configuration.pagination || {}),
          maxRecords: rowLimit,
        },
      },
    },
    itemsLimit: rowLimit,
  });
}
```

- [ ] **Step 6: Wire AI module**

Modify `jira.plugin.js`:

```js
const jiraAi = require("./ai/jira.ai");

backend: {
  ...jiraProtocol,
  ai: jiraAi,
},
```

- [ ] **Step 7: Run harness**

Run:

```bash
npm run test:run -- tests/unit/sourceAiHarness.test.js
```

Expected: PASS.

- [ ] **Step 8: Commit Jira AI**

```bash
git add server/sources/plugins/jira/ai server/sources/plugins/jira/jira.plugin.js server/tests/unit/sourceAiHarness.test.js
git commit -m "feat: add jira source ai planner"
```

---

## Task 9: Generic AI Dashboard Template Tool

**Files:**
- Create: `server/modules/ai/orchestrator/tools/createDashboardFromTemplate.js`
- Modify: `server/modules/ai/orchestrator/tools/index.js`
- Modify: `server/modules/ai/orchestrator/orchestrator.js`
- Modify: `server/tests/unit/sourceAiHarness.test.js`

- [ ] **Step 1: Add failing tool tests**

Add tests that the tool:

```txt
accepts source, template_slug, connection_id, and dashboard options
rejects unsupported source/template combinations
calls ChartTemplateController.createFromTemplate
is generic and not Jira-specific
```

- [ ] **Step 2: Run tests to verify failure**

Run:

```bash
npm run test:run -- tests/unit/sourceAiHarness.test.js
```

Expected: FAIL because the tool is missing.

- [ ] **Step 3: Implement tool**

Create `createDashboardFromTemplate.js`:

```js
const ChartTemplateController = require("../../../controllers/ChartTemplateController");

async function createDashboardFromTemplate(payload) {
  const {
    team_id: teamId,
    source,
    template_slug: templateSlug,
    connection_id: connectionId,
    dashboard,
    dataset_template_ids: datasetTemplateIds,
    chart_template_ids: chartTemplateIds,
    user,
  } = payload;

  if (!teamId) throw new Error("team_id is required");
  if (!source) throw new Error("source is required");
  if (!templateSlug) throw new Error("template_slug is required");
  if (!connectionId) throw new Error("connection_id is required");

  const controller = new ChartTemplateController();
  return controller.createFromTemplate(teamId, source, templateSlug, {
    connection_id: connectionId,
    dashboard,
    dataset_template_ids: datasetTemplateIds,
    chart_template_ids: chartTemplateIds,
  }, user);
}

module.exports = createDashboardFromTemplate;
```

- [ ] **Step 4: Register tool**

Export it from `tools/index.js`, add a tool schema to `orchestrator.js`, and dispatch it in the tool switch. The schema should be source-agnostic:

```js
{
  name: "create_dashboard_from_template",
  displayName: "Create dashboard from template",
  description: "Create a dashboard/project from a registered source chart template.",
  parameters: {
    type: "object",
    properties: {
      source: { type: "string" },
      template_slug: { type: "string" },
      connection_id: { type: "string" },
      dashboard: { type: "object" },
      dataset_template_ids: { type: "array", items: { type: "string" } },
      chart_template_ids: { type: "array", items: { type: "string" } },
    },
    required: ["source", "template_slug", "connection_id", "dashboard"],
  },
}
```

- [ ] **Step 5: Run harness**

Run:

```bash
npm run test:run -- tests/unit/sourceAiHarness.test.js
```

Expected: PASS.

- [ ] **Step 6: Commit generic dashboard-template tool**

```bash
git add server/modules/ai/orchestrator server/tests/unit/sourceAiHarness.test.js
git commit -m "feat: add ai dashboard template tool"
```

---

## Task 10: Full Verification

**Files:**
- All files from prior tasks.

- [ ] **Step 1: Run focused backend tests**

Run:

```bash
npm run test:run -- tests/unit/sourceRegistry.test.js tests/unit/sourcePluginStructure.test.js tests/unit/jiraProtocol.test.js tests/unit/jiraTemplates.test.js tests/unit/sourceAiHarness.test.js tests/integration/connectionRoute.security.test.js
```

Expected: PASS.

- [ ] **Step 2: Run server build/test command available for this repo**

Run:

```bash
npm run test:run -- --passWithNoTests
```

Expected: PASS.

- [ ] **Step 3: Run client build**

Run:

```bash
npm run build
```

Expected: PASS.

- [ ] **Step 4: Manual smoke check**

Start the app with the repo's normal dev command and verify:

```txt
Jira appears in the connection picker.
Jira connection form saves custom type/subType.
Jira builder opens for Jira datasets.
Visual/JQL/Advanced modes update DataRequest.configuration.
Template setup lists four Jira bundles.
AI source list includes Jira as source-owned configuration source.
```

- [ ] **Step 5: Commit verification fixes**

If verification required fixes:

```bash
git add server/sources/plugins/jira client/src/sources/jira server/tests/unit/jiraProtocol.test.js server/tests/unit/jiraTemplates.test.js server/tests/unit/sourceAiHarness.test.js
git commit -m "fix: verify jira source integration"
```

If no fixes were needed, do not create an empty commit.

---

## Self-Review

Spec coverage:

- Jira custom identity is covered in Task 1 and Task 5.
- Jira Cloud API-token auth is covered in Task 2 and Task 5.
- Custom runtime, normalization, pagination, variables, and caching are covered in Task 3.
- Separate templates are covered in Task 4.
- Stripe Official-style frontend builder and setup UI are covered in Tasks 6 and 7.
- Source-owned AI parity is covered in Task 8.
- Generic dashboard-template AI side quest is covered in Task 9.
- Verification is covered in Task 10.

Known implementation risk:

- The template variable binding test in Task 4 may reveal that `ChartTemplateController.createFromTemplate(...)` does not preserve `dataRequest.variableBindings`. If so, implement the smallest controller pass-through fix in Task 4 before creating Jira templates that depend on variables.