Improve onboarding defaults and issue goal fallback

This commit is contained in:
Dotta
2026-03-12 08:50:31 -05:00
parent 5f3f354b3a
commit 448e9c192b
9 changed files with 378 additions and 77 deletions

View File

@@ -1,7 +1,47 @@
import { eq } from "drizzle-orm";
import { and, asc, eq, isNull } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { goals } from "@paperclipai/db";
type GoalReader = Pick<Db, "select">;
export async function getDefaultCompanyGoal(db: GoalReader, companyId: string) {
const activeRootGoal = await db
.select()
.from(goals)
.where(
and(
eq(goals.companyId, companyId),
eq(goals.level, "company"),
eq(goals.status, "active"),
isNull(goals.parentId),
),
)
.orderBy(asc(goals.createdAt))
.then((rows) => rows[0] ?? null);
if (activeRootGoal) return activeRootGoal;
const anyRootGoal = await db
.select()
.from(goals)
.where(
and(
eq(goals.companyId, companyId),
eq(goals.level, "company"),
isNull(goals.parentId),
),
)
.orderBy(asc(goals.createdAt))
.then((rows) => rows[0] ?? null);
if (anyRootGoal) return anyRootGoal;
return db
.select()
.from(goals)
.where(and(eq(goals.companyId, companyId), eq(goals.level, "company")))
.orderBy(asc(goals.createdAt))
.then((rows) => rows[0] ?? null);
}
export function goalService(db: Db) {
return {
list: (companyId: string) => db.select().from(goals).where(eq(goals.companyId, companyId)),
@@ -13,6 +53,8 @@ export function goalService(db: Db) {
.where(eq(goals.id, id))
.then((rows) => rows[0] ?? null),
getDefaultCompanyGoal: (companyId: string) => getDefaultCompanyGoal(db, companyId),
create: (companyId: string, data: Omit<typeof goals.$inferInsert, "companyId">) =>
db
.insert(goals)

View File

@@ -0,0 +1,30 @@
type MaybeId = string | null | undefined;
export function resolveIssueGoalId(input: {
projectId: MaybeId;
goalId: MaybeId;
defaultGoalId: MaybeId;
}): string | null {
if (!input.projectId && !input.goalId) {
return input.defaultGoalId ?? null;
}
return input.goalId ?? null;
}
export function resolveNextIssueGoalId(input: {
currentProjectId: MaybeId;
currentGoalId: MaybeId;
projectId?: MaybeId;
goalId?: MaybeId;
defaultGoalId: MaybeId;
}): string | null {
const projectId =
input.projectId !== undefined ? input.projectId : input.currentProjectId;
const goalId =
input.goalId !== undefined ? input.goalId : input.currentGoalId;
if (!projectId && !goalId) {
return input.defaultGoalId ?? null;
}
return goalId ?? null;
}

View File

@@ -23,6 +23,8 @@ import {
parseProjectExecutionWorkspacePolicy,
} from "./execution-workspace-policy.js";
import { redactCurrentUserText } from "../log-redaction.js";
import { resolveIssueGoalId, resolveNextIssueGoalId } from "./issue-goal-fallback.js";
import { getDefaultCompanyGoal } from "./goals.js";
const ALL_ISSUE_STATUSES = ["backlog", "todo", "in_progress", "in_review", "blocked", "done", "cancelled"];
@@ -649,6 +651,7 @@ export function issueService(db: Db) {
throw unprocessable("in_progress issues require an assignee");
}
return db.transaction(async (tx) => {
const defaultCompanyGoal = await getDefaultCompanyGoal(tx, companyId);
let executionWorkspaceSettings =
(issueData.executionWorkspaceSettings as Record<string, unknown> | null | undefined) ?? null;
if (executionWorkspaceSettings == null && issueData.projectId) {
@@ -673,6 +676,11 @@ export function issueService(db: Db) {
const values = {
...issueData,
goalId: resolveIssueGoalId({
projectId: issueData.projectId,
goalId: issueData.goalId,
defaultGoalId: defaultCompanyGoal?.id ?? null,
}),
...(executionWorkspaceSettings ? { executionWorkspaceSettings } : {}),
companyId,
issueNumber,
@@ -752,6 +760,14 @@ export function issueService(db: Db) {
}
return db.transaction(async (tx) => {
const defaultCompanyGoal = await getDefaultCompanyGoal(tx, existing.companyId);
patch.goalId = resolveNextIssueGoalId({
currentProjectId: existing.projectId,
currentGoalId: existing.goalId,
projectId: issueData.projectId,
goalId: issueData.goalId,
defaultGoalId: defaultCompanyGoal?.id ?? null,
});
const updated = await tx
.update(issues)
.set(patch)