paperclipai/paperclip · error · Error

Paperclip source scope must specify either projectId or root

Error message

Paperclip source scope must specify either projectId or rootIssueId, not both.

What it means

Thrown by assertPaperclipSourceScopePayload() when a Paperclip source scope payload specifies BOTH projectId and rootIssueId. A source scope is exclusive: it scopes either to a single project (selected_projects) or to a set of root issues (root_issues), never both. The guard rejects ambiguous payloads before they are normalized.

Source

Thrown at packages/plugins/plugin-llm-wiki/src/wiki/core.ts:481

  const space = await resolveSpace(ctx, {
    companyId: input.companyId,
    wikiId: input.wikiId,
    spaceSlug: input.spaceSlug,
  });
  const profile = await profileForSpace(ctx, input.companyId, space);
  const decision = evaluatePaperclipProfilePolicy({
    space,
    profile,
    purpose,
    requireEnabledProfile: options.requireEnabledProfile,
  });
  if (!decision.allowed) throw new Error(decision.message);
  return decision.space;
}

function assertPaperclipSourceScopePayload(input: { projectId?: string | null; rootIssueId?: string | null }) {
  if (input.projectId && input.rootIssueId) {
    throw new Error("Paperclip source scope must specify either projectId or rootIssueId, not both.");
  }
}

function assertRequestedCharacterLimit(name: string, value: unknown, max: number) {
  if (value == null) return;
  if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
    throw new Error(`${name} must be a positive number.`);
  }
  if (Math.floor(value) > max) {
    throw new Error(`${name} exceeds the hard Paperclip ingestion cap of ${max} characters.`);
  }
}

function stableSpaceId(input: { companyId: string; wikiId: string; slug: string }): string {
  const hex = createHash("md5")
    .update(`${input.companyId}:${input.wikiId}:${input.slug}`)
    .digest("hex");
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Set exactly one of projectId or rootIssueId on the scope payload — leave the other null/undefined.
  2. In the UI, make the project and root-issue selectors mutually exclusive.
  3. Validate the payload with a schema that uses oneOf/exclusiveProperties before sending.

Example fix

// before
const scope = { projectId: "p1", rootIssueId: "i1" }; // throws
// after
const scope = { projectId: "p1" }; // project-scoped
Defensive patterns

Strategy: validation

Validate before calling

function assertScopePayload(p: { projectId?: string | null; rootIssueId?: string | null }) {
  if (p.projectId && p.rootIssueId) {
    throw new Error("Specify either projectId or rootIssueId, not both.");
  }
}

Type guard

function isExclusiveScopePayload(p: { projectId?: string | null; rootIssueId?: string | null }): boolean {
  return !(p.projectId && p.rootIssueId);
}

Prevention

When it happens

Trigger: Submitting a source scope payload with both fields populated (e.g. { projectId: "p1", rootIssueId: "i1" }). API client that builds the payload by merging two partial configs.

Common situations: Frontend form allowing both selectors to be filled. Migration code that unioned fields from two legacy shapes. Confused API consumer assuming both are required.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/8aac09a586212071. Report an issue: GitHub.