From 52bf48cce0f383509b0b0d638227c447f59f1def Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Fri, 7 Aug 2026 09:51:22 -0400 Subject: [PATCH] feat(twister,linear): structured-container taxonomy signals on links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LinkSignals gains taxonomy — namespaced keys for the structured containers an item belongs to (project, repository, board, folder), matched exactly against priority bindings so items route to the user's priorities without a model call. The Linear connector emits the issue's project (a container within the team; the team itself is already the sync channel). Claude-Session: https://claude.ai/code/session_01FHoQc2TdA62Wp4BwUxvR6P Co-authored-by: Claude Fable 5 --- .changeset/link-signals-taxonomy.md | 10 ++++++++++ connectors/linear/src/linear-sync.test.ts | 16 ++++++++++++++++ connectors/linear/src/linear-sync.ts | 10 ++++++++++ twister/src/signals.ts | 13 +++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 .changeset/link-signals-taxonomy.md diff --git a/.changeset/link-signals-taxonomy.md b/.changeset/link-signals-taxonomy.md new file mode 100644 index 00000000..67a2cf41 --- /dev/null +++ b/.changeset/link-signals-taxonomy.md @@ -0,0 +1,10 @@ +--- +"@plotday/twister": minor +--- + +Added: `LinkSignals.taxonomy` — namespaced structured-container keys +(`"::"`, e.g. `"linear:project:"`) a connector +emits for the project, repository, board or folder an item lives in. The +platform matches these exactly against priority bindings to route items to +the user's priorities without a model call. Use immutable ids, never display +names; omit the field when the source has no structured container. diff --git a/connectors/linear/src/linear-sync.test.ts b/connectors/linear/src/linear-sync.test.ts index e738c13e..3ca5f362 100644 --- a/connectors/linear/src/linear-sync.test.ts +++ b/connectors/linear/src/linear-sync.test.ts @@ -181,3 +181,19 @@ describe("buildIssueLink", () => { }); }); }); + +describe("buildIssueLink taxonomy signals", () => { + it("emits the issue's project as a namespaced taxonomy key", () => { + const link = buildIssueLink( + issue({ project: { id: "proj-7" } }), + "team-9", + false + ); + expect(link.signals?.taxonomy).toEqual(["linear:project:proj-7"]); + }); + + it("omits taxonomy when the issue has no project", () => { + const link = buildIssueLink(issue(), "team-9", false); + expect(link.signals?.taxonomy).toBeUndefined(); + }); +}); diff --git a/connectors/linear/src/linear-sync.ts b/connectors/linear/src/linear-sync.ts index e21fe8e2..0278708e 100644 --- a/connectors/linear/src/linear-sync.ts +++ b/connectors/linear/src/linear-sync.ts @@ -57,6 +57,7 @@ export const TEAM_ISSUES_BATCH_QUERY = ` creator { id name email avatarUrl } assignee { id name email avatarUrl } state { id type } + project { id } comments(first: ${COMMENTS_PER_ISSUE}) { nodes { id @@ -102,6 +103,9 @@ export type LinearIssueData = { creator: LinearUserData | null; assignee: LinearUserData | null; state: { id: string; type: string } | null; + // The Linear project the issue belongs to (a container WITHIN a team — + // teams are already the sync channel). Feeds signals.taxonomy. + project?: { id: string } | null; comments: { nodes: LinearCommentData[] }; attachments: { nodes: LinearAttachmentData[] }; }; @@ -227,6 +231,12 @@ export function buildIssueLink( linearId: issue.id, projectId, }, + // The issue's Linear project (a container within the team) as a + // namespaced taxonomy key — the platform matches it against priority + // bindings at L1. The team itself is the sync channel and needs no key. + ...(issue.project?.id + ? { signals: { taxonomy: [`linear:project:${issue.project.id}`] } } + : {}), actions: threadActions.length > 0 ? threadActions : undefined, sourceUrl: issue.url ?? null, notes, diff --git a/twister/src/signals.ts b/twister/src/signals.ts index dc65fbbe..9da8ec50 100644 --- a/twister/src/signals.ts +++ b/twister/src/signals.ts @@ -81,6 +81,19 @@ export type LinkSignals = { * platform classifies from `link.preview` instead. */ noteKey?: string | null; + /** + * Structured containers the source assigns this item to — the project, + * repository, board or folder the item lives in, as namespaced keys: + * `"::"` (e.g. `"linear:project:"`, + * `"github:repo:/"`). + * + * Use immutable ids (uuid/number), never mutable display names — the same + * rule as `link.source`. Emit every container the item genuinely belongs + * to; omit the field when the source has no structured container. The + * platform matches these keys exactly against priority bindings to route + * items to the user's priorities without a model call. + */ + taxonomy?: string[]; }; const NOREPLY_LOCALPART =