diff --git a/apps/web/src/components/work-os/work-unit-detail.tsx b/apps/web/src/components/work-os/work-unit-detail.tsx
index 671249a..d4707f1 100644
--- a/apps/web/src/components/work-os/work-unit-detail.tsx
+++ b/apps/web/src/components/work-os/work-unit-detail.tsx
@@ -10,6 +10,7 @@ import {
TriangleAlert,
} from "lucide-react";
+import { formatRelativeTime } from "@/lib/work-os/work-unit-projection";
import type { WorkUnitDetail as WorkUnitDetailData } from "@/lib/work-os/work-unit-projection";
const formatStatus = (
@@ -113,15 +114,32 @@ export const WorkUnitDetail = ({
) : null}
- {/* Signals — no signal-to-issue relation exists yet. Show explicit
- unavailable state rather than falsely presenting project Signals. */}
- } title="Signals">
-
- No Signals linked to this Work Unit yet.
-
-
- Linked Signals will appear here once attachment is available.
-
+ {/* Linked Signals — authenticated via signalIssueAttachments */}
+ }
+ title={`Signals (${detail.signalCount})`}
+ >
+ {detail.linkedSignals.length > 0 ? (
+
+ {detail.linkedSignals.map((signal) => (
+
+
{signal.title}
+
+ {signal.summary}
+
+
+ {signal.sourceCount} source
+ {signal.sourceCount === 1 ? "" : "s"} ·{" "}
+ {formatRelativeTime(signal.createdAt)}
+
+
+ ))}
+
+ ) : (
+
+ No Signals linked to this Work Unit.
+
+ )}
{/* Activity timeline */}
diff --git a/apps/web/src/hooks/work-os/use-work-os.ts b/apps/web/src/hooks/work-os/use-work-os.ts
index 8f82751..9a306e1 100644
--- a/apps/web/src/hooks/work-os/use-work-os.ts
+++ b/apps/web/src/hooks/work-os/use-work-os.ts
@@ -14,6 +14,7 @@ import {
eventsForIssue,
} from "@/lib/work-os/work-unit-projection";
import type {
+ LinkedSignal,
WorkUnitCard,
WorkUnitDetail,
} from "@/lib/work-os/work-unit-projection";
@@ -28,7 +29,6 @@ export interface SignalItem {
export type ComposerMode = "project" | "work-unit";
export interface WorkOsState {
- // Project
readonly projects: ReturnType["projects"];
readonly selectedProject: ReturnType<
typeof useProjectWorkspace
@@ -40,7 +40,6 @@ export interface WorkOsState {
readonly setRepository: (value: string) => void;
readonly connectRepository: () => Promise;
- // Work units
readonly workUnitCards: readonly WorkUnitCard[];
readonly selectedIssue: ReturnType<
typeof useProjectWorkspace
@@ -55,20 +54,36 @@ export interface WorkOsState {
readonly issueBody: string;
readonly setIssueBody: (value: string) => void;
- // Signals (project-scoped, displayed globally — not attributed to any issue)
readonly signals: readonly SignalItem[];
- // Chat
readonly composerMode: ComposerMode;
readonly setComposerMode: (mode: ComposerMode) => void;
readonly projectAgent: ChatAgentState;
readonly workUnitAgent: ChatAgentState;
- // Meta
readonly pendingAction: string | null;
readonly error: string | null;
}
+const toLinkedSignals = (
+ entries:
+ | readonly {
+ readonly signalId: Id<"signals">;
+ readonly title: string;
+ readonly summary: string;
+ readonly sourceCount: number;
+ readonly createdAt: number;
+ }[]
+ | undefined
+): readonly LinkedSignal[] =>
+ (entries ?? []).map((entry) => ({
+ createdAt: entry.createdAt,
+ signalId: entry.signalId,
+ sourceCount: entry.sourceCount,
+ summary: entry.summary,
+ title: entry.title,
+ }));
+
export const useWorkOs = (): WorkOsState => {
const workspace = useProjectWorkspace();
const orgState = usePersonalOrganization();
@@ -76,8 +91,6 @@ export const useWorkOs = (): WorkOsState => {
const activeProjectId = workspace.selectedProjectId;
- // Work-unit-scoped project-manager agent. Stays dormant until an issue is
- // selected, then follows that issue identity across re-renders.
const workUnitFlue = useFlueAgent({
id: workspace.selectedIssueId
? String(workspace.selectedIssueId)
@@ -93,9 +106,7 @@ export const useWorkOs = (): WorkOsState => {
status: workspace.selectedIssueId ? workUnitFlue.status : "idle",
};
- // Signals scoped to the active project. These are displayed as project-level
- // evidence, not attributed to any specific work unit (no signal-to-issue
- // relation exists yet).
+ // Project-level signals for the global sidebar panel.
const allSignals = useQuery(
api.signals.list,
orgState.organizationId
@@ -116,7 +127,13 @@ export const useWorkOs = (): WorkOsState => {
}));
}, [allSignals, activeProjectId]);
- // Composer mode tracks the selection by default but can be overridden.
+ // All linked signals for the active project, grouped by issue id.
+ // Used for card counts and detail display.
+ const linkedByIssue = useQuery(
+ api.signals.listLinkedSignalsForProject,
+ activeProjectId ? { projectId: activeProjectId } : "skip"
+ );
+
const [composerMode, setComposerMode] = useState("project");
const selectIssue = (issueId: Id<"projectIssues"> | null) => {
@@ -124,18 +141,21 @@ export const useWorkOs = (): WorkOsState => {
setComposerMode(issueId ? "work-unit" : "project");
};
- // Work unit card projections — derived from per-issue events only.
const workUnitCards = useMemo(() => {
if (!workspace.issues) {
return [];
}
return workspace.issues.map((issue) => {
const issueEvents = eventsForIssue(workspace.events, issue._id);
- return buildWorkUnitCard({ issue, issueEvents });
+ const linked = toLinkedSignals(linkedByIssue?.[String(issue._id)]);
+ return buildWorkUnitCard({
+ issue,
+ issueEvents,
+ linkedSignals: linked,
+ });
});
- }, [workspace.issues, workspace.events]);
+ }, [workspace.issues, workspace.events, linkedByIssue]);
- // Selected work unit detail — same per-issue derivation.
const selectedDetail = useMemo(() => {
if (!workspace.selectedIssue) {
return null;
@@ -144,11 +164,15 @@ export const useWorkOs = (): WorkOsState => {
workspace.events,
workspace.selectedIssue._id
);
+ const linked = toLinkedSignals(
+ linkedByIssue?.[String(workspace.selectedIssue._id)]
+ );
return buildWorkUnitDetail({
issue: workspace.selectedIssue,
issueEvents,
+ linkedSignals: linked,
});
- }, [workspace.selectedIssue, workspace.events]);
+ }, [workspace.selectedIssue, workspace.events, linkedByIssue]);
return {
composerMode,
diff --git a/apps/web/src/lib/work-os/work-unit-projection.test.ts b/apps/web/src/lib/work-os/work-unit-projection.test.ts
index 95f9c5d..b9f2f1b 100644
--- a/apps/web/src/lib/work-os/work-unit-projection.test.ts
+++ b/apps/web/src/lib/work-os/work-unit-projection.test.ts
@@ -10,6 +10,7 @@ import {
extractLatestSummary,
extractPullRequestFromEvents,
} from "./work-unit-projection";
+import type { LinkedSignal } from "./work-unit-projection";
type Issue = Doc<"projectIssues">;
type Event = Doc<"projectEvents">;
@@ -17,6 +18,17 @@ type Event = Doc<"projectEvents">;
const ISSUE_A = "issue-a" as Id<"projectIssues">;
const ISSUE_B = "issue-b" as Id<"projectIssues">;
+const makeLinkedSignal = (
+ overrides: Partial = {}
+): LinkedSignal => ({
+ createdAt: 5000,
+ signalId: "sig-1" as Id<"signals">,
+ sourceCount: 2,
+ summary: "Three users hit the same error.",
+ title: "OAuth crash on Safari",
+ ...overrides,
+});
+
const makeIssue = (overrides: Partial = {}): Issue =>
({
_creationTime: 1,
@@ -91,10 +103,11 @@ describe("extractArtifactPaths", () => {
});
describe("buildWorkUnitCard", () => {
- test("projects a fresh issue with no events", () => {
+ test("projects a fresh issue with no events and no linked signals", () => {
const card = buildWorkUnitCard({
issue: makeIssue(),
issueEvents: [],
+ linkedSignals: [],
});
expect(card.title).toBe("Fix Safari OAuth callback");
expect(card.status).toBe("open");
@@ -123,6 +136,7 @@ describe("buildWorkUnitCard", () => {
const card = buildWorkUnitCard({
issue: makeIssue({ status: "working" }),
issueEvents: events,
+ linkedSignals: [],
});
expect(card.artifactCount).toBe(2);
expect(card.stepCount).toBe(2);
@@ -137,6 +151,7 @@ describe("buildWorkUnitCard", () => {
const card = buildWorkUnitCard({
issue: makeIssue({ status: "needs-input" }),
issueEvents: events,
+ linkedSignals: [],
});
expect(card.needsInput).toBe(true);
expect(card.stepCount).toBe(3);
@@ -157,11 +172,68 @@ describe("buildWorkUnitCard", () => {
const card = buildWorkUnitCard({
issue: makeIssue({ status: "completed" }),
issueEvents: events,
+ linkedSignals: [],
});
expect(card.hasPullRequest).toBe(true);
});
});
+describe("linked signal projection", () => {
+ test("card signalCount reflects real linked signals", () => {
+ const card = buildWorkUnitCard({
+ issue: makeIssue(),
+ issueEvents: [],
+ linkedSignals: [
+ makeLinkedSignal(),
+ makeLinkedSignal({
+ signalId: "sig-2" as Id<"signals">,
+ title: "Build error",
+ }),
+ ],
+ });
+ expect(card.signalCount).toBe(2);
+ });
+
+ test("card signalCount is 0 when no signals are linked", () => {
+ const card = buildWorkUnitCard({
+ issue: makeIssue(),
+ issueEvents: [],
+ linkedSignals: [],
+ });
+ expect(card.signalCount).toBe(0);
+ });
+
+ test("detail exposes linked signals with title, summary, and source count", () => {
+ const detail = buildWorkUnitDetail({
+ issue: makeIssue(),
+ issueEvents: [],
+ linkedSignals: [
+ makeLinkedSignal(),
+ makeLinkedSignal({
+ signalId: "sig-2" as Id<"signals">,
+ sourceCount: 5,
+ summary: "CI broke",
+ }),
+ ],
+ });
+ expect(detail.signalCount).toBe(2);
+ expect(detail.linkedSignals).toHaveLength(2);
+ expect(detail.linkedSignals[0].title).toBe("OAuth crash on Safari");
+ expect(detail.linkedSignals[0].sourceCount).toBe(2);
+ expect(detail.linkedSignals[1].sourceCount).toBe(5);
+ });
+
+ test("detail linked signals empty when no attachment exists", () => {
+ const detail = buildWorkUnitDetail({
+ issue: makeIssue(),
+ issueEvents: [],
+ linkedSignals: [],
+ });
+ expect(detail.signalCount).toBe(0);
+ expect(detail.linkedSignals).toEqual([]);
+ });
+});
+
describe("extractLatestSummary", () => {
test("returns the most recent agent summary", () => {
const events = [
@@ -228,6 +300,19 @@ describe("buildActivityTimeline", () => {
expect(timeline[1].kind).toBe("issue.created");
expect(timeline[1].label).toBe("Work created");
});
+
+ test("maps signal.attached event to activity", () => {
+ const events = [
+ makeEvent(
+ "signal.attached",
+ { signalId: "sig-1", signalProblemTitle: "OAuth crash" },
+ { createdAt: 3000 }
+ ),
+ ];
+ const timeline = buildActivityTimeline(events);
+ expect(timeline[0].label).toBe("Signal linked");
+ expect(timeline[0].detail).toContain("OAuth crash");
+ });
});
describe("buildWorkUnitDetail", () => {
@@ -253,11 +338,12 @@ describe("buildWorkUnitDetail", () => {
const detail = buildWorkUnitDetail({
issue: makeIssue({ status: "completed" }),
issueEvents: events,
+ linkedSignals: [makeLinkedSignal()],
});
expect(detail.stepCount).toBe(4);
expect(detail.artifactCount).toBe(2);
expect(detail.artifactPaths).toEqual(["artifacts.md", "work.md"]);
- expect(detail.signalCount).toBe(0);
+ expect(detail.signalCount).toBe(1);
expect(detail.hasPullRequest).toBe(true);
expect(detail.pullRequestUrl).toBe("https://git.example.com/pulls/3");
expect(detail.pullRequestNumber).toBe(3);
@@ -272,6 +358,12 @@ describe("buildWorkUnitDetail", () => {
// ---------------------------------------------------------------------------
describe("cross-issue isolation", () => {
+ const SIGNAL_A = makeLinkedSignal({ signalId: "sig-a" as Id<"signals"> });
+ const SIGNAL_B = makeLinkedSignal({
+ signalId: "sig-b" as Id<"signals">,
+ title: "Different signal",
+ });
+
const sharedEvents: Event[] = [
makeEvent(
"issue.created",
@@ -316,6 +408,7 @@ describe("cross-issue isolation", () => {
const cardA = buildWorkUnitCard({
issue: makeIssue({ _id: ISSUE_A, number: 1 }),
issueEvents: eventsA,
+ linkedSignals: [SIGNAL_A],
});
expect(cardA.artifactCount).toBe(1);
expect(cardA.stepCount).toBe(4);
@@ -324,6 +417,7 @@ describe("cross-issue isolation", () => {
const cardB = buildWorkUnitCard({
issue: makeIssue({ _id: ISSUE_B, number: 2, title: "Issue B" }),
issueEvents: eventsB,
+ linkedSignals: [SIGNAL_B],
});
expect(cardB.artifactCount).toBe(1);
expect(cardB.stepCount).toBe(3);
@@ -334,18 +428,17 @@ describe("cross-issue isolation", () => {
const cardB = buildWorkUnitCard({
issue: makeIssue({ _id: ISSUE_B, number: 2, title: "Issue B" }),
issueEvents: eventsB,
+ linkedSignals: [],
});
expect(cardB.hasPullRequest).toBe(false);
});
test("issue A does not inherit issue B's summary", () => {
const eventsA = eventsForIssue(sharedEvents, ISSUE_A);
- const summaryA = extractLatestSummary(eventsA);
- expect(summaryA).toBe("Done for A.");
+ expect(extractLatestSummary(eventsA)).toBe("Done for A.");
const eventsB = eventsForIssue(sharedEvents, ISSUE_B);
- const summaryB = extractLatestSummary(eventsB);
- expect(summaryB).toBe("Failed for B.");
+ expect(extractLatestSummary(eventsB)).toBe("Failed for B.");
});
test("detail for issue A lists only issue A's artifact paths", () => {
@@ -353,6 +446,7 @@ describe("cross-issue isolation", () => {
const detailA = buildWorkUnitDetail({
issue: makeIssue({ _id: ISSUE_A, number: 1 }),
issueEvents: eventsA,
+ linkedSignals: [SIGNAL_A],
});
expect(detailA.artifactPaths).toEqual(["work.md"]);
expect(detailA.pullRequestNumber).toBe(5);
@@ -363,60 +457,28 @@ describe("cross-issue isolation", () => {
const detailB = buildWorkUnitDetail({
issue: makeIssue({ _id: ISSUE_B, number: 2, title: "Issue B" }),
issueEvents: eventsB,
+ linkedSignals: [],
});
expect(detailB.artifactPaths).toEqual(["artifacts.md"]);
expect(detailB.hasPullRequest).toBe(false);
expect(detailB.pullRequestUrl).toBeNull();
});
-});
-// ---------------------------------------------------------------------------
-// Signal isolation: a project may have signals, but no Work Unit should
-// claim them until a signal-to-issue relation is integrated.
-// ---------------------------------------------------------------------------
-
-describe("signal isolation", () => {
- test("card signalCount is 0 even when the project has signals", () => {
- // Simulate a project that has signals — these are NOT passed to the
- // card builder. The card must show 0 regardless.
- const projectHasSignals = [
- {
- sourceCount: 3,
- summary: "Three tickets about Safari",
- title: "Users report OAuth crash",
- },
- {
- sourceCount: 1,
- summary: "CI broke after merge",
- title: "Build failure on staging",
- },
- ];
-
- const card = buildWorkUnitCard({
- issue: makeIssue(),
- issueEvents: [],
+ test("issue A does not inherit issue B's linked signals", () => {
+ const detailA = buildWorkUnitDetail({
+ issue: makeIssue({ _id: ISSUE_A, number: 1 }),
+ issueEvents: eventsForIssue(sharedEvents, ISSUE_A),
+ linkedSignals: [SIGNAL_A],
});
- // The card never sees projectSignals — signalCount is always 0.
- expect(card.signalCount).toBe(0);
- expect(projectHasSignals.length).toBeGreaterThan(0);
- });
+ expect(detailA.signalCount).toBe(1);
+ expect(detailA.linkedSignals[0].title).toBe("OAuth crash on Safari");
- test("detail signalCount is 0 even when the project has signals", () => {
- const projectHasSignals = [
- {
- sourceCount: 3,
- summary: "Three tickets",
- title: "Users report OAuth crash",
- },
- ];
-
- const detail = buildWorkUnitDetail({
- issue: makeIssue(),
- issueEvents: [
- makeEvent("issue.created", { number: 1 }, { createdAt: 1000 }),
- ],
+ const detailB = buildWorkUnitDetail({
+ issue: makeIssue({ _id: ISSUE_B, number: 2, title: "Issue B" }),
+ issueEvents: eventsForIssue(sharedEvents, ISSUE_B),
+ linkedSignals: [SIGNAL_B],
});
- expect(detail.signalCount).toBe(0);
- expect(projectHasSignals.length).toBeGreaterThan(0);
+ expect(detailB.signalCount).toBe(1);
+ expect(detailB.linkedSignals[0].title).toBe("Different signal");
});
});
diff --git a/apps/web/src/lib/work-os/work-unit-projection.ts b/apps/web/src/lib/work-os/work-unit-projection.ts
index a69835c..d05a24c 100644
--- a/apps/web/src/lib/work-os/work-unit-projection.ts
+++ b/apps/web/src/lib/work-os/work-unit-projection.ts
@@ -4,16 +4,23 @@ import type { Doc, Id } from "@code/backend/convex/_generated/dataModel";
// Work Unit card projection — pure functions that turn Convex documents into
// the collapsed/expanded Work Unit views used by the Work OS surface.
//
-// Every count is derived from issue-scoped events only. We never use
-// project-wide artifact lists or signal lists, because those would
-// incorrectly attribute evidence to every issue in the project. Signal
-// linkage is intentionally 0 until a Lane A signal-to-issue relation is
-// integrated.
+// Counts are derived from issue-scoped evidence: events for artifacts/steps/
+// PRs, and authenticated signalIssueAttachments for linked Signals. We never
+// use project-wide artifact lists or signal lists.
// ---------------------------------------------------------------------------
export type ProjectEvent = Doc<"projectEvents">;
export type ProjectIssue = Doc<"projectIssues">;
+/** A Signal linked to a Work Unit via signalIssueAttachments. */
+export interface LinkedSignal {
+ readonly signalId: Id<"signals">;
+ readonly title: string;
+ readonly summary: string;
+ readonly sourceCount: number;
+ readonly createdAt: number;
+}
+
export interface WorkUnitCard {
readonly issueId: Id<"projectIssues">;
readonly number: number;
@@ -44,6 +51,7 @@ export interface WorkUnitDetail {
readonly artifactCount: number;
readonly artifactPaths: readonly string[];
readonly signalCount: number;
+ readonly linkedSignals: readonly LinkedSignal[];
readonly hasPullRequest: boolean;
readonly pullRequestUrl: string | null;
readonly pullRequestNumber: number | null;
@@ -148,6 +156,10 @@ const ACTIVITY_HANDLERS: Record<
detail: `Queued #${data.number ?? "?"} for the project manager`,
label: "Work queued",
}),
+ "signal.attached": (data) => ({
+ detail: `Signal "${getString(data, "signalProblemTitle", "Unknown")}" linked to this Work Unit`,
+ label: "Signal linked",
+ }),
};
const projectEventToActivity = (event: ProjectEvent): ActivityProjection => {
@@ -246,15 +258,17 @@ export const extractArtifactPaths = (
};
// ---------------------------------------------------------------------------
-// Card builder — derives all counts from issue-scoped events only
+// Card builder — derives counts from issue-scoped events + linked signals
// ---------------------------------------------------------------------------
export const buildWorkUnitCard = ({
issue,
issueEvents,
+ linkedSignals,
}: {
readonly issue: ProjectIssue;
readonly issueEvents: readonly ProjectEvent[];
+ readonly linkedSignals: readonly LinkedSignal[];
}): WorkUnitCard => {
const sorted = [...issueEvents].toSorted((a, b) => b.createdAt - a.createdAt);
const [latestEvent] = sorted;
@@ -272,9 +286,7 @@ export const buildWorkUnitCard = ({
issueId: issue._id,
needsInput: issue.status === "needs-input",
number: issue.number,
- // No signal-to-issue relation exists in the event model yet; Lane A will
- // add one. Show 0 rather than falsely claiming project-wide signals.
- signalCount: 0,
+ signalCount: linkedSignals.length,
status: issue.status,
stepCount: issueEvents.length,
summary,
@@ -284,15 +296,17 @@ export const buildWorkUnitCard = ({
};
// ---------------------------------------------------------------------------
-// Detail builder — same per-issue derivation, plus artifact paths
+// Detail builder — same per-issue derivation, plus linked signals
// ---------------------------------------------------------------------------
export const buildWorkUnitDetail = ({
issue,
issueEvents,
+ linkedSignals,
}: {
readonly issue: ProjectIssue;
readonly issueEvents: readonly ProjectEvent[];
+ readonly linkedSignals: readonly LinkedSignal[];
}): WorkUnitDetail => {
const activity = buildActivityTimeline(issueEvents);
const prInfo = extractPullRequestFromEvents(issueEvents);
@@ -305,10 +319,11 @@ export const buildWorkUnitDetail = ({
hasPullRequest: prInfo.hasPR,
issue,
latestSummary: extractLatestSummary(issueEvents),
+ linkedSignals,
needsInput: issue.status === "needs-input",
pullRequestNumber: prInfo.number,
pullRequestUrl: prInfo.url,
- signalCount: 0,
+ signalCount: linkedSignals.length,
stepCount: issueEvents.length,
};
};
diff --git a/packages/backend/convex/signals.ts b/packages/backend/convex/signals.ts
index fd21784..bcba426 100644
--- a/packages/backend/convex/signals.ts
+++ b/packages/backend/convex/signals.ts
@@ -9,7 +9,7 @@ import {
type QueryCtx,
query,
} from "./_generated/server";
-import { requireOrganizationMember } from "./authz";
+import { requireOrganizationMember, requireProjectMember } from "./authz";
const PROBLEM_STATEMENT = v.object({
title: v.string(),
@@ -435,3 +435,70 @@ export const get = query({
};
},
});
+
+// ---------------------------------------------------------------------------
+// Linked Signal query for the Work OS surface.
+// ---------------------------------------------------------------------------
+
+export interface LinkedSignalView {
+ readonly signalId: Id<"signals">;
+ readonly title: string;
+ readonly summary: string;
+ readonly sourceCount: number;
+ readonly createdAt: number;
+}
+
+/**
+ * List all Signal-issue attachments for a project, grouped by issue id.
+ * User-authenticated: the caller must be a member of the project organization.
+ * Used by the Work OS card list to show per-issue linked Signal counts.
+ */
+export const listLinkedSignalsForProject = query({
+ args: {
+ projectId: v.id("projects"),
+ },
+ handler: async (ctx, args): Promise> => {
+ const { organizationId } = await requireProjectMember(ctx, args.projectId);
+ const issues = await ctx.db
+ .query("projectIssues")
+ .withIndex("by_project_and_number", (q) =>
+ q.eq("projectId", args.projectId)
+ )
+ .take(100);
+ const result: Record = {};
+ for (const issue of issues) {
+ const attachments = await ctx.db
+ .query("signalIssueAttachments")
+ .withIndex("by_issue", (q) => q.eq("issueId", issue._id))
+ .collect();
+ if (attachments.length === 0) {
+ continue;
+ }
+ const signals = await Promise.all(
+ attachments.map(async (attachment) => {
+ const signal = await ctx.db.get(attachment.signalId);
+ if (!signal || signal.organizationId !== organizationId) {
+ return null;
+ }
+ const sources = await ctx.db
+ .query("signalSources")
+ .withIndex("by_signal_and_ordinal", (q) =>
+ q.eq("signalId", signal._id)
+ )
+ .collect();
+ return {
+ createdAt: signal.createdAt,
+ signalId: signal._id,
+ sourceCount: sources.length,
+ summary: signal.problemStatement.summary,
+ title: signal.problemStatement.title,
+ } satisfies LinkedSignalView;
+ })
+ );
+ result[String(issue._id)] = signals.filter(
+ (entry): entry is LinkedSignalView => entry !== null
+ );
+ }
+ return result;
+ },
+});