Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions openspec/changes/replace-sdk-core-string-ref-factories/tasks.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
## 1. Ref inventory and guardrails

- [ ] 1.1 Freeze the March 11, 2026 sdk-core factory inventory and define the approved fixed-ref pattern for covered wrapper modules.
- [ ] 1.2 Add or update guard coverage that fails if covered sdk-core API files reintroduce `getQueryRef(name: string)` or `getMutationRef(name: string)`.
- [x] 1.1 Freeze the March 11, 2026 sdk-core factory inventory and define the approved fixed-ref pattern for covered wrapper modules.
- [x] 1.2 Add or update guard coverage that fails if covered sdk-core API files reintroduce `getQueryRef(name: string)` or `getMutationRef(name: string)`.

## 2. Replace session and messaging-facing factories

- [ ] 2.1 Replace generic selector helpers in `sessions.ts`, `conversations.ts`, `visitors.ts`, `tickets.ts`, and `outbound.ts` with explicit fixed refs.
- [ ] 2.2 Preserve current wrapper contracts and update any touched sdk-core tests or fixtures for those domains.
- [x] 2.1 Replace generic selector helpers in `sessions.ts`, `conversations.ts`, `visitors.ts`, `tickets.ts`, and `outbound.ts` with explicit fixed refs.
- [x] 2.2 Preserve current wrapper contracts and update any touched sdk-core tests or fixtures for those domains.

## 3. Replace content and automation-facing factories

- [ ] 3.1 Replace generic selector helpers in `aiAgent.ts`, `articles.ts`, `carousels.ts`, `checklists.ts`, `commonIssues.ts`, `events.ts`, and `officeHours.ts`.
- [ ] 3.2 Keep any required `TS2589` workaround localized to fixed ref declarations or another explicit shallow boundary.
- [x] 3.1 Replace generic selector helpers in `aiAgent.ts`, `articles.ts`, `carousels.ts`, `checklists.ts`, `commonIssues.ts`, `events.ts`, and `officeHours.ts`.
- [x] 3.2 Keep any required `TS2589` workaround localized to fixed ref declarations or another explicit shallow boundary.

## 4. Verification

- [ ] 4.1 Run `pnpm --filter @opencom/sdk-core typecheck`.
- [ ] 4.2 Run targeted sdk-core tests for the touched wrapper modules.
- [ ] 4.3 Run `openspec validate replace-sdk-core-string-ref-factories --strict --no-interactive`.
- [x] 4.1 Run `pnpm --filter @opencom/sdk-core typecheck`.
- [x] 4.2 Run targeted sdk-core tests for the touched wrapper modules.
- [x] 4.3 Run `openspec validate replace-sdk-core-string-ref-factories --strict --no-interactive`.
33 changes: 20 additions & 13 deletions packages/sdk-core/src/api/aiAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ import { getClient, getConfig } from "./client";
import type { ConversationId, VisitorId } from "../types";
import { getVisitorState } from "../state/visitor";

function getQueryRef(name: string): FunctionReference<"query"> {
return makeFunctionReference(name) as FunctionReference<"query">;
}

function getMutationRef(name: string): FunctionReference<"mutation"> {
return makeFunctionReference(name) as FunctionReference<"mutation">;
}
// Generated api.aiAgent.* refs trigger TS2589 in sdk-core, so keep the fallback
// localized to these explicit AI agent refs only.
const GET_PUBLIC_AI_SETTINGS_REF =
makeFunctionReference("aiAgent:getPublicSettings") as FunctionReference<"query">;
const GET_RELEVANT_KNOWLEDGE_REF =
makeFunctionReference("aiAgent:getRelevantKnowledge") as FunctionReference<"query">;
const GET_CONVERSATION_AI_RESPONSES_REF =
makeFunctionReference("aiAgent:getConversationResponses") as FunctionReference<"query">;
const SUBMIT_AI_FEEDBACK_REF =
makeFunctionReference("aiAgent:submitFeedback") as FunctionReference<"mutation">;
const HANDOFF_TO_HUMAN_REF =
makeFunctionReference("aiAgent:handoffToHuman") as FunctionReference<"mutation">;
const SHOULD_AI_RESPOND_REF =
makeFunctionReference("aiAgent:shouldRespond") as FunctionReference<"query">;

export type AIResponseId = Id<"aiResponses">;

Expand Down Expand Up @@ -59,7 +66,7 @@ export async function getAISettings(): Promise<AIAgentSettings> {
const client = getClient();
const config = getConfig();

const settings = await client.query(getQueryRef("aiAgent:getPublicSettings"), {
const settings = await client.query(GET_PUBLIC_AI_SETTINGS_REF, {
workspaceId: config.workspaceId as Id<"workspaces">,
});

Expand All @@ -73,7 +80,7 @@ export async function getRelevantKnowledge(
const client = getClient();
const config = getConfig();

const results = await client.query(getQueryRef("aiAgent:getRelevantKnowledge"), {
const results = await client.query(GET_RELEVANT_KNOWLEDGE_REF, {
workspaceId: config.workspaceId as Id<"workspaces">,
query,
limit,
Expand All @@ -92,7 +99,7 @@ export async function getConversationAIResponses(
const resolvedVisitorId = visitorId ?? state.visitorId ?? undefined;
const token = sessionToken ?? state.sessionToken ?? undefined;

const responses = await client.query(getQueryRef("aiAgent:getConversationResponses"), {
const responses = await client.query(GET_CONVERSATION_AI_RESPONSES_REF, {
conversationId,
visitorId: resolvedVisitorId,
sessionToken: token,
Expand All @@ -112,7 +119,7 @@ export async function submitAIFeedback(
const resolvedVisitorId = visitorId ?? state.visitorId ?? undefined;
const token = sessionToken ?? state.sessionToken ?? undefined;

await client.mutation(getMutationRef("aiAgent:submitFeedback"), {
await client.mutation(SUBMIT_AI_FEEDBACK_REF, {
responseId,
feedback,
visitorId: resolvedVisitorId,
Expand All @@ -131,7 +138,7 @@ export async function handoffToHuman(
const resolvedVisitorId = visitorId ?? state.visitorId ?? undefined;
const token = sessionToken ?? state.sessionToken ?? undefined;

const result = await client.mutation(getMutationRef("aiAgent:handoffToHuman"), {
const result = await client.mutation(HANDOFF_TO_HUMAN_REF, {
conversationId,
visitorId: resolvedVisitorId,
sessionToken: token,
Expand All @@ -148,7 +155,7 @@ export async function shouldAIRespond(): Promise<{
const client = getClient();
const config = getConfig();

const result = await client.query(getQueryRef("aiAgent:shouldRespond"), {
const result = await client.query(SHOULD_AI_RESPOND_REF, {
workspaceId: config.workspaceId as Id<"workspaces">,
});

Expand Down
16 changes: 10 additions & 6 deletions packages/sdk-core/src/api/articles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import { getClient, getConfig } from "./client";
import type { VisitorId, ArticleId, ArticleData } from "../types";
import { getVisitorState } from "../state/visitor";

function getQueryRef(name: string): FunctionReference<"query"> {
return makeFunctionReference(name) as FunctionReference<"query">;
}
// Generated api.articles.* refs trigger TS2589 in sdk-core, so keep the fallback
// localized to these explicit article refs only.
const SEARCH_ARTICLES_FOR_VISITOR_REF =
makeFunctionReference("articles:searchForVisitor") as FunctionReference<"query">;
const LIST_ARTICLES_FOR_VISITOR_REF =
makeFunctionReference("articles:listForVisitor") as FunctionReference<"query">;
const GET_ARTICLE_REF = makeFunctionReference("articles:get") as FunctionReference<"query">;

interface ArticleDoc {
_id: ArticleId;
Expand All @@ -25,7 +29,7 @@ export async function searchArticles(params: {
const state = getVisitorState();
const sessionToken = params.sessionToken ?? state.sessionToken ?? undefined;

const results = await client.query(getQueryRef("articles:searchForVisitor"), {
const results = await client.query(SEARCH_ARTICLES_FOR_VISITOR_REF, {
workspaceId: config.workspaceId as Id<"workspaces">,
visitorId: params.visitorId,
sessionToken,
Expand All @@ -49,7 +53,7 @@ export async function listArticles(
const state = getVisitorState();
const token = sessionToken ?? state.sessionToken ?? undefined;

const results = await client.query(getQueryRef("articles:listForVisitor"), {
const results = await client.query(LIST_ARTICLES_FOR_VISITOR_REF, {
workspaceId: config.workspaceId as Id<"workspaces">,
visitorId,
sessionToken: token,
Expand All @@ -66,7 +70,7 @@ export async function listArticles(
export async function getArticle(articleId: ArticleId): Promise<ArticleData | null> {
const client = getClient();

const article = await client.query(getQueryRef("articles:get"), { id: articleId });
const article = await client.query(GET_ARTICLE_REF, { id: articleId });

if (!article) return null;

Expand Down
20 changes: 10 additions & 10 deletions packages/sdk-core/src/api/carousels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { getClient, getConfig } from "./client";
import type { VisitorId, CarouselId, CarouselData, CarouselScreen } from "../types";
import { getVisitorState } from "../state/visitor";

function getQueryRef(name: string): FunctionReference<"query"> {
return makeFunctionReference(name) as FunctionReference<"query">;
}

function getMutationRef(name: string): FunctionReference<"mutation"> {
return makeFunctionReference(name) as FunctionReference<"mutation">;
}
// Generated api.carousels.* refs trigger TS2589 in sdk-core, so keep the fallback
// localized to these explicit carousel refs only.
const GET_CAROUSEL_REF = makeFunctionReference("carousels:get") as FunctionReference<"query">;
const RECORD_CAROUSEL_IMPRESSION_REF =
makeFunctionReference("carousels:recordImpression") as FunctionReference<"mutation">;
const LIST_ACTIVE_CAROUSELS_REF =
makeFunctionReference("carousels:listActive") as FunctionReference<"query">;

interface CarouselDoc {
_id: CarouselId;
Expand All @@ -21,7 +21,7 @@ interface CarouselDoc {
export async function getCarousel(carouselId: CarouselId): Promise<CarouselData | null> {
const client = getClient();

const carousel = await client.query(getQueryRef("carousels:get"), { id: carouselId });
const carousel = await client.query(GET_CAROUSEL_REF, { id: carouselId });

if (!carousel) return null;

Expand All @@ -42,7 +42,7 @@ export async function recordCarouselImpression(params: {
const client = getClient();
const token = params.sessionToken ?? getVisitorState().sessionToken ?? undefined;

await client.mutation(getMutationRef("carousels:recordImpression"), {
await client.mutation(RECORD_CAROUSEL_IMPRESSION_REF, {
carouselId: params.carouselId,
visitorId: params.visitorId,
sessionToken: token,
Expand All @@ -59,7 +59,7 @@ export async function listActiveCarousels(
const config = getConfig();
const token = sessionToken ?? getVisitorState().sessionToken ?? undefined;

const carousels = await client.query(getQueryRef("carousels:listActive"), {
const carousels = await client.query(LIST_ACTIVE_CAROUSELS_REF, {
workspaceId: config.workspaceId as Id<"workspaces">,
visitorId,
sessionToken: token,
Expand Down
25 changes: 13 additions & 12 deletions packages/sdk-core/src/api/checklists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import type { VisitorId } from "../types";
import { getVisitorState } from "../state/visitor";
import { makeFunctionReference, type FunctionReference } from "convex/server";

function getMutationRef(name: string): FunctionReference<"mutation"> {
return makeFunctionReference(name) as FunctionReference<"mutation">;
}

function getQueryRef(name: string): FunctionReference<"query"> {
return makeFunctionReference(name) as FunctionReference<"query">;
}
// Generated api.checklists.* refs trigger TS2589 in sdk-core, so keep the
// fallback localized to these explicit checklist refs only.
const GET_ELIGIBLE_CHECKLISTS_REF =
makeFunctionReference("checklists:getEligible") as FunctionReference<"query">;
const GET_CHECKLIST_PROGRESS_REF =
makeFunctionReference("checklists:getProgress") as FunctionReference<"query">;
const COMPLETE_CHECKLIST_TASK_REF =
makeFunctionReference("checklists:completeTask") as FunctionReference<"mutation">;

export type ChecklistId = Id<"checklists">;

Expand Down Expand Up @@ -63,7 +64,7 @@ export async function getEligibleChecklists(
const config = getConfig();
const token = sessionToken ?? getVisitorState().sessionToken ?? undefined;

const results = await client.query(getQueryRef("checklists:getEligible"), {
const results = await client.query(GET_ELIGIBLE_CHECKLISTS_REF, {
workspaceId: config.workspaceId as Id<"workspaces">,
visitorId,
sessionToken: token,
Expand All @@ -81,7 +82,7 @@ export async function getChecklistProgress(
const config = getConfig();
const token = sessionToken ?? getVisitorState().sessionToken ?? undefined;

const progress = await client.query(getQueryRef("checklists:getProgress"), {
const progress = await client.query(GET_CHECKLIST_PROGRESS_REF, {
visitorId,
checklistId,
workspaceId: config.workspaceId as Id<"workspaces">,
Expand All @@ -107,7 +108,7 @@ export async function completeChecklistItem(
const config = getConfig();
const token = sessionToken ?? getVisitorState().sessionToken ?? undefined;

await client.mutation(getMutationRef("checklists:completeTask"), {
await client.mutation(COMPLETE_CHECKLIST_TASK_REF, {
visitorId,
checklistId,
taskId,
Expand All @@ -126,7 +127,7 @@ export async function dismissChecklist(
const config = getConfig();
const token = sessionToken ?? getVisitorState().sessionToken ?? undefined;

const checklists = await client.query(getQueryRef("checklists:getEligible"), {
const checklists = await client.query(GET_ELIGIBLE_CHECKLISTS_REF, {
workspaceId: config.workspaceId as Id<"workspaces">,
visitorId,
sessionToken: token,
Expand All @@ -138,7 +139,7 @@ export async function dismissChecklist(
// Complete all remaining tasks to dismiss
for (const task of checklist.checklist.tasks) {
if (!checklist.progress?.completedTaskIds.includes(task.id)) {
await client.mutation(getMutationRef("checklists:completeTask"), {
await client.mutation(COMPLETE_CHECKLIST_TASK_REF, {
visitorId,
checklistId,
taskId: task.id,
Expand Down
9 changes: 5 additions & 4 deletions packages/sdk-core/src/api/commonIssues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { Id } from "@opencom/convex/dataModel";
import { getClient, getConfig } from "./client";
import { makeFunctionReference, type FunctionReference } from "convex/server";

function getQueryRef(name: string): FunctionReference<"query"> {
return makeFunctionReference(name) as FunctionReference<"query">;
}
// Generated api.commonIssueButtons.* refs trigger TS2589 in sdk-core, so keep
// the fallback localized to these explicit common-issue refs only.
const LIST_COMMON_ISSUE_BUTTONS_REF =
makeFunctionReference("commonIssueButtons:list") as FunctionReference<"query">;

export type CommonIssueButtonId = Id<"commonIssueButtons">;

Expand All @@ -28,7 +29,7 @@ export async function getCommonIssueButtons(): Promise<CommonIssueButton[]> {
const client = getClient();
const config = getConfig();

const buttons = await client.query(getQueryRef("commonIssueButtons:list"), {
const buttons = await client.query(LIST_COMMON_ISSUE_BUTTONS_REF, {
workspaceId: config.workspaceId as Id<"workspaces">,
});

Expand Down
31 changes: 18 additions & 13 deletions packages/sdk-core/src/api/conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import { getClient, getConfig } from "./client";
import type { VisitorId, ConversationId } from "../types";
import { getVisitorState } from "../state/visitor";

function getMutationRef(name: string): FunctionReference<"mutation"> {
return makeFunctionReference(name) as FunctionReference<"mutation">;
}

function getQueryRef(name: string): FunctionReference<"query"> {
return makeFunctionReference(name) as FunctionReference<"query">;
}
// Generated api.conversations.* and api.messages.* refs trigger TS2589 in sdk-core,
// so keep the fallback localized to these explicit conversation refs only.
const CREATE_CONVERSATION_REF =
makeFunctionReference("conversations:createForVisitor") as FunctionReference<"mutation">;
const GET_OR_CREATE_CONVERSATION_REF =
makeFunctionReference("conversations:getOrCreateForVisitor") as FunctionReference<"mutation">;
const LIST_MESSAGES_REF = makeFunctionReference("messages:list") as FunctionReference<"query">;
const LIST_CONVERSATIONS_BY_VISITOR_REF =
makeFunctionReference("conversations:listByVisitor") as FunctionReference<"query">;
const MARK_AS_READ_REF =
makeFunctionReference("conversations:markAsRead") as FunctionReference<"mutation">;
const SEND_MESSAGE_REF = makeFunctionReference("messages:send") as FunctionReference<"mutation">;

function requireVisitorSessionToken(sessionToken?: string): string {
const resolvedSessionToken = sessionToken ?? getVisitorState().sessionToken ?? undefined;
Expand All @@ -28,7 +33,7 @@ export async function createConversation(
const config = getConfig();
const resolvedSessionToken = requireVisitorSessionToken(sessionToken);

const result = await client.mutation(getMutationRef("conversations:createForVisitor"), {
const result = await client.mutation(CREATE_CONVERSATION_REF, {
workspaceId: config.workspaceId as Id<"workspaces">,
visitorId,
sessionToken: resolvedSessionToken,
Expand All @@ -45,7 +50,7 @@ export async function getOrCreateConversation(
const config = getConfig();
const resolvedSessionToken = requireVisitorSessionToken(sessionToken);

const result = await client.mutation(getMutationRef("conversations:getOrCreateForVisitor"), {
const result = await client.mutation(GET_OR_CREATE_CONVERSATION_REF, {
workspaceId: config.workspaceId as Id<"workspaces">,
visitorId,
sessionToken: resolvedSessionToken,
Expand All @@ -70,7 +75,7 @@ export async function getMessages(
> {
const client = getClient();

const result = await client.query(getQueryRef("messages:list"), {
const result = await client.query(LIST_MESSAGES_REF, {
conversationId,
visitorId,
sessionToken,
Expand All @@ -84,7 +89,7 @@ export async function getConversations(visitorId: VisitorId, sessionToken?: stri
const config = getConfig();
const resolvedSessionToken = requireVisitorSessionToken(sessionToken);

const result = await client.query(getQueryRef("conversations:listByVisitor"), {
const result = await client.query(LIST_CONVERSATIONS_BY_VISITOR_REF, {
visitorId,
sessionToken: resolvedSessionToken,
workspaceId: config.workspaceId as Id<"workspaces">,
Expand All @@ -100,7 +105,7 @@ export async function markAsRead(
): Promise<void> {
const client = getClient();
const resolvedSessionToken = requireVisitorSessionToken(sessionToken);
await client.mutation(getMutationRef("conversations:markAsRead"), {
await client.mutation(MARK_AS_READ_REF, {
id: conversationId,
readerType: "visitor",
visitorId,
Expand All @@ -117,7 +122,7 @@ export async function sendMessage(params: {
const client = getClient();
const resolvedSessionToken = params.sessionToken ?? getVisitorState().sessionToken ?? undefined;

await client.mutation(getMutationRef("messages:send"), {
await client.mutation(SEND_MESSAGE_REF, {
conversationId: params.conversationId,
senderId: params.visitorId,
senderType: "visitor",
Expand Down
Loading
Loading