Skip to content

Latest commit

 

History

History
659 lines (477 loc) · 26.8 KB

File metadata and controls

659 lines (477 loc) · 26.8 KB

AGENTS

Non-negotiables

  • Use PNPM commands in this repo (workspace uses pnpm-workspace.yaml).
  • Always run new/updated tests after creating or changing them.
  • Prefer focused verification first (targeted package/spec), then broader checks when needed.
  • At the end of each proposal when ready for a PR, run pnpm ci:check to ensure all checks pass.

Quick Repo Orientation

  • Monorepo root: opencom
  • Main apps: apps/web, apps/landing, apps/mobile, apps/widget
  • Backend: packages/convex
  • OpenSpec source of truth: openspec/changes/<change-name>/

General Workflow Guardrails

  • Start every non-trivial task by grounding in current repo state before changing files:
    1. identify the active scope
    2. read the relevant files/specs/tests
    3. verify whether the work is already partly done
    4. choose a narrow verification plan
  • If working from an existing OpenSpec change, always read:
    • openspec status --change "<change-name>" --json
    • openspec instructions apply --change "<change-name>" --json
    • the current proposal.md, design.md, specs/**/*.md, and tasks.md
  • Never assume unchecked boxes in tasks.md mean the code is still missing. Verify the current implementation first, then update artifacts or tasks to match reality.
  • Before creating a new OpenSpec change, quickly check for overlapping active changes or existing specs so you do not create duplicates or split ownership accidentally.
  • For multi-step work, keep an explicit plan/todo and update it as tasks complete. Prefer one active task at a time.
  • When changing course mid-task, record the new scope and the reason in the active change artifacts if they are affected.
  • Before marking work complete, verify both code and artifacts:
    • code/tests/typechecks reflect the final state
    • tasks.md checkboxes match what is actually done
    • any follow-up work is written down explicitly instead of left implicit

Existing Proposal Discipline

  • If you did not create the current proposal/change, treat the artifacts as hypotheses until verified against the codebase.
  • Separate findings into three buckets before editing artifacts:
    • already implemented
    • still unfinished
    • intentionally out of scope or accepted exception
  • Only put unfinished work into active proposal/spec/task artifacts.
  • If code and artifacts disagree, prefer fixing the artifact first unless the user explicitly asked for implementation.
  • When leaving partial progress, record exact remaining file clusters, blockers, and verification still needed so a later pass can continue without re-auditing the whole repo.

High-Value Commands (copy/paste)

Typecheck

  • Convex only:
    • pnpm --filter @opencom/convex typecheck
  • Web only:
    • pnpm --filter @opencom/web typecheck
  • Whole workspace:
    • pnpm typecheck

Convex TypeScript deep-instantiation workaround

  • Canonical guide: docs/convex-type-safety-playbook.md
  • If Convex typecheck hits TS2589 (Type instantiation is excessively deep and possibly infinite) at generated refs like api.foo.bar or internal.foo.bar, prefer a local escape hatch instead of broad weakening.
  • First keep call signatures shallow at the hot spot:
    • cast ctx.scheduler.runAfter, ctx.runQuery, or ctx.runMutation to a local shallow function type.
  • If merely referencing api... / internal... still triggers TS2589, use makeFunctionReference("module:function") from convex/server at that call site instead of property access on generated refs.
  • Keep this workaround localized only to pathological sites. Continue using generated api / internal refs normally elsewhere.
  • Expect hidden follow-on errors: rerun pnpm --filter @opencom/convex typecheck after each small batch of fixes, because resolving one deep-instantiation site can reveal additional ones.

Convex Type Safety Standards

  • Read docs/convex-type-safety-playbook.md before adding new Convex boundaries.
  • Frontend runtime/UI modules must not import convex/react directly. Use local adapters and wrapper hooks instead.
  • Keep Convex refs at module scope. Never create makeFunctionReference(...) values inside React components or hooks.
  • Do not add new getQueryRef(name: string), getMutationRef(name: string), or getActionRef(name: string) factories.
  • Backend cross-function calls should use generated api / internal refs by default. Only move to fixed makeFunctionReference("module:function") refs after a real TS2589 hotspot is confirmed.
  • Keep unavoidable casts localized to adapters or named backend hotspot helpers. Do not spread as unknown as, unsafeApi, or unsafeInternal through runtime code.
  • After changing a boundary, update the relevant hardening guard:
    • packages/convex/tests/runtimeTypeHardeningGuard.test.ts
    • apps/web/src/app/typeHardeningGuard.test.ts
    • apps/widget/src/test/refHardeningGuard.test.ts
    • packages/react-native-sdk/tests/hookBoundaryGuard.test.ts

Convex Hardening Audit Triage

  • Before treating an audit item as open work, verify whether it is already implemented and only the guard/proposal text is stale.
  • Default classification for current repo state:
    • packages/sdk-core/src/api/*.ts manual fixed refs are generally approved TS2589 hotspots, not automatic cleanup targets.
    • packages/sdk-core/src/api/aiAgent.ts already routes getRelevantKnowledge through client.action(...); do not reopen the old query-path migration unless you find a current regression.
    • packages/convex/convex/embeddings.ts batching/backfill concurrency work is already in place; do not create new perf tasks for generateBatch, backfillExisting, or generateBatchInternal unless the current code regressed.
    • packages/convex/convex/testAdmin.ts is an explicit dynamic exception because it intentionally dispatches caller-selected internal test mutations.
  • Treat these patterns differently:
    • Remaining cleanup target: generic name: string ref helpers such as makeInternalQueryRef(name) / getQueryRef(name) in covered runtime files.
    • Usually acceptable hotspot: fixed module-scope makeFunctionReference("module:function") constants with a narrow comment or guard-railed TS2589 justification.
    • Accepted exception: intentionally dynamic dispatch that is security-constrained and documented (currently testAdmin.ts).
  • When cleaning backend Convex boundaries, prefer this order:
    1. Generated api / internal refs
    2. Named shallow runner helper at the hot spot
    3. Fixed makeFunctionReference("module:function") constant
    4. Only if intentionally dynamic and documented, a narrow exception
  • Do not add new generic helper factories to shared ref modules. If a module exists to share refs, export fixed named refs from it.

Testing Best Practices

Do

  • Create isolated test data using helpers
  • Clean up after tests
  • Use descriptive test names
  • Test both success and error cases
  • Use data-testid attributes for E2E selectors
  • Keep tests focused and independent

Don't

  • Share state between tests
  • Rely on specific database IDs
  • Skip cleanup in afterAll
  • Hard-code timeouts (use Playwright's auto-wait)

Code Style and Comments

Comment Tags

Use these tags to highlight important information in code comments:

  • IMPORTANT: - Critical information that must not be overlooked
  • NOTE: - Helpful context or clarification
  • WARNING: - Potential pitfalls or dangerous operations
  • TODO: - Future work that should be done
  • FIXME: - Known issues that need fixing

Code Patterns

  • Use MUST / MUST NOT for hard requirements
  • Use NEVER / ALWAYS for absolute rules
  • Use AVOID for anti-patterns to stay away from
  • Use DO NOT for explicit prohibitions

Example

// IMPORTANT: This function must be called before any Convex operations
// NOTE: The widget uses Shadow DOM, so overlays must portal into the shadow root
// WARNING: Never fall back to wildcard "*" for CORS
// TODO: Add rate limiting to this endpoint
// FIXME: This cast should be removed after TS2589 is resolved

Modularity Patterns

Module Organization

  • Separate orchestration from rendering
  • Extract helper logic from page components
  • Use explicit domain modules instead of co-locating all logic
  • Preserve existing behavior when refactoring

Key Principles

  1. Single Responsibility: Each module should have one clear purpose
  2. Explicit Contracts: Modules must expose typed internal contracts
  3. Preserve Semantics: Refactoring must preserve existing behavior
  4. Shared Utilities: Common logic should be extracted to shared modules

Common Patterns

  • Controller/View Separation: Separate orchestration from rendering
  • Domain Modules: Group related functionality by domain
  • Adapter Pattern: Use adapters for external dependencies
  • Wrapper Hooks: Wrap external hooks with local adapters

Error Handling Patterns

Standard Error Functions

Use the standardized error functions from packages/convex/convex/utils/errors.ts:

  • throwNotFound(resourceType) - Resource not found
  • throwNotAuthenticated() - Authentication required
  • throwPermissionDenied(permission?) - Permission denied

Error Feedback

  • Use standardized non-blocking error feedback for frontend paths
  • Provide actionable user messaging
  • Centralize unknown error mapping for covered paths

Documentation Standards

Source of Truth

  • OpenSpec specs are the source of truth for requirements
  • docs/ contains reference documentation
  • AGENTS.md contains AI agent guardrails
  • Code comments provide inline guidance

When to Update Docs

  • When adding new features or changing behavior
  • When fixing bugs that affect user-facing behavior
  • When refactoring that changes module boundaries
  • When adding new patterns or conventions

Agent Handoff Notes

  • When converting a repo audit into OpenSpec artifacts, put only unfinished work into proposal.md, spec deltas, and tasks.md.
  • Explicitly call out already-finished adjacent work so a follow-up agent does not reopen it by mistake.
  • For the current Convex hardening area, the default out-of-scope items are:
    • sdk-core getRelevantKnowledge action routing
    • embedding batching/backfill concurrency in packages/convex/convex/embeddings.ts
  • If you change the covered hardening inventory or accepted exceptions, update the matching guard in the same change. Common files:
    • packages/convex/tests/runtimeTypeHardeningGuard.test.ts
    • packages/sdk-core/tests/refHardeningGuard.test.ts
  • When leaving work half-finished, record the remaining file clusters explicitly in openspec/changes/<change>/tasks.md so the next agent can resume without re-auditing the repo.

Tests

  • Convex targeted file:
    • pnpm --filter @opencom/convex test -- --run tests/<file>.test.ts
  • Convex full package tests:
    • pnpm --filter @opencom/convex test
  • Web unit tests:
    • pnpm --filter @opencom/web test
  • Web E2E (single file):
    • pnpm playwright test apps/web/e2e/<spec>.ts --project=chromium

E2E prep that is often required

  • Build/distribute widget before web E2E runs:
    • bash scripts/build-widget-for-tests.sh
  • If Convex-backed tests need env values loaded in shell:
    • bash -lc 'set -a; source packages/convex/.env.local; set +a; <your command>'

OpenSpec Workflow Cheatsheet

Check change status

  • openspec status --change "<change-name>" --json

Get apply context + progress

  • openspec instructions apply --change "<change-name>" --json

Validate before marking done

  • openspec validate <change-name> --strict --no-interactive

Important artifact dependency rule (spec-driven schema)

  • tasks.md can stay blocked until both design.md and specs/**/*.md are ready.
  • If status shows:
    • tasks: blocked
    • missingDeps: ["design", "specs"] this is expected; finish design/spec artifacts first.

Recommended Finish Checklist for Changes

  1. Implement scoped code changes.
  2. Run package-level typecheck(s).
  3. Run targeted tests for touched area.
  4. Run strict OpenSpec validation.
  5. Update openspec/changes/<change>/tasks.md checkboxes.
  6. Sync tracker in openspec/proposal-execution-plan.md when proposal status changes.

Skills / Slash Commands to Prefer

  • /opsx-apply — implement tasks for a change
  • /opsx-continue — advance artifact workflow
  • /opsx-verify — verify implementation vs artifacts
  • /opsx-archive — archive completed change

Use these when working within OpenSpec-driven requests to reduce setup time in fresh chats.

Warning: Running scripts inline causes the terminal to hang and crash. Create files and run them that way. Avoid running commmands like ... node - <<"NODE ..." or python3 - <<'PY' ...

Convex Type Safety Playbook

This is the canonical guide for adding or changing Convex-backed code in this repo.

Use it when you are:

  • adding a new Convex query, mutation, or action
  • calling one Convex function from another
  • wiring a new frontend feature to Convex
  • hitting TS2589 (Type instantiation is excessively deep and possibly infinite)
  • deciding where a cast or makeFunctionReference(...) is acceptable

Historical hardening notes still exist in openspec/archive/refactor-* and runtime-type-hardening-2026-03-05.md, but this file is the current source of truth for new code.

Goals

  • Keep runtime and UI code on explicit, local types.
  • Keep unavoidable Convex typing escape hatches small and centralized.
  • Prevent new generic string-ref factories, broad casts, and component-local ref creation.
  • Make it obvious which pattern to use for each call site.

Decision Table

Situation Preferred approach Where Why
Define a new public Convex query or mutation Export a normal Convex function with narrow v.* args and a narrow return shape packages/convex/convex/** Keeps the source contract explicit and reusable
Call Convex from web or widget UI/runtime code Use the local surface adapter plus a feature-local wrapper hook or fixed ref constant apps/web/src/**, apps/widget/src/** Keeps convex/react and ref typing out of runtime/UI modules
Call one Convex function from another and generated refs typecheck normally Use generated api.* / internal.* refs packages/convex/convex/** This is the default, simplest path
Call one Convex function from another and generated refs hit TS2589 Add a local shallow runQuery / runMutation / runAction / runAfter helper the hotspot file only Shrinks type instantiation at the call boundary
The generated ref itself still triggers TS2589 Replace only that hot ref with a fixed, typed makeFunctionReference("module:function") constant the hotspot file only Avoids broad weakening of the entire module
Convex React hook tuple typing still needs help Keep a tiny adapter-local helper/cast in the surface adapter adapter file only Localizes the last unavoidable boundary

Non-Negotiable Rules

1. Do not import convex/react in runtime or feature UI files

Use the surface adapter layer instead:

  • web: apps/web/src/lib/convex/hooks.ts
  • widget: apps/widget/src/lib/convex/hooks.ts
  • mobile: follow the same local-wrapper pattern; do not add new direct screen/context-level convex/react usage

Direct convex/react imports are only acceptable in:

  • explicit adapter files
  • bootstrap/provider wiring
  • targeted tests that intentionally mock the adapter boundary

The current hardening guards freeze these boundaries:

  • apps/web/src/app/typeHardeningGuard.test.ts
  • apps/widget/src/test/refHardeningGuard.test.ts
  • apps/mobile/src/typeHardeningGuard.test.ts
  • packages/react-native-sdk/tests/hookBoundaryGuard.test.ts

2. Do not create refs inside React components or hooks

Bad:

function WidgetPane() {
  const listRef = makeFunctionReference<"query", Args, Result>("messages:list");
  const data = useQuery(listRef, args);
}

Good:

const LIST_REF = widgetQueryRef<Args, Result>("messages:list");

function WidgetPane() {
  const data = useWidgetQuery(LIST_REF, args);
}

All refs must be module-scope constants.

3. Do not add new generic string-ref factories

Do not introduce helpers like:

  • getQueryRef(name: string)
  • getMutationRef(name: string)
  • getActionRef(name: string)

Those patterns weaken the type boundary and make review harder. Some older code still has them, but they are legacy, not the standard for new work.

Use named fixed refs instead:

const LIST_MESSAGES_REF = webQueryRef<ListMessagesArgs, MessageRecord[]>("messages:list");
const SEND_MESSAGE_REF = webMutationRef<SendMessageArgs, Id<"messages">>("messages:send");

4. Keep casts local, named, and justified

Allowed:

  • a tiny adapter-local cast needed to satisfy a Convex hook tuple type
  • a hotspot-local shallow helper for ctx.runQuery, ctx.runMutation, ctx.runAction, or ctx.scheduler.runAfter
  • a hotspot-local typed makeFunctionReference("module:function") when generated refs trigger TS2589

Not allowed for new code:

  • as any
  • broad unsafeApi / unsafeInternal object aliases in runtime code
  • repeated as unknown as across multiple call sites
  • hiding transport typing inside UI/controller modules

5. Update guard tests when you add or move a boundary

If you intentionally add a new approved boundary, document it in the relevant guard test at the same time.

Standard Patterns

A. Defining a new Convex query or mutation

Default backend pattern:

import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import type { Id } from "./_generated/dataModel";

type VisitorSummary = {
  _id: Id<"visitors">;
  name?: string;
  email?: string;
};

export const listByWorkspace = query({
  args: {
    workspaceId: v.id("workspaces"),
  },
  handler: async (ctx, args): Promise<VisitorSummary[]> => {
    const visitors = await ctx.db
      .query("visitors")
      .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId))
      .collect();

    return visitors.map((visitor) => ({
      _id: visitor._id,
      name: visitor.name,
      email: visitor.email,
    }));
  },
});

Rules:

  • Use narrow v.* validators.
  • Prefer explicit local return types for shared, frontend-facing, or cross-function contracts.
  • Convert untyped or broad data to a narrow shape before returning it.
  • If you need v.any(), document it in security/convex-v-any-arg-exceptions.json.

B. Consuming Convex from web or widget code

Runtime/UI files should consume feature-local wrappers or fixed refs through the local adapter.

Widget example:

import type { Id } from "@opencom/convex/dataModel";
import { useWidgetQuery, widgetQueryRef } from "../lib/convex/hooks";

type TicketRecord = {
  _id: Id<"tickets">;
  subject: string;
};

const VISITOR_TICKETS_REF = widgetQueryRef<
  { workspaceId: Id<"workspaces">; visitorId: Id<"visitors">; sessionToken: string },
  TicketRecord[]
>("tickets:listByVisitor");

export function useVisitorTickets(
  workspaceId: Id<"workspaces"> | undefined,
  visitorId: Id<"visitors"> | null,
  sessionToken: string | null
) {
  return useWidgetQuery(
    VISITOR_TICKETS_REF,
    workspaceId && visitorId && sessionToken ? { workspaceId, visitorId, sessionToken } : "skip"
  );
}

Use the same structure in web with webQueryRef, webMutationRef, webActionRef, useWebQuery, useWebMutation, and useWebAction.

Rules:

  • Define refs once at module scope.
  • Keep skip / gating logic in the wrapper where practical.
  • Export narrow feature-local result types instead of leaking giant inferred shapes.
  • Do not import convex/react directly in feature components, screens, contexts, or controller hooks.

C. Calling one Convex function from another

Preferred default: generated refs

Start here when the types are normal:

import { internal } from "./_generated/api";

await ctx.runMutation(internal.notifications.deliver, {
  conversationId,
});

This is the standard path until it hits a real TS2589 problem.

TS2589 fallback: shallow helper first

If ctx.runQuery(...), ctx.runMutation(...), ctx.runAction(...), or ctx.scheduler.runAfter(...) causes deep-instantiation errors, add a local helper:

import { type FunctionReference } from "convex/server";

type ConvexRef<
  Type extends "query" | "mutation" | "action",
  Visibility extends "internal" | "public",
  Args extends Record<string, unknown>,
  Return = unknown,
> = FunctionReference<Type, Visibility, Args, Return>;

function getShallowRunMutation(ctx: { runMutation: unknown }) {
  return ctx.runMutation as unknown as <
    Visibility extends "internal" | "public",
    Args extends Record<string, unknown>,
    Return,
  >(
    mutationRef: ConvexRef<"mutation", Visibility, Args, Return>,
    mutationArgs: Args
  ) => Promise<Return>;
}

Then call through the helper:

const runMutation = getShallowRunMutation(ctx);
await runMutation(internal.notifications.deliver, { conversationId });

TS2589 fallback: fixed typed ref when the generated ref itself is the problem

If simply referencing api.foo.bar or internal.foo.bar still triggers TS2589, switch only that hot call site to a fixed typed ref:

import { makeFunctionReference, type FunctionReference } from "convex/server";

type DeliverArgs = { conversationId: Id<"conversations"> };
type DeliverResult = null;

type ConvexRef<
  Type extends "query" | "mutation" | "action",
  Visibility extends "internal" | "public",
  Args extends Record<string, unknown>,
  Return = unknown,
> = FunctionReference<Type, Visibility, Args, Return>;

const DELIVER_NOTIFICATION_REF = makeFunctionReference<"mutation", DeliverArgs, DeliverResult>(
  "notifications:deliver"
) as unknown as ConvexRef<"mutation", "internal", DeliverArgs, DeliverResult>;

Use this only after the generated ref path proved pathological.

Which approach to choose

Use generated api / internal refs when

  • the call is backend-to-backend
  • the generated ref typechecks normally
  • you are not in a known TS2589 hotspot

Use fixed typed makeFunctionReference(...) constants when

  • you are in a surface adapter or feature-local wrapper file
  • you need a stable local ref for a frontend wrapper
  • a backend hotspot still blows up after trying generated refs

Use local wrapper hooks when

  • the consumer is React UI, runtime, controller, screen, or context code
  • the feature needs gating or skip behavior
  • you want to normalize the result shape once for multiple consumers

Use a shallow backend helper when

  • the problem is ctx.runQuery / ctx.runMutation / ctx.runAction / runAfter
  • the ref type is okay, but the invocation is too deep

Use an adapter-local cast only when

  • Convex’s React hook typing still needs an exact tuple or helper shape
  • the cast can stay in the adapter file and nowhere else

Current Surface Standards

Backend (packages/convex)

  • Default to generated refs.
  • Localize TS2589 workarounds with named getShallowRun* helpers.
  • If needed, use fixed typed refs at the hotspot only.
  • Keep guard coverage in packages/convex/tests/runtimeTypeHardeningGuard.test.ts.

Web (apps/web)

  • Feature/runtime code should not import convex/react directly.
  • Use feature-local wrapper hooks and the web adapter in apps/web/src/lib/convex/hooks.ts.
  • Keep refs at module scope.
  • Guard coverage lives in apps/web/src/app/typeHardeningGuard.test.ts.

Widget (apps/widget)

  • Feature/runtime code should not import convex/react directly.
  • Use the widget adapter in apps/widget/src/lib/convex/hooks.ts.
  • The only remaining adapter escape hatch is the query-args tuple helper required by Convex’s hook typing.
  • Guard coverage lives in apps/widget/src/test/refHardeningGuard.test.ts.

Mobile (apps/mobile)

  • Target the same pattern as web/widget: local wrapper hooks plus module-scope typed refs.
  • Do not add new direct convex/react usage to screens, contexts, or controller-style hooks.
  • If a local adapter/wrapper does not exist for the feature yet, create one instead of importing hooks directly into runtime UI.
  • Guard coverage lives in apps/mobile/src/typeHardeningGuard.test.ts.

Anti-Patterns To Avoid

  • function getQueryRef(name: string) { ... }
  • function getMutationRef(name: string) { ... }
  • function getActionRef(name: string) { ... }
  • component-local makeFunctionReference(...)
  • as any
  • broad unsafeApi / unsafeInternal aliases
  • scattering the same as unknown as across many call sites
  • returning unknown or string when a branded Id<"..."> or explicit object type is the real contract

Verification Checklist

After changing Convex typing boundaries:

  1. Run the touched package typecheck first.
  2. Run the touched package tests.
  3. Run the relevant hardening guard tests.
  4. If this work is OpenSpec-driven, run strict OpenSpec validation.

Useful commands:

pnpm --filter @opencom/convex typecheck
pnpm --filter @opencom/web typecheck
pnpm --filter @opencom/widget typecheck

pnpm --filter @opencom/convex test -- --run tests/runtimeTypeHardeningGuard.test.ts
pnpm --filter @opencom/web test -- --run src/app/typeHardeningGuard.test.ts
pnpm --filter @opencom/widget test -- --run src/test/refHardeningGuard.test.ts
pnpm exec vitest run --config apps/mobile/vitest.config.ts apps/mobile/src/typeHardeningGuard.test.ts

Review Rule of Thumb

If you are about to:

  • add a new direct convex/react import in feature code
  • add a new get*Ref(name: string) factory
  • add unsafeApi, unsafeInternal, as any, or repeated as unknown as
  • create a ref inside a React component

stop and use one of the standard patterns above instead.

This project uses Convex as its backend.

When working on Convex code, always read convex/_generated/ai/guidelines.md first for important guidelines on how to correctly use Convex APIs and patterns. The file contains rules that override what you may have learned about Convex from training data.

Convex agent skills for common tasks can be installed by running npx convex ai-files install.