@plandesk/api 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/events.d.ts +6 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/routes/agent-runs.d.ts +1 -1
- package/dist/routes/agent-runs.js +20 -0
- package/dist/routes/goals.d.ts +4 -0
- package/dist/routes/goals.js +138 -0
- package/dist/routes/projects.d.ts +1 -1
- package/dist/routes/projects.js +12 -1
- package/dist/routes/submissions.d.ts +5 -0
- package/dist/routes/submissions.js +58 -0
- package/dist/serialize.d.ts +22 -1
- package/dist/serialize.js +35 -0
- package/dist/server.js +5 -1
- package/dist/services/canvas.d.ts +2 -0
- package/dist/services/canvas.js +2 -1
- package/dist/services/goals.d.ts +239 -0
- package/dist/services/goals.js +323 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +3 -0
- package/dist/services/projects.js +4 -1
- package/dist/services/sync.d.ts +5 -3
- package/dist/services/sync.js +45 -2
- package/dist/services/tasks.d.ts +10 -1
- package/dist/services/tasks.js +33 -5
- package/package.json +2 -2
- package/web/assets/index-DcD7-VGi.css +1 -0
- package/web/assets/index-Dcaq3wR-.js +318 -0
- package/web/index.html +2 -2
- package/web/assets/index-CUxSE_WZ.css +0 -1
- package/web/assets/index-DSmNB4RA.js +0 -235
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { type Db, type GoalStatus } from '@plandesk/db';
|
|
2
|
+
import type { EventBus } from '../events.js';
|
|
3
|
+
export type GoalServiceDeps = {
|
|
4
|
+
db: Db;
|
|
5
|
+
eventBus: EventBus;
|
|
6
|
+
};
|
|
7
|
+
export declare class InvalidGoalTransitionError extends Error {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class GoalCompletionBlockedError extends Error {
|
|
11
|
+
incompleteTaskIds: string[];
|
|
12
|
+
constructor(incompleteTaskIds: string[]);
|
|
13
|
+
}
|
|
14
|
+
export declare class InvalidVerificationSurfaceError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class GoalVerificationRequiredError extends Error {
|
|
18
|
+
requiredKind: VerificationSurfaceKind;
|
|
19
|
+
constructor(requiredKind: VerificationSurfaceKind);
|
|
20
|
+
}
|
|
21
|
+
export type VerificationSurfaceKind = 'gate_command' | 'acceptance_checklist' | 'human_sign_off';
|
|
22
|
+
export type GateCommandSurface = {
|
|
23
|
+
kind: 'gate_command';
|
|
24
|
+
command: string;
|
|
25
|
+
};
|
|
26
|
+
export type AcceptanceChecklistSurface = {
|
|
27
|
+
kind: 'acceptance_checklist';
|
|
28
|
+
items: Array<{
|
|
29
|
+
criterion: string;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
export type HumanSignOffSurface = {
|
|
33
|
+
kind: 'human_sign_off';
|
|
34
|
+
};
|
|
35
|
+
export type VerificationSurface = GateCommandSurface | AcceptanceChecklistSurface | HumanSignOffSurface;
|
|
36
|
+
export type GateCommandEvidence = {
|
|
37
|
+
kind: 'gate_command';
|
|
38
|
+
exit_code: number;
|
|
39
|
+
command?: string;
|
|
40
|
+
detail?: string;
|
|
41
|
+
};
|
|
42
|
+
export type AcceptanceChecklistEvidence = {
|
|
43
|
+
kind: 'acceptance_checklist';
|
|
44
|
+
checked: string[];
|
|
45
|
+
};
|
|
46
|
+
export type HumanSignOffEvidence = {
|
|
47
|
+
kind: 'human_sign_off';
|
|
48
|
+
approved_by: string;
|
|
49
|
+
};
|
|
50
|
+
export type VerificationEvidence = GateCommandEvidence | AcceptanceChecklistEvidence | HumanSignOffEvidence;
|
|
51
|
+
export type LastVerificationRecord = {
|
|
52
|
+
at: string;
|
|
53
|
+
green: boolean;
|
|
54
|
+
kind: VerificationSurfaceKind | null;
|
|
55
|
+
detail?: string;
|
|
56
|
+
};
|
|
57
|
+
export type CreateGoalInput = {
|
|
58
|
+
objective: string;
|
|
59
|
+
verificationSurface?: string | null;
|
|
60
|
+
constraints?: string | null;
|
|
61
|
+
boundaries?: string | null;
|
|
62
|
+
iterationPolicy?: string | null;
|
|
63
|
+
stopCondition?: string | null;
|
|
64
|
+
budget?: string | null;
|
|
65
|
+
status?: GoalStatus;
|
|
66
|
+
};
|
|
67
|
+
export type UpdateGoalInput = {
|
|
68
|
+
objective?: string;
|
|
69
|
+
verificationSurface?: string | null;
|
|
70
|
+
constraints?: string | null;
|
|
71
|
+
boundaries?: string | null;
|
|
72
|
+
iterationPolicy?: string | null;
|
|
73
|
+
stopCondition?: string | null;
|
|
74
|
+
budget?: string | null;
|
|
75
|
+
};
|
|
76
|
+
export declare function parseVerificationSurface(raw: string | null): VerificationSurface | null;
|
|
77
|
+
export declare function evaluateEvidence(surface: VerificationSurface, evidence: VerificationEvidence): {
|
|
78
|
+
green: boolean;
|
|
79
|
+
detail?: string;
|
|
80
|
+
};
|
|
81
|
+
export declare function createGoalService(deps: GoalServiceDeps): {
|
|
82
|
+
create(projectId: string, input: CreateGoalInput): {
|
|
83
|
+
id: string;
|
|
84
|
+
project_id: string;
|
|
85
|
+
objective: string;
|
|
86
|
+
status: "active" | "paused" | "complete" | "blocked";
|
|
87
|
+
verification_surface: string | null;
|
|
88
|
+
constraints: string | null;
|
|
89
|
+
boundaries: string | null;
|
|
90
|
+
iteration_policy: string | null;
|
|
91
|
+
stop_condition: string | null;
|
|
92
|
+
budget: string | null;
|
|
93
|
+
last_verification: {
|
|
94
|
+
at: string;
|
|
95
|
+
green: boolean;
|
|
96
|
+
kind: string | null;
|
|
97
|
+
detail?: string;
|
|
98
|
+
} | null;
|
|
99
|
+
created_at: string;
|
|
100
|
+
updated_at: string;
|
|
101
|
+
} | undefined;
|
|
102
|
+
get(goalId: string): {
|
|
103
|
+
cycle_tasks: {
|
|
104
|
+
tags?: import("../serialize.js").SerializedTag[] | undefined;
|
|
105
|
+
id: string;
|
|
106
|
+
project_id: string;
|
|
107
|
+
goal_id: string;
|
|
108
|
+
label: string;
|
|
109
|
+
status: "scope" | "todo" | "in_progress" | "done" | "backlog";
|
|
110
|
+
description: string | null;
|
|
111
|
+
x: number;
|
|
112
|
+
y: number;
|
|
113
|
+
assignee: string | null;
|
|
114
|
+
due_date: string | null;
|
|
115
|
+
created_at: string;
|
|
116
|
+
updated_at: string;
|
|
117
|
+
}[];
|
|
118
|
+
id: string;
|
|
119
|
+
project_id: string;
|
|
120
|
+
objective: string;
|
|
121
|
+
status: "active" | "paused" | "complete" | "blocked";
|
|
122
|
+
verification_surface: string | null;
|
|
123
|
+
constraints: string | null;
|
|
124
|
+
boundaries: string | null;
|
|
125
|
+
iteration_policy: string | null;
|
|
126
|
+
stop_condition: string | null;
|
|
127
|
+
budget: string | null;
|
|
128
|
+
last_verification: {
|
|
129
|
+
at: string;
|
|
130
|
+
green: boolean;
|
|
131
|
+
kind: string | null;
|
|
132
|
+
detail?: string;
|
|
133
|
+
} | null;
|
|
134
|
+
created_at: string;
|
|
135
|
+
updated_at: string;
|
|
136
|
+
} | undefined;
|
|
137
|
+
listByProject(projectId: string): {
|
|
138
|
+
id: string;
|
|
139
|
+
project_id: string;
|
|
140
|
+
objective: string;
|
|
141
|
+
status: "active" | "paused" | "complete" | "blocked";
|
|
142
|
+
verification_surface: string | null;
|
|
143
|
+
constraints: string | null;
|
|
144
|
+
boundaries: string | null;
|
|
145
|
+
iteration_policy: string | null;
|
|
146
|
+
stop_condition: string | null;
|
|
147
|
+
budget: string | null;
|
|
148
|
+
last_verification: {
|
|
149
|
+
at: string;
|
|
150
|
+
green: boolean;
|
|
151
|
+
kind: string | null;
|
|
152
|
+
detail?: string;
|
|
153
|
+
} | null;
|
|
154
|
+
created_at: string;
|
|
155
|
+
updated_at: string;
|
|
156
|
+
}[] | undefined;
|
|
157
|
+
update(goalId: string, input: UpdateGoalInput): {
|
|
158
|
+
id: string;
|
|
159
|
+
project_id: string;
|
|
160
|
+
objective: string;
|
|
161
|
+
status: "active" | "paused" | "complete" | "blocked";
|
|
162
|
+
verification_surface: string | null;
|
|
163
|
+
constraints: string | null;
|
|
164
|
+
boundaries: string | null;
|
|
165
|
+
iteration_policy: string | null;
|
|
166
|
+
stop_condition: string | null;
|
|
167
|
+
budget: string | null;
|
|
168
|
+
last_verification: {
|
|
169
|
+
at: string;
|
|
170
|
+
green: boolean;
|
|
171
|
+
kind: string | null;
|
|
172
|
+
detail?: string;
|
|
173
|
+
} | null;
|
|
174
|
+
created_at: string;
|
|
175
|
+
updated_at: string;
|
|
176
|
+
} | undefined;
|
|
177
|
+
pause(goalId: string): {
|
|
178
|
+
id: string;
|
|
179
|
+
project_id: string;
|
|
180
|
+
objective: string;
|
|
181
|
+
status: "active" | "paused" | "complete" | "blocked";
|
|
182
|
+
verification_surface: string | null;
|
|
183
|
+
constraints: string | null;
|
|
184
|
+
boundaries: string | null;
|
|
185
|
+
iteration_policy: string | null;
|
|
186
|
+
stop_condition: string | null;
|
|
187
|
+
budget: string | null;
|
|
188
|
+
last_verification: {
|
|
189
|
+
at: string;
|
|
190
|
+
green: boolean;
|
|
191
|
+
kind: string | null;
|
|
192
|
+
detail?: string;
|
|
193
|
+
} | null;
|
|
194
|
+
created_at: string;
|
|
195
|
+
updated_at: string;
|
|
196
|
+
} | undefined;
|
|
197
|
+
resume(goalId: string): {
|
|
198
|
+
id: string;
|
|
199
|
+
project_id: string;
|
|
200
|
+
objective: string;
|
|
201
|
+
status: "active" | "paused" | "complete" | "blocked";
|
|
202
|
+
verification_surface: string | null;
|
|
203
|
+
constraints: string | null;
|
|
204
|
+
boundaries: string | null;
|
|
205
|
+
iteration_policy: string | null;
|
|
206
|
+
stop_condition: string | null;
|
|
207
|
+
budget: string | null;
|
|
208
|
+
last_verification: {
|
|
209
|
+
at: string;
|
|
210
|
+
green: boolean;
|
|
211
|
+
kind: string | null;
|
|
212
|
+
detail?: string;
|
|
213
|
+
} | null;
|
|
214
|
+
created_at: string;
|
|
215
|
+
updated_at: string;
|
|
216
|
+
} | undefined;
|
|
217
|
+
complete(goalId: string, evidence?: VerificationEvidence): {
|
|
218
|
+
id: string;
|
|
219
|
+
project_id: string;
|
|
220
|
+
objective: string;
|
|
221
|
+
status: "active" | "paused" | "complete" | "blocked";
|
|
222
|
+
verification_surface: string | null;
|
|
223
|
+
constraints: string | null;
|
|
224
|
+
boundaries: string | null;
|
|
225
|
+
iteration_policy: string | null;
|
|
226
|
+
stop_condition: string | null;
|
|
227
|
+
budget: string | null;
|
|
228
|
+
last_verification: {
|
|
229
|
+
at: string;
|
|
230
|
+
green: boolean;
|
|
231
|
+
kind: string | null;
|
|
232
|
+
detail?: string;
|
|
233
|
+
} | null;
|
|
234
|
+
created_at: string;
|
|
235
|
+
updated_at: string;
|
|
236
|
+
} | undefined;
|
|
237
|
+
};
|
|
238
|
+
export type GoalService = ReturnType<typeof createGoalService>;
|
|
239
|
+
//# sourceMappingURL=goals.d.ts.map
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { createGoal, createTask, getGoal, getProject, InvalidGoalStatusError, isGoalStatus, listGoals, listTagsByTaskForProject, listTasks, updateGoal, updateGoalStatus, updateTask, } from '@plandesk/db';
|
|
2
|
+
import { serializeGoal, serializeTask } from '../serialize.js';
|
|
3
|
+
export class InvalidGoalTransitionError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'InvalidGoalTransitionError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class GoalCompletionBlockedError extends Error {
|
|
10
|
+
incompleteTaskIds;
|
|
11
|
+
constructor(incompleteTaskIds) {
|
|
12
|
+
super('Goal cannot be completed until all cycle-tasks are done');
|
|
13
|
+
this.name = 'GoalCompletionBlockedError';
|
|
14
|
+
this.incompleteTaskIds = incompleteTaskIds;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class InvalidVerificationSurfaceError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'InvalidVerificationSurfaceError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class GoalVerificationRequiredError extends Error {
|
|
24
|
+
requiredKind;
|
|
25
|
+
constructor(requiredKind) {
|
|
26
|
+
super(`Verification evidence required for surface kind: ${requiredKind}`);
|
|
27
|
+
this.name = 'GoalVerificationRequiredError';
|
|
28
|
+
this.requiredKind = requiredKind;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function isRecord(value) {
|
|
32
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function isNonEmptyString(value) {
|
|
35
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
36
|
+
}
|
|
37
|
+
export function parseVerificationSurface(raw) {
|
|
38
|
+
if (raw === null) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(raw);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new InvalidVerificationSurfaceError('verification_surface must be valid JSON');
|
|
47
|
+
}
|
|
48
|
+
if (!isRecord(parsed) || typeof parsed.kind !== 'string') {
|
|
49
|
+
throw new InvalidVerificationSurfaceError('verification_surface must include a kind');
|
|
50
|
+
}
|
|
51
|
+
switch (parsed.kind) {
|
|
52
|
+
case 'gate_command': {
|
|
53
|
+
if (!isNonEmptyString(parsed.command)) {
|
|
54
|
+
throw new InvalidVerificationSurfaceError('gate_command requires a command');
|
|
55
|
+
}
|
|
56
|
+
return { kind: 'gate_command', command: parsed.command };
|
|
57
|
+
}
|
|
58
|
+
case 'acceptance_checklist': {
|
|
59
|
+
if (!Array.isArray(parsed.items) || parsed.items.length === 0) {
|
|
60
|
+
throw new InvalidVerificationSurfaceError('acceptance_checklist requires non-empty items');
|
|
61
|
+
}
|
|
62
|
+
const items = [];
|
|
63
|
+
for (const item of parsed.items) {
|
|
64
|
+
if (!isRecord(item) || !isNonEmptyString(item.criterion)) {
|
|
65
|
+
throw new InvalidVerificationSurfaceError('acceptance_checklist items require a criterion');
|
|
66
|
+
}
|
|
67
|
+
items.push({ criterion: item.criterion });
|
|
68
|
+
}
|
|
69
|
+
return { kind: 'acceptance_checklist', items };
|
|
70
|
+
}
|
|
71
|
+
case 'human_sign_off':
|
|
72
|
+
return { kind: 'human_sign_off' };
|
|
73
|
+
default:
|
|
74
|
+
throw new InvalidVerificationSurfaceError(`Unknown verification_surface kind: ${parsed.kind}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function evaluateEvidence(surface, evidence) {
|
|
78
|
+
if (surface.kind !== evidence.kind) {
|
|
79
|
+
return { green: false, detail: `Expected evidence kind ${surface.kind}` };
|
|
80
|
+
}
|
|
81
|
+
if (surface.kind === 'gate_command' && evidence.kind === 'gate_command') {
|
|
82
|
+
if (evidence.exit_code === 0) {
|
|
83
|
+
return { green: true };
|
|
84
|
+
}
|
|
85
|
+
const detail = evidence.detail ??
|
|
86
|
+
`exit_code ${String(evidence.exit_code)}${evidence.command ? ` for \`${evidence.command}\`` : ''}`;
|
|
87
|
+
return { green: false, detail };
|
|
88
|
+
}
|
|
89
|
+
if (surface.kind === 'acceptance_checklist' && evidence.kind === 'acceptance_checklist') {
|
|
90
|
+
const missing = surface.items
|
|
91
|
+
.map((item) => item.criterion)
|
|
92
|
+
.filter((criterion) => !evidence.checked.includes(criterion));
|
|
93
|
+
if (missing.length === 0) {
|
|
94
|
+
return { green: true };
|
|
95
|
+
}
|
|
96
|
+
return { green: false, detail: `Missing criteria: ${missing.join(', ')}` };
|
|
97
|
+
}
|
|
98
|
+
if (surface.kind === 'human_sign_off' && evidence.kind === 'human_sign_off') {
|
|
99
|
+
if (isNonEmptyString(evidence.approved_by)) {
|
|
100
|
+
return { green: true };
|
|
101
|
+
}
|
|
102
|
+
return { green: false, detail: 'approved_by is required' };
|
|
103
|
+
}
|
|
104
|
+
return { green: false, detail: `Expected evidence kind ${surface.kind}` };
|
|
105
|
+
}
|
|
106
|
+
function validateVerificationSurfaceInput(raw) {
|
|
107
|
+
if (raw === undefined) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
parseVerificationSurface(raw);
|
|
111
|
+
}
|
|
112
|
+
function acceptanceBlockMarker(goalId) {
|
|
113
|
+
return `Acceptance-block-for: ${goalId}`;
|
|
114
|
+
}
|
|
115
|
+
function hasUnresolvedBlockingTask(db, projectId, goalId) {
|
|
116
|
+
const marker = acceptanceBlockMarker(goalId);
|
|
117
|
+
return listTasks(db, projectId).some((task) => task.goalId === goalId && task.status === 'scope' && task.description?.includes(marker));
|
|
118
|
+
}
|
|
119
|
+
// When acceptance finally passes, the remediation task filed on the earlier red
|
|
120
|
+
// result is obsolete — resolve it so a completed goal never shows open work.
|
|
121
|
+
function resolveBlockingTasks(db, projectId, goalId) {
|
|
122
|
+
const marker = acceptanceBlockMarker(goalId);
|
|
123
|
+
for (const task of listTasks(db, projectId)) {
|
|
124
|
+
if (task.goalId === goalId && task.status !== 'done' && task.description?.includes(marker)) {
|
|
125
|
+
updateTask(db, task.id, { status: 'done' });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function truncateObjective(objective, maxLength = 80) {
|
|
130
|
+
if (objective.length <= maxLength) {
|
|
131
|
+
return objective;
|
|
132
|
+
}
|
|
133
|
+
return `${objective.slice(0, maxLength - 1)}…`;
|
|
134
|
+
}
|
|
135
|
+
function buildBlockingTaskDescription(goalId, surfaceKind, detail) {
|
|
136
|
+
const lines = [acceptanceBlockMarker(goalId), '', `Surface kind: ${surfaceKind}`];
|
|
137
|
+
if (detail) {
|
|
138
|
+
lines.push(`Detail: ${detail}`);
|
|
139
|
+
}
|
|
140
|
+
return lines.join('\n');
|
|
141
|
+
}
|
|
142
|
+
function cycleTasksForGoal(db, projectId, goalId) {
|
|
143
|
+
const tagsByTask = listTagsByTaskForProject(db, projectId);
|
|
144
|
+
return listTasks(db, projectId)
|
|
145
|
+
.filter((task) => task.goalId === goalId)
|
|
146
|
+
.map((task) => serializeTask(task, tagsByTask.get(task.id) ?? []));
|
|
147
|
+
}
|
|
148
|
+
function emitGoalUpdated(eventBus, goalId, projectId) {
|
|
149
|
+
eventBus.emit({ type: 'goal_updated', goalId, projectId });
|
|
150
|
+
}
|
|
151
|
+
function recordLastVerification(at, green, kind, detail) {
|
|
152
|
+
const record = { at, green, kind };
|
|
153
|
+
if (detail) {
|
|
154
|
+
record.detail = detail;
|
|
155
|
+
}
|
|
156
|
+
return JSON.stringify(record);
|
|
157
|
+
}
|
|
158
|
+
export function createGoalService(deps) {
|
|
159
|
+
const { db, eventBus } = deps;
|
|
160
|
+
return {
|
|
161
|
+
create(projectId, input) {
|
|
162
|
+
if (input.status !== undefined && !isGoalStatus(input.status)) {
|
|
163
|
+
throw new InvalidGoalStatusError(input.status);
|
|
164
|
+
}
|
|
165
|
+
validateVerificationSurfaceInput(input.verificationSurface);
|
|
166
|
+
const project = getProject(db, projectId);
|
|
167
|
+
if (!project) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const goal = db.transaction((tx) => createGoal(tx, {
|
|
171
|
+
projectId,
|
|
172
|
+
objective: input.objective,
|
|
173
|
+
status: input.status,
|
|
174
|
+
verificationSurface: input.verificationSurface,
|
|
175
|
+
constraints: input.constraints,
|
|
176
|
+
boundaries: input.boundaries,
|
|
177
|
+
iterationPolicy: input.iterationPolicy,
|
|
178
|
+
stopCondition: input.stopCondition,
|
|
179
|
+
budget: input.budget,
|
|
180
|
+
}));
|
|
181
|
+
emitGoalUpdated(eventBus, goal.id, projectId);
|
|
182
|
+
return serializeGoal(goal);
|
|
183
|
+
},
|
|
184
|
+
get(goalId) {
|
|
185
|
+
const goal = getGoal(db, goalId);
|
|
186
|
+
if (!goal) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
...serializeGoal(goal),
|
|
191
|
+
cycle_tasks: cycleTasksForGoal(db, goal.projectId, goalId),
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
listByProject(projectId) {
|
|
195
|
+
const project = getProject(db, projectId);
|
|
196
|
+
if (!project) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
return listGoals(db, projectId).map(serializeGoal);
|
|
200
|
+
},
|
|
201
|
+
update(goalId, input) {
|
|
202
|
+
const existing = getGoal(db, goalId);
|
|
203
|
+
if (!existing) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
validateVerificationSurfaceInput(input.verificationSurface);
|
|
207
|
+
const goal = db.transaction((tx) => updateGoal(tx, goalId, input));
|
|
208
|
+
if (!goal) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
emitGoalUpdated(eventBus, goalId, existing.projectId);
|
|
212
|
+
return serializeGoal(goal);
|
|
213
|
+
},
|
|
214
|
+
pause(goalId) {
|
|
215
|
+
const existing = getGoal(db, goalId);
|
|
216
|
+
if (!existing) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
if (existing.status !== 'active') {
|
|
220
|
+
throw new InvalidGoalTransitionError('Goal can only be paused from active status');
|
|
221
|
+
}
|
|
222
|
+
const goal = db.transaction((tx) => updateGoalStatus(tx, goalId, 'paused'));
|
|
223
|
+
if (!goal) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
emitGoalUpdated(eventBus, goalId, existing.projectId);
|
|
227
|
+
return serializeGoal(goal);
|
|
228
|
+
},
|
|
229
|
+
resume(goalId) {
|
|
230
|
+
const existing = getGoal(db, goalId);
|
|
231
|
+
if (!existing) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
if (existing.status !== 'paused') {
|
|
235
|
+
throw new InvalidGoalTransitionError('Goal can only be resumed from paused status');
|
|
236
|
+
}
|
|
237
|
+
const goal = db.transaction((tx) => updateGoalStatus(tx, goalId, 'active'));
|
|
238
|
+
if (!goal) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
emitGoalUpdated(eventBus, goalId, existing.projectId);
|
|
242
|
+
return serializeGoal(goal);
|
|
243
|
+
},
|
|
244
|
+
complete(goalId, evidence) {
|
|
245
|
+
const existing = getGoal(db, goalId);
|
|
246
|
+
if (!existing) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
if (existing.status === 'complete') {
|
|
250
|
+
throw new InvalidGoalTransitionError('Goal is already complete');
|
|
251
|
+
}
|
|
252
|
+
if (existing.status !== 'blocked') {
|
|
253
|
+
const incomplete = listTasks(db, existing.projectId)
|
|
254
|
+
.filter((task) => task.goalId === goalId && task.status !== 'done')
|
|
255
|
+
.map((task) => task.id);
|
|
256
|
+
if (incomplete.length > 0) {
|
|
257
|
+
throw new GoalCompletionBlockedError(incomplete);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const surface = parseVerificationSurface(existing.verificationSurface);
|
|
261
|
+
const at = new Date().toISOString();
|
|
262
|
+
if (!surface) {
|
|
263
|
+
const goal = db.transaction((tx) => updateGoal(tx, goalId, {
|
|
264
|
+
status: 'complete',
|
|
265
|
+
lastVerification: recordLastVerification(at, true, null),
|
|
266
|
+
}));
|
|
267
|
+
if (!goal) {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
emitGoalUpdated(eventBus, goalId, existing.projectId);
|
|
271
|
+
return serializeGoal(goal);
|
|
272
|
+
}
|
|
273
|
+
if (!evidence || evidence.kind !== surface.kind) {
|
|
274
|
+
throw new GoalVerificationRequiredError(surface.kind);
|
|
275
|
+
}
|
|
276
|
+
const evaluation = evaluateEvidence(surface, evidence);
|
|
277
|
+
const lastVerification = recordLastVerification(at, evaluation.green, surface.kind, evaluation.detail);
|
|
278
|
+
if (evaluation.green) {
|
|
279
|
+
const goal = db.transaction((tx) => {
|
|
280
|
+
const updated = updateGoal(tx, goalId, {
|
|
281
|
+
status: 'complete',
|
|
282
|
+
lastVerification,
|
|
283
|
+
});
|
|
284
|
+
if (!updated) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
resolveBlockingTasks(tx, existing.projectId, goalId);
|
|
288
|
+
return updated;
|
|
289
|
+
});
|
|
290
|
+
if (!goal) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
emitGoalUpdated(eventBus, goalId, existing.projectId);
|
|
294
|
+
return serializeGoal(goal);
|
|
295
|
+
}
|
|
296
|
+
const goal = db.transaction((tx) => {
|
|
297
|
+
const updated = updateGoal(tx, goalId, {
|
|
298
|
+
status: 'blocked',
|
|
299
|
+
lastVerification,
|
|
300
|
+
});
|
|
301
|
+
if (!updated) {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
if (!hasUnresolvedBlockingTask(tx, existing.projectId, goalId)) {
|
|
305
|
+
createTask(tx, {
|
|
306
|
+
projectId: existing.projectId,
|
|
307
|
+
goalId,
|
|
308
|
+
label: `Fix acceptance failure: ${truncateObjective(existing.objective)}`,
|
|
309
|
+
status: 'scope',
|
|
310
|
+
description: buildBlockingTaskDescription(goalId, surface.kind, evaluation.detail),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return updated;
|
|
314
|
+
});
|
|
315
|
+
if (!goal) {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
emitGoalUpdated(eventBus, goalId, existing.projectId);
|
|
319
|
+
return serializeGoal(goal);
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=goals.js.map
|
package/dist/services/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { type NoteService } from './notes.js';
|
|
|
8
8
|
import { type ProjectService } from './projects.js';
|
|
9
9
|
import { type AgentRunService } from './agent-runs.js';
|
|
10
10
|
import { type TagService } from './tags.js';
|
|
11
|
+
import { type GoalService } from './goals.js';
|
|
11
12
|
import { type TaskService } from './tasks.js';
|
|
12
13
|
import { type TokenService } from './tokens.js';
|
|
13
14
|
import { type ShareService } from './share.js';
|
|
@@ -19,6 +20,7 @@ export type ServicesDeps = {
|
|
|
19
20
|
export type Services = {
|
|
20
21
|
eventBus: EventBus;
|
|
21
22
|
projectService: ProjectService;
|
|
23
|
+
goalService: GoalService;
|
|
22
24
|
taskService: TaskService;
|
|
23
25
|
tagService: TagService;
|
|
24
26
|
canvasService: CanvasService;
|
package/dist/services/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createNoteService } from './notes.js';
|
|
|
7
7
|
import { createProjectService } from './projects.js';
|
|
8
8
|
import { createAgentRunService } from './agent-runs.js';
|
|
9
9
|
import { createTagService } from './tags.js';
|
|
10
|
+
import { createGoalService } from './goals.js';
|
|
10
11
|
import { createTaskService } from './tasks.js';
|
|
11
12
|
import { createTokenService } from './tokens.js';
|
|
12
13
|
import { createShareService } from './share.js';
|
|
@@ -14,6 +15,7 @@ import { createSyncService } from './sync.js';
|
|
|
14
15
|
export function createServices(deps) {
|
|
15
16
|
const eventBus = deps.eventBus ?? createEventBus();
|
|
16
17
|
const projectService = createProjectService({ db: deps.db, eventBus });
|
|
18
|
+
const goalService = createGoalService({ db: deps.db, eventBus });
|
|
17
19
|
const taskService = createTaskService({ db: deps.db, eventBus });
|
|
18
20
|
const tagService = createTagService({ db: deps.db, eventBus });
|
|
19
21
|
const canvasService = createCanvasService({ db: deps.db, eventBus });
|
|
@@ -28,6 +30,7 @@ export function createServices(deps) {
|
|
|
28
30
|
return {
|
|
29
31
|
eventBus,
|
|
30
32
|
projectService,
|
|
33
|
+
goalService,
|
|
31
34
|
taskService,
|
|
32
35
|
tagService,
|
|
33
36
|
canvasService,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clearDocumentParentRefsByProject, clearFolderParentRefsByProject, createDocument, createEdge, createProject as dbCreateProject, createTask, deleteAgentRun, deleteAgentRunEventsByRunId, deleteCommentsByProjectId, deleteDocumentsByProjectId, deleteFoldersByProjectId, deleteNotesByProjectId, deleteEdgesByProjectId, deleteShareSubmissionsByProjectId, deleteSharesByProjectId, deleteTagsByProjectId, deleteSyncRemoteByProjectId, deleteSyncStateByProjectId, deleteProject as dbDeleteProject, deleteTasksByProjectId, getProject as dbGetProject, InvalidTaskStatusError, isTaskStatus, listAgentRuns, listProjects as dbListProjects, listTasks, updateProject as dbUpdateProject, } from '@plandesk/db';
|
|
1
|
+
import { clearDocumentParentRefsByProject, clearFolderParentRefsByProject, createDocument, createEdge, createProject as dbCreateProject, createTask, deleteAgentRun, deleteAgentRunEventsByRunId, deleteCommentsByProjectId, deleteDocumentsByProjectId, deleteFoldersByProjectId, deleteNotesByProjectId, deleteEdgesByProjectId, deleteGoalsByProjectId, deleteShareSubmissionsByProjectId, deleteSharesByProjectId, deleteTagsByProjectId, deleteSyncRemoteByProjectId, deleteSyncStateByProjectId, deleteProject as dbDeleteProject, deleteTasksByProjectId, getOrCreateDefaultGoal, getProject as dbGetProject, InvalidTaskStatusError, isTaskStatus, listAgentRuns, listProjects as dbListProjects, listTasks, updateProject as dbUpdateProject, } from '@plandesk/db';
|
|
2
2
|
import { emptyTaskStatusSummary, serializeDocument, serializeEdge, serializeProject, serializeProjectDetail, serializeTask, } from '../serialize.js';
|
|
3
3
|
export class InvalidScaffoldError extends Error {
|
|
4
4
|
constructor(message) {
|
|
@@ -94,6 +94,7 @@ export function createProjectService(deps) {
|
|
|
94
94
|
deleteNotesByProjectId(tx, id);
|
|
95
95
|
deleteTagsByProjectId(tx, id);
|
|
96
96
|
deleteTasksByProjectId(tx, id);
|
|
97
|
+
deleteGoalsByProjectId(tx, id);
|
|
97
98
|
deleteShareSubmissionsByProjectId(tx, id);
|
|
98
99
|
deleteSyncStateByProjectId(tx, id);
|
|
99
100
|
deleteSyncRemoteByProjectId(tx, id);
|
|
@@ -116,11 +117,13 @@ export function createProjectService(deps) {
|
|
|
116
117
|
description: input.description,
|
|
117
118
|
});
|
|
118
119
|
projectId = project.id;
|
|
120
|
+
const defaultGoal = getOrCreateDefaultGoal(tx, project.id);
|
|
119
121
|
input.tasks.forEach((taskInput, i) => {
|
|
120
122
|
const x = taskInput.x ?? (i % 4) * 240;
|
|
121
123
|
const y = taskInput.y ?? Math.floor(i / 4) * 160;
|
|
122
124
|
const task = createTask(tx, {
|
|
123
125
|
projectId: project.id,
|
|
126
|
+
goalId: defaultGoal.id,
|
|
124
127
|
label: taskInput.label,
|
|
125
128
|
status: taskInput.status,
|
|
126
129
|
description: taskInput.description,
|
package/dist/services/sync.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Db, type ShareSubmission, type ShareSubmissionStatus
|
|
1
|
+
import { type Db, type ShareSubmission, type ShareSubmissionStatus } from '@plandesk/db';
|
|
2
2
|
import type { EventBus } from '../events.js';
|
|
3
3
|
import type { ShareService } from './share.js';
|
|
4
4
|
import type { TaskService } from './tasks.js';
|
|
@@ -11,6 +11,9 @@ export declare class SyncUnauthorizedError extends Error {
|
|
|
11
11
|
export declare class InvalidTriageError extends Error {
|
|
12
12
|
constructor(message?: string);
|
|
13
13
|
}
|
|
14
|
+
export declare class InvalidTriageInputError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
14
17
|
export type SyncRemote = {
|
|
15
18
|
serverUrl: string;
|
|
16
19
|
globalProjectId: string;
|
|
@@ -65,9 +68,8 @@ export declare function createSyncService(deps: SyncServiceDeps): {
|
|
|
65
68
|
getRemote(projectId: string): SyncRemote | undefined;
|
|
66
69
|
triage(submissionId: string, action: "accept" | "reject", remote: SyncRemote, asTask?: {
|
|
67
70
|
label?: string;
|
|
68
|
-
status?: TaskStatus;
|
|
69
71
|
description?: string;
|
|
70
|
-
}): Promise<SerializedSubmission>;
|
|
72
|
+
}, linkTaskId?: string): Promise<SerializedSubmission>;
|
|
71
73
|
watchPush(projectId: string, remote: SyncRemote, opts?: WatchPushOptions): WatchPushController;
|
|
72
74
|
};
|
|
73
75
|
export type SyncService = ReturnType<typeof createSyncService>;
|