npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cesar-richard/git-connector-sdk

v2.6.0

Published

TypeScript SDK for the git-connector v1 API (work items + iterations aggregated from GitHub/GitLab). Version published on npm tracks server releases.

Readme

@cesar-richard/git-connector-sdk

npm version license

TypeScript SDK for git-connector — a server that aggregates GitHub + GitLab activity (PRs, MRs, issues, commits, comments, reviews, CI runs) and exposes a unified, ticket-correlated read API.

This SDK is a thin, fully-typed wrapper around the server's /v1/* HTTP API, generated from its OpenAPI spec via openapi-fetch.

Version policy: the SDK version always tracks the git-connector server release it was generated against. Pinning the same major+minor as your server is recommended.


Install

npm install @cesar-richard/git-connector-sdk
# or
pnpm add @cesar-richard/git-connector-sdk
# or
bun add @cesar-richard/git-connector-sdk

Quick start

import { createGitConnectorClient } from "@cesar-richard/git-connector-sdk";

const client = createGitConnectorClient({
  baseUrl: "https://git-connector.example.com",
  token: process.env.GIT_CONNECTOR_TOKEN!, // gck_…
});

const { data, error } = await client.GET("/v1/work-items", {
  params: { query: { state: "open", iteration: "current" } },
});

if (error) throw new Error(`git-connector error: ${JSON.stringify(error)}`);

for (const item of data.items) {
  console.log(`${item.source}/${item.projectKey}#${item.number} — ${item.title}`);
  for (const pr of item.linkedActivities) {
    console.log(`  ↳ ${pr.type.toUpperCase()} ${pr.url} (${pr.state})`);
  }
}

The client is fully typed: request params, query strings, and response bodies are all derived from the server's OpenAPI schema. Hover any field in your editor to see its shape.

Authentication

The SDK sends Authorization: Bearer <token> on every request. Mint a token from the git-connector control UI or via:

curl -X POST https://git-connector.example.com/admin/api-keys \
     -H "Content-Type: application/json" \
     -d '{"name":"my-integration"}'
# → { "token": "gck_…", "id": 12, "name": "my-integration", "createdAt": "…" }

Cookie auth (same-origin web UIs)

When you're calling from the same origin as the git-connector deployment (e.g. its own SPA), you can omit token entirely — the server accepts the gck_session httpOnly cookie minted by the OIDC /auth/callback flow:

const client = createGitConnectorClient({
  baseUrl: window.location.origin,
  // Custom fetch so the browser attaches the gck_session cookie.
  fetch: (input, init) =>
    globalThis.fetch(input, { ...init, credentials: "include" }),
});

External SDK consumers (cross-origin scripts, CI integrations) should keep passing a Bearer token.

Tokens with the gck_ prefix are immutable once minted; revoke and reissue via the same endpoint.


Activity tracking / CRA workflow

The /v1/* surface is designed to power activity reports (CRA, billable-time sheets, "what did I do this month"). The primitives below let you slice your data per-user, per-day, per-type.

What did I do today?

const today = new Date().toISOString().slice(0, 10);
const { data } = await client.GET("/v1/events", {
  params: { query: { from: today, to: today, user: "cesar" } },
});
for (const e of data.events) {
  console.log(`[${e.day}] ${e.type} — ${e.title} (${e.repo})`);
}

List my open PRs / MRs

const { data } = await client.GET("/v1/activities", {
  params: {
    query: { author: "cesar", state: "open,draft", type: "pr,mr" },
  },
});
for (const a of data.items) {
  console.log(`${a.source}/${a.repo}#${a.number} — ${a.title} (${a.state})`);
}

List issues I opened

const { data } = await client.GET("/v1/work-items", {
  params: { query: { author: "cesar", state: "open" } },
});

Incremental polling (every 5 minutes)

Use ?since=<ISO> to fetch only events newer than your last cursor:

let cursor = "2026-05-18T00:00:00Z";
const { data } = await client.GET("/v1/events", {
  params: {
    query: { from: "2026-05-01", to: "2026-05-31", since: cursor },
  },
});
if (data.events.length > 0) {
  cursor = data.events[data.events.length - 1].occurredAt;
}

Monthly CRA: count events per day

const { data } = await client.GET("/v1/events", {
  params: {
    query: {
      from: "2026-05-01",
      to: "2026-05-31",
      user: "cesar",
      type: "commit,review,comment,state-transition",
    },
  },
});
const byDay: Record<string, number> = {};
for (const e of data.events) {
  byDay[e.day] = (byDay[e.day] ?? 0) + 1;
}

Reliability of the author field

author (on Activity and Event) is the GitHub or GitLab account login (e.g. cesar, never César Richard). If the underlying provider payload doesn't expose a login — rare cases like a git commit by an email not linked to any account — author is null. The git-config commit-author name is never used for author; it lives in meta.commitAuthorName when available, for display purposes only.

Filters like ?author=cesar (on /v1/activities and /v1/work-items) and ?user=cesar (on /v1/events) match the login exactly (case-insensitive). They do NOT fuzzy-match display names. This is a deliberate design choice for auditable activity tracking.

GitLab resolver: commits ingested via polling are resolved server-side by mapping commit.author_email → GL user login via the GitLab Users API. The result is cached (default TTL 90 days). If the email cannot be resolved (anonymous commit, email not associated with any GL user, private email on a self-hosted instance), author stays null and the git-config name lives in meta.commitAuthorName. Webhook-ingested GL commits use the pusher's username directly (no resolver needed).

Structured author access via authorResolved: commit events also carry an authorResolved blob with {login, name, email} (each nullable) — a single typed accessor that aggregates everything we know about the author:

for (const e of data.events) {
  if (e.type !== "commit") continue;
  const { login, name, email } = e.authorResolved ?? {};
  console.log(`${login ?? "anonymous"} (${name ?? "?"} <${email ?? "?"}>)`);
}

Available on every commit event regardless of provider or ingestion path. event.author is always equal to event.authorResolved?.login.

User resolution on work-items (assigneesResolved, authorResolved, reviewerResolved)

Beyond commit events, /v1/work-items exposes resolved user identities on work-item assignees, linked-activity authors, and reviewers. Each is a ResolvedUser shape:

type ResolvedUser = {
  id: string;     // "gl:<user_id>" or "gh:<login>" — stable across login renames
  login: string;
  name: string | null;
  email: string | null;
};

The id is stable: two references to the same GitLab user retain the same gl:<user_id> even after the user's username changes (a common pattern in self-hosted GitLab where matricule-based logins get rewritten to friendly names later). GitHub usernames don't typically change in a way that affects this, so id is gh:<login> for GitHub users.

const { data } = await client.GET("/v1/work-items");
for (const wi of data.items) {
  for (const a of wi.assigneesResolved ?? []) {
    console.log(`${wi.title} assigned to ${a.name ?? a.login} (id=${a.id})`);
  }
  for (const pr of wi.linkedActivities) {
    if (pr.authorResolved) {
      console.log(`PR ${pr.url} by ${pr.authorResolved.name ?? pr.authorResolved.login}`);
    }
    for (const rev of pr.reviews.entries) {
      console.log(`  review by ${rev.reviewerResolved?.name ?? rev.reviewer}`);
    }
  }
}

Link traceability (linkedActivities[].linkSource)

Each entry in linkedActivities carries an optional linkSource object with the raw hintSource persisted on the link row and a human-readable kind derived from it (body-ref, title-ref, pr-comment-ref, issue-comment-ref, unknown). Consumers can use this to surface WHY a link was created — useful for debugging missing or unexpected links in the Explorer UI.

for (const pr of wi.linkedActivities) {
  if (pr.linkSource) {
    console.log(`  ↳ ${pr.url} linked via ${pr.linkSource.kind} (${pr.linkSource.hintSource})`);
  }
}

GitLab resolution uses a 90-day cache (configurable via GITLAB_USER_CACHE_TTL_DAYS).

Issue ↔ PR/MR link detection patterns

WorkItem.linkedActivities is populated by scanning PR/MR bodies, commit messages, notes, AND issue comments for references to PR/MR numbers or URLs. The recognized patterns are:

Canonical (in PR/MR body / commits / notes — forward direction):

  • Fixes #1234, Fix #1234, Closes #1234, Close #1234, Resolves #1234, Resolve #1234 (English short refs).
  • https://github.com/<owner>/<repo>/issues/<n> or https://<gitlab>/.../-/issues/<n> (full URLs).
  • <owner>/<repo>#<n> (cross-repo GitHub).
  • <alias>#<n> and <alias>!<n> (project-alias cross-repo).
  • Short refs #<n> and !<n> (resolved against the active provider/project).

Issue-comment reverse direction (hint_source="issue_comment"):

The above PR/MR URLs and short refs are all recognized in issue comments as evidence that the cited PR/MR is linked to the issue.

Additionally, the following explicit phrase prefixes are recognized, case-insensitively, to catch informal "this issue was delivered" comments:

| Phrase | Example | Behavior | |---|---|---| | Fixed by …, Fixed in … | Fixed by 1214, Fixed by https://…/pull/1214 | Treat the cited ref as a linked PR/MR | | Fixes by …, Fix by … | Fix by #99 | Same | | Resolved by …, Resolves by …, Resolve by … | Resolved by 42 | Same | | Closed by …, Closes by …, Close by … | Closed by !99 | Same | | FR: Corrigé par …, Corrigée par …, Corrige par … | Corrigé par 42 | Same | | FR: Résolu par …, Resolu par … | Résolu par !99 | Same | | FR: Fermé par …, Ferme par … | Fermé par #1214 | Same | | FR: … dans <n> variant | Corrigé dans 42 | Same |

The phrase prefix lets the SDK detect refs even when the number is bare (no # or ! prefix), e.g. Fixed by 1214 instead of Fixed by #1214.

Code blocks (fenced ``` and inline `) are stripped before scanning.

Correlation confidence & billing flags (attributable, resolves)

Every linkedActivities[] entry carries billing-grade correlation metadata so a client app can decide, auditably, what to impute and how to display issue status. All fields are optional (only present once an entity has been (re)synced under scoring algo corr-2026.06).

| field | type | meaning | |---|---|---| | linkKind | "reference" \| "resolution" \| "reverse_comment" | how the link reads: a mention, an explicit closure, or a link discovered from the issue's own comments | | confidence | number \| null | [0,1]. Driven by the form of the evidence: canonical URL 0.95 > cross-ref (group/repo#N) 0.85 > bare short ref 0.78; −0.13 in a title, +0.07 for a resolution verb; ambiguous heuristic-resolved refs capped at 0.60 | | status | "confirmed" \| "candidate" | confirmed when confidence ≥ 0.75 and unambiguous (or confirmed via bidirectional evidence) | | attributable | boolean | the flag to use for imputation / billing. truestatus==='confirmed', any linkKind. A PR that merely references an issue (preparatory work) — or is only linked from the issue's comments — still counts as contribution | | resolves | boolean | trueconfirmed && linkKind==='resolution'. Use for issue status, never for billing | | evidence | { field, text, matchStart, matchEnd } \| null | the matched snippet + offsets, for human audit |

Migration note (was billable): the short-lived billable field (SDK 1.57.0) was removed. It conflated two questions. Replace it as follows:

  • imputation / "who worked on this issue" → use attributable (NOT the old billable, which only fired on closures and missed preparatory work and reverse-linked PRs);
  • "what closed the issue" / status → use resolves (this is what the old billable actually computed).
const { data } = await client.GET("/v1/work-items", {
  params: { query: { state: "open" } },
});
for (const wi of data.items) {
  // Everyone who contributed to this issue (for a CRA / billing sheet):
  const contributors = new Set(
    wi.linkedActivities
      .filter((pr) => pr.attributable)
      .map((pr) => pr.authorResolved?.login ?? pr.author)
      .filter(Boolean),
  );

  // Is the issue actually resolved by a merged PR?
  const resolved = wi.linkedActivities.some((pr) => pr.resolves && pr.isMerged);

  console.log(`${wi.title}: worked on by [${[...contributors].join(", ")}]`, { resolved });
}

Two PRs in different repos (e.g. a GitHub PR for terraform groundwork + a GitLab MR for the feature) that both reference the same issue both show up as attributable linked activities on that one work item — that's how cross-repo, cross-provider work is consolidated: the issue is the pivot.

PR review status (reviewStatus)

Each LinkedActivity carries an optional reviewStatus field with 8 possible values, computed server-side from the PR's state, draft flag, requested reviewers, and review history:

| Value | Meaning | |---|---| | draft | PR is a draft — not yet ready for review | | open-no-review | Open, no reviewer requested | | open-awaiting-my-review | Open, you (the viewer) are a requested reviewer | | open-awaiting-other-review | Open, waiting on someone else's review | | open-changes-requested | Open, at least one reviewer requested changes (and that verdict hasn't been superseded by an approval) | | open-approved | Open and approved, no outstanding change requests | | merged | Merged | | closed | Closed without merging |

Pass ?viewer=<login> (case-insensitive) on GET /v1/work-items or GET /v1/work-items/{source}/{projectKey}/{number} to drive the open-awaiting-my-review vs open-awaiting-other-review split. Without viewer, awaiting-review PRs always show as open-awaiting-other-review.

const { data } = await client.GET("/v1/work-items", {
  params: { query: { state: "open", viewer: "cesar" } },
});
for (const wi of data.items) {
  for (const pr of wi.linkedActivities) {
    if (pr.reviewStatus === "open-awaiting-my-review") {
      console.log(`Needs your review: ${pr.url}`);
    }
  }
}

API surface

The /v1/* API exposes these resources: work items, iterations, work-item details, the unified events stream, raw activities, deliverables, and per-project delivery rules.

GET /v1/work-items — list aggregated work items

Issues from GitHub + GitLab, with linked PRs/MRs resolved server-side via ticket-reference correlation. Filter by state, iteration (id or "current"), assignee, label, source.

const { data } = await client.GET("/v1/work-items", {
  params: {
    query: {
      state: "open",
      iteration: "current",
      assignee: "alice",
    },
  },
});
// data: { items: WorkItem[]; total: number; limit: number }

Each WorkItem includes its linkedActivities (PRs/MRs with full enrichment: reviews summary, unresolved threads, volume, status history, reviewRequestedAt, mergedAt).

GET /v1/work-items/{source}/{projectKey}/{number} — detail

const { data } = await client.GET(
  "/v1/work-items/{source}/{projectKey}/{number}",
  { params: { path: { source: "gitlab", projectKey: "acme/ops", number: 42 } } },
);

GET /v1/iterations — list iterations

const { data } = await client.GET("/v1/iterations");
// data: IterationWithCount[] — each carries workItemCount across all linked WorkItems

GET /v1/events — unified activity stream

Daily-bucketed (Paris-TZ) chronological event stream. Mixes commits, comments, reviews, state-transitions, and CI runs across both providers. Cursor-paginated.

const { data } = await client.GET("/v1/events", {
  params: {
    query: {
      from: "2026-05-01",
      to: "2026-05-17",
      type: "ci-run,review", // CSV
      user: "alice",
      limit: "200",
    },
  },
});
// data: { events: GitEvent[]; total: number; nextCursor: string | null }

// Paginate:
if (data.nextCursor) {
  const { data: next } = await client.GET("/v1/events", {
    params: { query: { from: "2026-05-01", to: "2026-05-17", cursor: data.nextCursor } },
  });
}

Event types in the stream:

| type | Source | parentActivityId | Notable meta keys | |--------|--------|--------------------|---------------------| | commit | Default branch | null | fullMessage | | comment | Issue/PR/MR notes | resolves to PR/MR/issue | body | | review | GH PR review / GL MR approval | resolves to PR/MR | state, reviewer | | state-transition | Derived from activity state diff | resolves to PR/MR/issue | from, to | | ci-run | GH Actions job / GL pipeline job | resolves to PR/MR via head SHA | runId, jobName, conclusion, durationSec, sha, branch |

GET /v1/deliverables — classified deliverables of a window

Issues classified as delivered, in_progress, or not_delivered over a time window, driven by a per-project label convention (see delivery-rules below). not_delivered issues are always dropped from the response.

Query params:

| param | required | meaning | |-------|----------|---------| | from | yes | Window start. ISO date (YYYY-MM-DD) or datetime. | | to | yes | Window end. ISO date (inclusive end-of-day) or datetime. | | projectKey | no | CSV of project keys to restrict to. | | author | no | CSV of author login aliases (case-insensitive). When set, only issues attributable to one of the aliases are returned and attribution is populated. When absent, every window deliverable is returned with attribution: null. | | source | no | github or gitlab. |

Each DeliverableIssue carries deliveryState, deliveredAt, deliveryReason (a typed { kind, ruleLabel, transitionAt, transitionBy } explaining WHY the classification fired — read deliveryReason.ruleLabel to get the scoped label that triggered it), attribution ({ isAssignee, hasOwnCommit, isMergedPRAuthor, result } with result precedence assigned > committer > pr_author > none), linkedActivities, and an evidence blob (deliveryTransitions, consideredCommits, consideredPRs) for human audit.

// Monthly CRA: who delivered what this month, attributed to me.
const { data } = await client.GET("/v1/deliverables", {
  params: {
    query: {
      from: "2026-05-01",
      to: "2026-05-31",
      projectKey: "id/cohort360/gestion-de-projet",
      author: "cesar-richard,crichard",
    },
  },
});
// data.issues: DeliverableIssue[] — deliveryState, deliveredAt, attribution, evidence
for (const issue of data.issues) {
  console.log(
    `${issue.projectKey}#${issue.number} — ${issue.deliveryState} ` +
      `(${issue.attribution?.result ?? "n/a"}) @ ${issue.deliveredAt ?? "?"}`,
  );
}

When author is omitted you get the full set of window deliverables with attribution: null — useful for a team-wide "what shipped this month" view.

GET / PATCH /v1/projects/{source}/{projectKey}/delivery-rules — label convention

The classification above is configurable per project. deliveryLabels are the scoped labels whose "added" transition marks an issue delivered (e.g. the GitLab scoped label =::Pushed en dev); openedLabels mark in_progress. When no rule row exists (or deliveryLabels is empty) the endpoint falls back to a native closed-state heuristic.

// Read the current convention (empty defaults when unset).
const { data: rules } = await client.GET(
  "/v1/projects/{source}/{projectKey}/delivery-rules",
  { params: { path: { source: "gitlab", projectKey: "acme/ops" } } },
);
// { source, projectKey, deliveryLabels, openedLabels, updatedAt }

// PATCH is partial — omitted keys keep their stored value.
await client.PATCH("/v1/projects/{source}/{projectKey}/delivery-rules", {
  params: { path: { source: "gitlab", projectKey: "acme/ops" } },
  body: { deliveryLabels: ["=::Pushed en dev"], openedLabels: ["=::En cours"] },
});

projectKey segments containing / must be URL-encoded (e.g. acme%2Fops).


Schema as a separate import

If you need the raw OpenAPI types without the client (e.g. for codegen or for non-openapi-fetch consumers), import directly:

import type { components } from "@cesar-richard/git-connector-sdk/schema";

type WorkItem = components["schemas"]["WorkItem"];
type GitEvent = components["schemas"]["GitEvent"];

The full OpenAPI document is also published in the package — see openapi.json for the contract.

Error handling

openapi-fetch never throws on HTTP-level errors — it returns { data, error, response }. Always check error before using data:

const { data, error, response } = await client.GET("/v1/work-items");
if (error) {
  if (response.status === 401) throw new Error("Missing/invalid gck_ token");
  if (response.status === 403) throw new Error("Token revoked");
  throw new Error(`git-connector ${response.status}: ${JSON.stringify(error)}`);
}
// data is non-null and fully typed from here

Network errors (DNS, timeout, connection refused) reject the promise as usual.

Custom fetch

Pass your own fetch implementation to wire up retries, telemetry, or a non-global fetch (e.g. inside Cloudflare Workers):

import { fetch as undiciFetch } from "undici";

const client = createGitConnectorClient({
  baseUrl: "https://git-connector.example.com",
  token: process.env.GIT_CONNECTOR_TOKEN!,
  fetch: undiciFetch,
});

Listing the labels of a project

GET /v1/projects/{source}/{projectKey}/labels returns the live label set of a GitLab project or GitHub repository, including scope computed server-side for GitLab scoped labels (<scope>::<value>). Cached in-memory for 15 minutes (configurable via LABELS_CACHE_TTL_MS).

const { data, error } = await client.GET(
  "/v1/projects/{source}/{projectKey}/labels",
  { params: { path: { source: "gitlab", projectKey: "group/sub/project" } } },
);
if (error) throw error;
for (const label of data.labels) {
  console.log(label.scope ?? "(no scope)", label.name, label.color);
}

The endpoint returns 404 when the project does not exist on the upstream provider, 403 when the configured provider token lacks access, and 502 on upstream unreachable (e.g. expired GitLab NSC cookie).

Compatibility

  • Node ≥ 18 (uses global fetch).
  • Bun ≥ 1.0.
  • Modern browsers (no Node-only APIs).
  • ESM only.

User identities

Group multiple (provider, login) aliases under one canonical identity. Filter any ?author-supporting endpoint by identity instead of login.

// List identities (paginated, with optional search)
const { data } = await client.GET("/v1/identities", {
  params: { query: { limit: 50, q: "thomas" } },
});

// Create one with initial aliases
const { data: created } = await client.POST("/v1/identities", {
  body: {
    displayName: "Thomas",
    aliases: [
      { provider: "github", login: "tfi90" },
      { provider: "gitlab", login: "70666666" },
    ],
  },
});

// Attach an alias later — moves it from another identity if needed
await client.POST("/v1/identities/{id}/aliases", {
  params: { path: { id: created.id } },
  body: { provider: "github", login: "anotherlogin" },
});

// Filter work items / events / activities / deliverables by identity
const { data: workItems } = await client.GET("/v1/work-items", {
  params: { query: { identity: String(created.id), withIdentity: "1" } },
});
// Each work item now carries `authorIdentity: { id, displayName } | null`.

?identity and ?author are mutually exclusive: passing both returns 400. ?withIdentity=1 opts into the authorIdentity field on each item.

Auto-discovery + enrichment (server-side)

The server runs a nightly cron (3 AM UTC) + a post-sync event hook that:

  1. Scans git_events + git_activities for any (provider, login) not yet attached to an identity → creates one identity per login with display_name = login (fallback) and display_name_source = "auto".
  2. Calls the provider's user-API (GitLab users?username=..., GitHub users.getByUsername) to enrich display_name, email, avatar_url.
  3. Caches both positive AND negative results (enriched_at ISO timestamp, 7-day TTL).
  4. Never touches identities edited manually (display_name_source = "manual").

Identities created via POST /v1/identities or whose displayName was set via PATCH keep display_name_source = "manual" automatically.

Rendering deliverable narratives

/v1/deliverables now ships every field needed to render a rich per-topic summary without a second API call. Each DeliverableIssue exposes:

  • The full topic context: body, state, assignees + assigneesResolved, iteration, milestone, labels, statusHistory, createdAt, updatedAt.
  • A typed deliveryReason: { kind, ruleLabel, transitionAt, transitionBy } digest explaining WHY the classification fired. kind is one of rule_label_added, closed_in_window, in_progress_opened_label_active, in_progress_delivered_after_window, in_progress_fallback_open. Use deliveryReason.ruleLabel to display the scoped label that triggered the state (replaces the legacy deliveryLabel field).
  • A typed array of linked PR/MRs with stable cross-endpoint ids and the names of requested reviewers:
LinkedActivity {
  id: string;                                // "gitlab:org/proj:mr:42"
  source: "github" | "gitlab";
  externalId: string;                        // "42"
  number: number;                            // 42
  type: "pr" | "mr";
  title: string;
  url: string;
  state: "open" | "closed" | "merged" | "draft" | "unknown";
  isMerged: boolean;
  isDraft: boolean;
  awaitingReview: boolean;
  requestedReviewers: string[];              // logins
  requestedReviewersResolved?: ResolvedUser[]; // with name/email
  // ...mergedAt, reviews, volume, etc.
}

Deduplicating activities

LinkedActivity.id is stable across endpoints — the same id format is used by Activity.id from /v1/activities. Dedupe by id, never by URL:

const linkedIds = new Set(
  deliverables.issues.flatMap(i => i.linkedActivities.map(la => la.id)),
);
const orphans = rawActivities.filter(a => !linkedIds.has(a.id));

Rendering a topic narrative

function renderLinkedActivity(la: LinkedActivity): string {
  const num = `#${la.number}`;
  if (la.isMerged) {
    const when = la.mergedAt
      ? `mergée le ${new Date(la.mergedAt).toLocaleDateString()}`
      : "mergée";
    return `${num} '${la.title}' ${when}`;
  }
  if (la.isDraft) return `${num} '${la.title}' en draft`;
  if (la.awaitingReview && la.requestedReviewers.length > 0) {
    const named = (la.requestedReviewersResolved ?? []).map(
      r => r.name ?? r.login,
    );
    const unnamed = la.requestedReviewers.filter(
      l => !(la.requestedReviewersResolved ?? []).some(r => r.login === l),
    );
    const all = [...named, ...unnamed].slice(0, 3).join(", ");
    return `${num} '${la.title}' en attente de review de ${all}`;
  }
  return `${num} '${la.title}' ${la.state}`;
}

function renderTopic(issue: DeliverableIssue): string {
  const num = `#${issue.number}`;
  const owner =
    issue.assigneesResolved?.[0]?.name ?? issue.assignees[0] ?? "personne";
  const iter = issue.iteration?.title ?? "hors itération";
  const linked = issue.linkedActivities;
  const merged = linked.filter(la => la.isMerged).length;
  const open = linked.filter(la => la.state === "open").length;
  // Use deliveryReason.ruleLabel + kind to phrase the classification.
  const reason = issue.deliveryReason.ruleLabel
    ? ` (via label '${issue.deliveryReason.ruleLabel}')`
    : issue.deliveryReason.kind === "closed_in_window"
      ? " (fermée dans la fenêtre)"
      : "";
  const head =
    `Le sujet '${issue.title}' (${num}), assigné à ${owner}, ` +
    `dans ${iter}, est ${issue.deliveryState}${reason} ` +
    `avec ${linked.length} PR liées (${merged} mergées, ${open} en cours)`;
  const details = linked.map(renderLinkedActivity).join("\n  - ");
  return `${head}:\n  - ${details}`;
}

Sample output:

Le sujet 'Refactor auth' (#42), assigné à Bob Smith, dans Sprint-25, est in_progress (via label '=::En cours') avec 3 PR liées (2 mergées, 1 en cours):
  - #18 'Initial refactor' mergée le 2026-05-15
  - #21 'Tests' mergée le 2026-05-20
  - #24 'Polish' en attente de review de Alice Doe, Carol Roe

Deduplicating orphans (CRA "non rattachés")

Each Activity and LinkedActivity exposes linkedIssueRefs: IssueRef[] — the issues this PR/MR is linked to in either direction:

  • forward: the PR/MR mentions the issue (body/notes URL/short-ref/header).
  • reverse: an issue's body/notes mentions this PR/MR.

The field is window-independent (derived from git_activity_links), unlike linkedActivities[].id which is scoped to the issues a given query returns.

Use it to dedupe a CRA "orphans" section:

const acts = await client.GET("/v1/activities", {
  params: { query: { updatedSince: monthStart, updatedBefore: monthEnd } },
});
const orphans = acts.data.items.filter(
  (a) => (a.type === "pr" || a.type === "mr") && a.linkedIssueRefs.length === 0,
);
// A PR/MR linked to ANY issue (even one outside the queried window) is NOT
// an orphan. The dashboard can choose to render it under its linked issue
// when the issue is a deliverable of the month, or omit it otherwise.

Related

  • Server / control UI: git-connector
  • OpenAPI spec: ships with this package as openapi.json — generated server-side, the SDK types derive from it via openapi-typescript.
  • Releases: SDK and server release in lockstep via semantic-release on every merge to main.

License

MIT