@plandesk/api 0.7.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 +20 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +5 -1
- package/dist/routes/agent-runs.d.ts +1 -1
- package/dist/routes/agent-runs.js +20 -0
- package/dist/routes/documents.js +2 -0
- package/dist/routes/folders.d.ts +4 -0
- package/dist/routes/folders.js +69 -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 +23 -3
- package/dist/routes/submissions.d.ts +5 -0
- package/dist/routes/submissions.js +58 -0
- package/dist/routes/tags.d.ts +4 -0
- package/dist/routes/tags.js +62 -0
- package/dist/routes/tasks.d.ts +1 -0
- package/dist/routes/tasks.js +9 -1
- package/dist/serialize.d.ts +51 -2
- package/dist/serialize.js +101 -1
- package/dist/server.js +9 -1
- package/dist/services/canvas.d.ts +4 -0
- package/dist/services/canvas.js +3 -2
- package/dist/services/documents.d.ts +5 -1
- package/dist/services/documents.js +31 -2
- package/dist/services/folders.d.ts +27 -0
- package/dist/services/folders.js +107 -0
- package/dist/services/goals.d.ts +239 -0
- package/dist/services/goals.js +323 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +9 -0
- package/dist/services/projects.js +8 -2
- package/dist/services/sync.d.ts +5 -3
- package/dist/services/sync.js +45 -2
- package/dist/services/tags.d.ts +27 -0
- package/dist/services/tags.js +79 -0
- package/dist/services/tasks.d.ts +20 -2
- package/dist/services/tasks.js +98 -26
- package/package.json +3 -3
- 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-cyyf9-Uw.js +0 -234
|
@@ -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
|
@@ -3,9 +3,12 @@ import { type EventBus } from '../events.js';
|
|
|
3
3
|
import { type CanvasService } from './canvas.js';
|
|
4
4
|
import { type CommentService } from './comments.js';
|
|
5
5
|
import { type DocumentService } from './documents.js';
|
|
6
|
+
import { type FolderService } from './folders.js';
|
|
6
7
|
import { type NoteService } from './notes.js';
|
|
7
8
|
import { type ProjectService } from './projects.js';
|
|
8
9
|
import { type AgentRunService } from './agent-runs.js';
|
|
10
|
+
import { type TagService } from './tags.js';
|
|
11
|
+
import { type GoalService } from './goals.js';
|
|
9
12
|
import { type TaskService } from './tasks.js';
|
|
10
13
|
import { type TokenService } from './tokens.js';
|
|
11
14
|
import { type ShareService } from './share.js';
|
|
@@ -17,9 +20,12 @@ export type ServicesDeps = {
|
|
|
17
20
|
export type Services = {
|
|
18
21
|
eventBus: EventBus;
|
|
19
22
|
projectService: ProjectService;
|
|
23
|
+
goalService: GoalService;
|
|
20
24
|
taskService: TaskService;
|
|
25
|
+
tagService: TagService;
|
|
21
26
|
canvasService: CanvasService;
|
|
22
27
|
documentService: DocumentService;
|
|
28
|
+
folderService: FolderService;
|
|
23
29
|
noteService: NoteService;
|
|
24
30
|
commentService: CommentService;
|
|
25
31
|
agentRunService: AgentRunService;
|
package/dist/services/index.js
CHANGED
|
@@ -2,9 +2,12 @@ import { createEventBus } from '../events.js';
|
|
|
2
2
|
import { createCanvasService } from './canvas.js';
|
|
3
3
|
import { createCommentService } from './comments.js';
|
|
4
4
|
import { createDocumentService } from './documents.js';
|
|
5
|
+
import { createFolderService } from './folders.js';
|
|
5
6
|
import { createNoteService } from './notes.js';
|
|
6
7
|
import { createProjectService } from './projects.js';
|
|
7
8
|
import { createAgentRunService } from './agent-runs.js';
|
|
9
|
+
import { createTagService } from './tags.js';
|
|
10
|
+
import { createGoalService } from './goals.js';
|
|
8
11
|
import { createTaskService } from './tasks.js';
|
|
9
12
|
import { createTokenService } from './tokens.js';
|
|
10
13
|
import { createShareService } from './share.js';
|
|
@@ -12,9 +15,12 @@ import { createSyncService } from './sync.js';
|
|
|
12
15
|
export function createServices(deps) {
|
|
13
16
|
const eventBus = deps.eventBus ?? createEventBus();
|
|
14
17
|
const projectService = createProjectService({ db: deps.db, eventBus });
|
|
18
|
+
const goalService = createGoalService({ db: deps.db, eventBus });
|
|
15
19
|
const taskService = createTaskService({ db: deps.db, eventBus });
|
|
20
|
+
const tagService = createTagService({ db: deps.db, eventBus });
|
|
16
21
|
const canvasService = createCanvasService({ db: deps.db, eventBus });
|
|
17
22
|
const documentService = createDocumentService({ db: deps.db, eventBus });
|
|
23
|
+
const folderService = createFolderService({ db: deps.db, eventBus });
|
|
18
24
|
const noteService = createNoteService({ db: deps.db, eventBus });
|
|
19
25
|
const commentService = createCommentService({ db: deps.db, eventBus });
|
|
20
26
|
const agentRunService = createAgentRunService({ db: deps.db, eventBus });
|
|
@@ -24,9 +30,12 @@ export function createServices(deps) {
|
|
|
24
30
|
return {
|
|
25
31
|
eventBus,
|
|
26
32
|
projectService,
|
|
33
|
+
goalService,
|
|
27
34
|
taskService,
|
|
35
|
+
tagService,
|
|
28
36
|
canvasService,
|
|
29
37
|
documentService,
|
|
38
|
+
folderService,
|
|
30
39
|
noteService,
|
|
31
40
|
commentService,
|
|
32
41
|
agentRunService,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clearDocumentParentRefsByProject, createDocument, createEdge, createProject as dbCreateProject, createTask, deleteAgentRun, deleteAgentRunEventsByRunId, deleteCommentsByProjectId, deleteDocumentsByProjectId, deleteNotesByProjectId, deleteEdgesByProjectId, deleteShareSubmissionsByProjectId, deleteSharesByProjectId, 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) {
|
|
@@ -89,8 +89,12 @@ export function createProjectService(deps) {
|
|
|
89
89
|
clearDocumentParentRefsByProject(tx, id);
|
|
90
90
|
deleteCommentsByProjectId(tx, id);
|
|
91
91
|
deleteDocumentsByProjectId(tx, id);
|
|
92
|
+
clearFolderParentRefsByProject(tx, id);
|
|
93
|
+
deleteFoldersByProjectId(tx, id);
|
|
92
94
|
deleteNotesByProjectId(tx, id);
|
|
95
|
+
deleteTagsByProjectId(tx, id);
|
|
93
96
|
deleteTasksByProjectId(tx, id);
|
|
97
|
+
deleteGoalsByProjectId(tx, id);
|
|
94
98
|
deleteShareSubmissionsByProjectId(tx, id);
|
|
95
99
|
deleteSyncStateByProjectId(tx, id);
|
|
96
100
|
deleteSyncRemoteByProjectId(tx, id);
|
|
@@ -113,11 +117,13 @@ export function createProjectService(deps) {
|
|
|
113
117
|
description: input.description,
|
|
114
118
|
});
|
|
115
119
|
projectId = project.id;
|
|
120
|
+
const defaultGoal = getOrCreateDefaultGoal(tx, project.id);
|
|
116
121
|
input.tasks.forEach((taskInput, i) => {
|
|
117
122
|
const x = taskInput.x ?? (i % 4) * 240;
|
|
118
123
|
const y = taskInput.y ?? Math.floor(i / 4) * 160;
|
|
119
124
|
const task = createTask(tx, {
|
|
120
125
|
projectId: project.id,
|
|
126
|
+
goalId: defaultGoal.id,
|
|
121
127
|
label: taskInput.label,
|
|
122
128
|
status: taskInput.status,
|
|
123
129
|
description: taskInput.description,
|
|
@@ -164,7 +170,7 @@ export function createProjectService(deps) {
|
|
|
164
170
|
}
|
|
165
171
|
return {
|
|
166
172
|
project: serializeProject(project),
|
|
167
|
-
tasks: taskRows.map(serializeTask),
|
|
173
|
+
tasks: taskRows.map((task) => serializeTask(task)),
|
|
168
174
|
edges: edgeRows.map(serializeEdge),
|
|
169
175
|
documents: documentRows.map(serializeDocument),
|
|
170
176
|
key_to_id: Object.fromEntries(keyToId),
|