@adecore/plan 0.0.1 → 0.17.0-beta.2
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/LICENSE +110 -0
- package/README.md +50 -0
- package/dist/apply.d.ts +252 -0
- package/dist/apply.js +379 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/markdown.d.ts +8 -0
- package/dist/markdown.js +271 -0
- package/dist/permissions.d.ts +7 -0
- package/dist/permissions.js +152 -0
- package/dist/protocol.d.ts +478 -0
- package/dist/protocol.js +148 -0
- package/dist/text.d.ts +9 -0
- package/dist/text.js +143 -0
- package/dist/tree.d.ts +41 -0
- package/dist/tree.js +142 -0
- package/package.json +40 -2
- package/src/apply.ts +452 -0
- package/src/index.ts +46 -0
- package/src/markdown.ts +275 -0
- package/src/permissions.ts +162 -0
- package/src/protocol.ts +194 -0
- package/src/text.ts +159 -0
- package/src/tree.ts +206 -0
package/src/apply.ts
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PlanChecksSchema,
|
|
3
|
+
PlanItemIdSchema,
|
|
4
|
+
PlanKindSchema,
|
|
5
|
+
PlanMetaSchema,
|
|
6
|
+
PlanOpSchema,
|
|
7
|
+
PlanSchema,
|
|
8
|
+
PlanSectionSchema,
|
|
9
|
+
PlanStepSchema,
|
|
10
|
+
PlanStepStateSchema,
|
|
11
|
+
PlanTextSchema,
|
|
12
|
+
type Plan,
|
|
13
|
+
type PlanActor,
|
|
14
|
+
type PlanItem,
|
|
15
|
+
type PlanMeta,
|
|
16
|
+
type PlanOp,
|
|
17
|
+
type PlanSection,
|
|
18
|
+
type PlanStep,
|
|
19
|
+
type PlanStepState,
|
|
20
|
+
type PlanText
|
|
21
|
+
} from './protocol.ts';
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { canApply } from './permissions.ts';
|
|
24
|
+
import { allItems, allSteps, effectiveChecks, locateItem, refuse, structureProblem, type PlanRefusal } from './tree.ts';
|
|
25
|
+
|
|
26
|
+
export interface PlanApplyOptions {
|
|
27
|
+
actor: PlanActor;
|
|
28
|
+
/* ISO time written into `at`; injected so a test never reads the clock. */
|
|
29
|
+
now: string;
|
|
30
|
+
/* Mints an id for an item that comes without one; retried until it is not taken. */
|
|
31
|
+
mintId?: () => string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PlanApplied {
|
|
35
|
+
ok: true;
|
|
36
|
+
plan: Plan;
|
|
37
|
+
/* Ids minted for items that came without one, in the order they were added. */
|
|
38
|
+
minted: string[];
|
|
39
|
+
/* Steps that got their first sub-step and so lost the state they had. */
|
|
40
|
+
dropped: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
44
|
+
|
|
45
|
+
export function randomItemId(): string {
|
|
46
|
+
const bytes = crypto.getRandomValues(new Uint8Array(6));
|
|
47
|
+
return Array.from(bytes, (byte) => ID_ALPHABET[byte % ID_ALPHABET.length]).join('');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function issueMessage(error: z.ZodError): string {
|
|
51
|
+
const issue = error.issues[0];
|
|
52
|
+
if (!issue) {
|
|
53
|
+
return 'The plan is not valid';
|
|
54
|
+
}
|
|
55
|
+
const path = issue.path.map((part) => (typeof part === 'number' ? `[${part}]` : `.${String(part)}`)).join('');
|
|
56
|
+
return `${path ? `${path.replace(/^\./, '')}: ` : ''}${issue.message}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function idMinter(taken: Set<string>, mintId: () => string, minted: string[]) {
|
|
60
|
+
return (): string => {
|
|
61
|
+
for (let attempt = 0; attempt < 1000; attempt++) {
|
|
62
|
+
const id = mintId();
|
|
63
|
+
if (!taken.has(id)) {
|
|
64
|
+
taken.add(id);
|
|
65
|
+
minted.push(id);
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw new Error('Could not mint a free plan item id');
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function setState(step: PlanStep, state: PlanStepState, actor: PlanActor, now: string): void {
|
|
74
|
+
// An agent confirming what a person set leaves the person's mark on it.
|
|
75
|
+
if (actor === 'agent' && step.by === 'person' && step.state === state) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// A person taking a mark away leaves nothing an agent has to respect, so a mis-click never locks the agent out.
|
|
79
|
+
if (actor === 'person' && state === 'open') {
|
|
80
|
+
step.state = state;
|
|
81
|
+
delete step.by;
|
|
82
|
+
delete step.at;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
step.state = state;
|
|
86
|
+
step.by = actor;
|
|
87
|
+
step.at = now;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function setNote(step: PlanStep, text: string): void {
|
|
91
|
+
if (text === '') {
|
|
92
|
+
delete step.note;
|
|
93
|
+
} else {
|
|
94
|
+
step.note = text;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* The array a new or moved item goes into and where, or a refusal. */
|
|
99
|
+
function placeFor(
|
|
100
|
+
plan: Plan,
|
|
101
|
+
type: PlanItem['type'],
|
|
102
|
+
under: string | undefined,
|
|
103
|
+
after: string | undefined
|
|
104
|
+
): { siblings: PlanItem[]; index: number; parent: PlanSection | PlanStep | null } | PlanRefusal {
|
|
105
|
+
let parent: PlanSection | PlanStep | null = null;
|
|
106
|
+
let siblings: PlanItem[] = plan.items;
|
|
107
|
+
if (under !== undefined) {
|
|
108
|
+
const location = locateItem(plan, under);
|
|
109
|
+
if (!location) {
|
|
110
|
+
return refuse('plan-missing-item', `The plan has no item "${under}"`);
|
|
111
|
+
}
|
|
112
|
+
const target = location.item;
|
|
113
|
+
if (target.type === 'text') {
|
|
114
|
+
return refuse('plan-bad-position', `"${under}" is a text block, which holds no items`);
|
|
115
|
+
}
|
|
116
|
+
parent = target;
|
|
117
|
+
if (target.type === 'section') {
|
|
118
|
+
siblings = target.items;
|
|
119
|
+
} else {
|
|
120
|
+
target.steps ??= [];
|
|
121
|
+
siblings = target.steps;
|
|
122
|
+
}
|
|
123
|
+
} else if (after !== undefined) {
|
|
124
|
+
const location = locateItem(plan, after);
|
|
125
|
+
if (!location) {
|
|
126
|
+
return refuse('plan-missing-item', `The plan has no item "${after}"`);
|
|
127
|
+
}
|
|
128
|
+
parent = location.parent;
|
|
129
|
+
siblings = location.siblings;
|
|
130
|
+
}
|
|
131
|
+
if (type === 'section' && parent !== null) {
|
|
132
|
+
return refuse('plan-bad-position', 'A section only stands at the top of a plan');
|
|
133
|
+
}
|
|
134
|
+
if (type === 'text' && parent?.type === 'step') {
|
|
135
|
+
return refuse('plan-bad-position', 'A step only holds steps, not a text block');
|
|
136
|
+
}
|
|
137
|
+
if (after === undefined) {
|
|
138
|
+
return { siblings, index: siblings.length, parent };
|
|
139
|
+
}
|
|
140
|
+
const index = siblings.findIndex((item) => item.id === after);
|
|
141
|
+
if (index === -1) {
|
|
142
|
+
return refuse('plan-bad-position', `"${after}" is not directly under "${under}"`);
|
|
143
|
+
}
|
|
144
|
+
return { siblings, index: index + 1, parent };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* A leaf that gets its first sub-step: its state now follows from its children. */
|
|
148
|
+
function becomeParent(parent: PlanSection | PlanStep | null, dropped: string[]): void {
|
|
149
|
+
if (parent?.type !== 'step' || (parent.steps?.length ?? 0) !== 1) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (parent.state !== undefined) {
|
|
153
|
+
dropped.push(parent.id);
|
|
154
|
+
}
|
|
155
|
+
delete parent.state;
|
|
156
|
+
delete parent.by;
|
|
157
|
+
delete parent.at;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function pruneEmptySteps(items: readonly PlanItem[]): void {
|
|
161
|
+
for (const item of allItems(items)) {
|
|
162
|
+
if (item.type === 'step' && item.steps?.length === 0) {
|
|
163
|
+
delete item.steps;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function applyOne(plan: Plan, op: PlanOp, options: PlanApplyOptions, mint: () => string, dropped: string[]): PlanRefusal | null {
|
|
169
|
+
const verdict = canApply(op, options.actor, plan);
|
|
170
|
+
if (!verdict.ok) {
|
|
171
|
+
return verdict;
|
|
172
|
+
}
|
|
173
|
+
const stepAt = (id: string): PlanStep => locateItem(plan, id)!.item as PlanStep;
|
|
174
|
+
switch (op.op) {
|
|
175
|
+
case 'set': {
|
|
176
|
+
for (const id of op.ids) {
|
|
177
|
+
const step = stepAt(id);
|
|
178
|
+
setState(step, op.state, options.actor, options.now);
|
|
179
|
+
if (op.note !== undefined) {
|
|
180
|
+
setNote(step, op.note);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (op.next !== undefined) {
|
|
184
|
+
setState(stepAt(op.next), 'active', options.actor, options.now);
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
case 'note':
|
|
189
|
+
setNote(stepAt(op.id), op.text);
|
|
190
|
+
return null;
|
|
191
|
+
case 'unlock': {
|
|
192
|
+
const roots = op.ids === 'all' ? plan.items : op.ids.map((id) => stepAt(id));
|
|
193
|
+
for (const step of allSteps(roots)) {
|
|
194
|
+
if (effectiveChecks(plan, step) === 'agent') {
|
|
195
|
+
step.unlocked = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
case 'add': {
|
|
201
|
+
const id = op.id ?? mint();
|
|
202
|
+
if (op.id !== undefined && locateItem(plan, op.id)) {
|
|
203
|
+
return refuse('duplicate-id', `The plan already has an item "${op.id}"`);
|
|
204
|
+
}
|
|
205
|
+
if (op.checks !== undefined && op.type !== 'step') {
|
|
206
|
+
return refuse('plan-invalid', 'Only a step has checks');
|
|
207
|
+
}
|
|
208
|
+
const place = placeFor(plan, op.type, op.under, op.after);
|
|
209
|
+
if ('ok' in place) {
|
|
210
|
+
return place;
|
|
211
|
+
}
|
|
212
|
+
const base = { id, title: op.title, ...(op.description ? { description: op.description } : {}) };
|
|
213
|
+
const item: PlanItem =
|
|
214
|
+
op.type === 'section'
|
|
215
|
+
? { type: 'section', ...base, items: [] }
|
|
216
|
+
: op.type === 'text'
|
|
217
|
+
? { type: 'text', ...base }
|
|
218
|
+
: { type: 'step', ...base, ...(op.checks ? { checks: op.checks } : {}) };
|
|
219
|
+
place.siblings.splice(place.index, 0, item);
|
|
220
|
+
becomeParent(place.parent, dropped);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
case 'edit': {
|
|
224
|
+
const item = locateItem(plan, op.id)!.item;
|
|
225
|
+
if (op.checks !== undefined && item.type !== 'step') {
|
|
226
|
+
return refuse('plan-invalid', 'Only a step has checks');
|
|
227
|
+
}
|
|
228
|
+
if (op.title !== undefined) {
|
|
229
|
+
item.title = op.title;
|
|
230
|
+
}
|
|
231
|
+
if (op.description === '') {
|
|
232
|
+
delete item.description;
|
|
233
|
+
} else if (op.description !== undefined) {
|
|
234
|
+
item.description = op.description;
|
|
235
|
+
}
|
|
236
|
+
if (op.checks !== undefined && item.type === 'step') {
|
|
237
|
+
item.checks = op.checks;
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
case 'move': {
|
|
242
|
+
const location = locateItem(plan, op.id)!;
|
|
243
|
+
if (op.after === op.id || (op.under !== undefined && allItems([location.item]).some((item) => item.id === op.under))) {
|
|
244
|
+
return refuse('plan-bad-position', `"${op.id}" cannot move into itself`);
|
|
245
|
+
}
|
|
246
|
+
location.siblings.splice(location.index, 1);
|
|
247
|
+
const place = placeFor(plan, location.item.type, op.under, op.after);
|
|
248
|
+
if ('ok' in place) {
|
|
249
|
+
return place;
|
|
250
|
+
}
|
|
251
|
+
place.siblings.splice(place.index, 0, location.item);
|
|
252
|
+
becomeParent(place.parent, dropped);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
case 'remove': {
|
|
256
|
+
const location = locateItem(plan, op.id)!;
|
|
257
|
+
location.siblings.splice(location.index, 1);
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
case 'meta': {
|
|
261
|
+
if (op.title !== undefined) {
|
|
262
|
+
plan.meta.title = op.title;
|
|
263
|
+
}
|
|
264
|
+
for (const key of ['summary', 'status'] as const) {
|
|
265
|
+
if (op[key] === '') {
|
|
266
|
+
delete plan.meta[key];
|
|
267
|
+
} else if (op[key] !== undefined) {
|
|
268
|
+
plan.meta[key] = op[key];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (op.checks !== undefined) {
|
|
272
|
+
plan.meta.checks = op.checks;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/*
|
|
280
|
+
* Applies a batch of operations for one actor, all or nothing. Each operation sees the plan as the
|
|
281
|
+
* ones before it left it, and the whole batch is one rev, so a step done and the next one active land
|
|
282
|
+
* together.
|
|
283
|
+
*/
|
|
284
|
+
export function applyPlanOps(plan: Plan, ops: readonly PlanOp[], options: PlanApplyOptions): PlanApplied | PlanRefusal {
|
|
285
|
+
const parsedOps = z.array(PlanOpSchema).min(1).safeParse(ops);
|
|
286
|
+
if (!parsedOps.success) {
|
|
287
|
+
return refuse('plan-invalid', issueMessage(parsedOps.error));
|
|
288
|
+
}
|
|
289
|
+
const next = structuredClone(plan);
|
|
290
|
+
const minted: string[] = [];
|
|
291
|
+
const dropped: string[] = [];
|
|
292
|
+
const taken = new Set(allItems(next.items).map((item) => item.id));
|
|
293
|
+
const mint = idMinter(taken, options.mintId ?? randomItemId, minted);
|
|
294
|
+
for (const op of parsedOps.data) {
|
|
295
|
+
const problem = applyOne(next, op, options, mint, dropped);
|
|
296
|
+
if (problem) {
|
|
297
|
+
return problem;
|
|
298
|
+
}
|
|
299
|
+
for (const item of allItems(next.items)) {
|
|
300
|
+
taken.add(item.id);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
pruneEmptySteps(next.items);
|
|
304
|
+
next.rev = plan.rev + 1;
|
|
305
|
+
const checked = validatePlan(next);
|
|
306
|
+
if (!checked.ok) {
|
|
307
|
+
return checked;
|
|
308
|
+
}
|
|
309
|
+
return { ok: true, plan: checked.plan, minted, dropped };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* A plan as it is stored or sent: the schema, then the rules that need the whole tree. */
|
|
313
|
+
export function validatePlan(value: unknown): { ok: true; plan: Plan } | PlanRefusal {
|
|
314
|
+
const parsed = PlanSchema.safeParse(value);
|
|
315
|
+
if (!parsed.success) {
|
|
316
|
+
return refuse('plan-invalid', issueMessage(parsed.error));
|
|
317
|
+
}
|
|
318
|
+
const problem = structureProblem(parsed.data);
|
|
319
|
+
return problem ?? { ok: true, plan: parsed.data };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/*
|
|
323
|
+
* What an agent hands `plan new`. Strict, so a misspelled field is refused by its path instead of
|
|
324
|
+
* dropped. Ids are optional and minted when absent; who set a state and when is never the agent's to say.
|
|
325
|
+
*/
|
|
326
|
+
export const PlanDraftStepSchema = z.strictObject({
|
|
327
|
+
type: PlanStepSchema.shape.type,
|
|
328
|
+
id: PlanItemIdSchema.optional(),
|
|
329
|
+
title: PlanStepSchema.shape.title,
|
|
330
|
+
description: PlanStepSchema.shape.description,
|
|
331
|
+
checks: PlanStepSchema.shape.checks,
|
|
332
|
+
state: PlanStepStateSchema.optional(),
|
|
333
|
+
note: PlanStepSchema.shape.note,
|
|
334
|
+
get steps() {
|
|
335
|
+
return z.array(PlanDraftStepSchema).optional();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
export type PlanDraftStep = z.infer<typeof PlanDraftStepSchema>;
|
|
339
|
+
|
|
340
|
+
export const PlanDraftTextSchema = z.strictObject({ ...PlanTextSchema.shape, id: PlanItemIdSchema.optional() });
|
|
341
|
+
export type PlanDraftText = z.infer<typeof PlanDraftTextSchema>;
|
|
342
|
+
|
|
343
|
+
export const PlanDraftSectionSchema = z.strictObject({
|
|
344
|
+
...PlanSectionSchema.shape,
|
|
345
|
+
id: PlanItemIdSchema.optional(),
|
|
346
|
+
items: z.array(z.discriminatedUnion('type', [PlanDraftTextSchema, PlanDraftStepSchema]))
|
|
347
|
+
});
|
|
348
|
+
export type PlanDraftSection = z.infer<typeof PlanDraftSectionSchema>;
|
|
349
|
+
|
|
350
|
+
export const PlanDraftItemSchema = z.discriminatedUnion('type', [PlanDraftSectionSchema, PlanDraftTextSchema, PlanDraftStepSchema]);
|
|
351
|
+
export type PlanDraftItem = z.infer<typeof PlanDraftItemSchema>;
|
|
352
|
+
|
|
353
|
+
export const PlanDraftMetaSchema = z.strictObject({
|
|
354
|
+
title: PlanMetaSchema.shape.title.optional(),
|
|
355
|
+
kind: PlanKindSchema.optional(),
|
|
356
|
+
summary: PlanMetaSchema.shape.summary,
|
|
357
|
+
status: PlanMetaSchema.shape.status,
|
|
358
|
+
checks: PlanChecksSchema.optional()
|
|
359
|
+
});
|
|
360
|
+
export type PlanDraftMeta = z.infer<typeof PlanDraftMetaSchema>;
|
|
361
|
+
|
|
362
|
+
export const PlanDraftSchema = z.strictObject({
|
|
363
|
+
meta: PlanDraftMetaSchema.optional(),
|
|
364
|
+
items: z.array(PlanDraftItemSchema)
|
|
365
|
+
});
|
|
366
|
+
export type PlanDraft = z.infer<typeof PlanDraftSchema>;
|
|
367
|
+
|
|
368
|
+
export function parsePlanDraft(value: unknown): { ok: true; draft: PlanDraft } | PlanRefusal {
|
|
369
|
+
const parsed = PlanDraftSchema.safeParse(value);
|
|
370
|
+
return parsed.success ? { ok: true, draft: parsed.data } : refuse('plan-invalid', issueMessage(parsed.error));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export interface PlanCreateOptions {
|
|
374
|
+
id: string;
|
|
375
|
+
now: string;
|
|
376
|
+
mintId?: () => string;
|
|
377
|
+
/* What the verb's flags say; wins over the draft's own meta. */
|
|
378
|
+
meta?: Partial<Pick<PlanMeta, 'title' | 'kind' | 'checks'>>;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* A new plan from an agent's draft: ids minted, states marked as the agent's, and the same rules as any later change. */
|
|
382
|
+
export function createPlan(draft: PlanDraft, options: PlanCreateOptions): PlanApplied | PlanRefusal {
|
|
383
|
+
const parsed = PlanDraftSchema.safeParse(draft);
|
|
384
|
+
if (!parsed.success) {
|
|
385
|
+
return refuse('plan-invalid', issueMessage(parsed.error));
|
|
386
|
+
}
|
|
387
|
+
const source = parsed.data;
|
|
388
|
+
const title = options.meta?.title ?? source.meta?.title;
|
|
389
|
+
if (!title) {
|
|
390
|
+
return refuse('plan-invalid', 'A plan needs a title');
|
|
391
|
+
}
|
|
392
|
+
const meta: PlanMeta = {
|
|
393
|
+
title,
|
|
394
|
+
kind: options.meta?.kind ?? source.meta?.kind ?? 'steps',
|
|
395
|
+
...(source.meta?.summary ? { summary: source.meta.summary } : {}),
|
|
396
|
+
...(source.meta?.status ? { status: source.meta.status } : {}),
|
|
397
|
+
checks: options.meta?.checks ?? source.meta?.checks ?? 'anyone'
|
|
398
|
+
};
|
|
399
|
+
const minted: string[] = [];
|
|
400
|
+
const taken = new Set<string>();
|
|
401
|
+
for (const item of allDraftItems(source.items)) {
|
|
402
|
+
if (item.id !== undefined) {
|
|
403
|
+
if (taken.has(item.id)) {
|
|
404
|
+
return refuse('duplicate-id', `Two items share the id "${item.id}"`);
|
|
405
|
+
}
|
|
406
|
+
taken.add(item.id);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const mint = idMinter(taken, options.mintId ?? randomItemId, minted);
|
|
410
|
+
const described = <T extends { description?: string }>(value: T): T => {
|
|
411
|
+
if (value.description === '') {
|
|
412
|
+
delete value.description;
|
|
413
|
+
}
|
|
414
|
+
return value;
|
|
415
|
+
};
|
|
416
|
+
const toStep = (step: PlanDraftStep): PlanStep => {
|
|
417
|
+
const { steps, state, note, ...rest } = step;
|
|
418
|
+
const result: PlanStep = described({ ...rest, id: step.id ?? mint() });
|
|
419
|
+
if (steps && steps.length > 0) {
|
|
420
|
+
result.steps = steps.map(toStep);
|
|
421
|
+
}
|
|
422
|
+
if (state !== undefined) {
|
|
423
|
+
Object.assign(result, { state, by: 'agent', at: options.now });
|
|
424
|
+
}
|
|
425
|
+
if (note) {
|
|
426
|
+
result.note = note;
|
|
427
|
+
}
|
|
428
|
+
return result;
|
|
429
|
+
};
|
|
430
|
+
const toText = (text: PlanDraftText): PlanText => described({ ...text, id: text.id ?? mint() });
|
|
431
|
+
const items = source.items.map((item): PlanItem => {
|
|
432
|
+
if (item.type === 'section') {
|
|
433
|
+
return described({ ...item, id: item.id ?? mint(), items: item.items.map((child) => (child.type === 'text' ? toText(child) : toStep(child))) });
|
|
434
|
+
}
|
|
435
|
+
return item.type === 'text' ? toText(item) : toStep(item);
|
|
436
|
+
});
|
|
437
|
+
const plan: Plan = { id: options.id, rev: 0, createdAt: options.now, meta, items };
|
|
438
|
+
const checked = validatePlan(plan);
|
|
439
|
+
if (!checked.ok) {
|
|
440
|
+
return checked;
|
|
441
|
+
}
|
|
442
|
+
for (const step of allSteps(checked.plan.items)) {
|
|
443
|
+
if (step.state !== undefined && effectiveChecks(checked.plan, step) === 'person') {
|
|
444
|
+
return refuse('person-only', `Only a person checks "${step.id}"`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { ok: true, plan: checked.plan, minted, dropped: [] };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function allDraftItems(items: readonly PlanDraftItem[]): PlanDraftItem[] {
|
|
451
|
+
return items.flatMap((item) => [item, ...allDraftItems(item.type === 'section' ? item.items : item.type === 'step' ? (item.steps ?? []) : [])]);
|
|
452
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type PlanApplyOptions,
|
|
3
|
+
type PlanApplied,
|
|
4
|
+
randomItemId,
|
|
5
|
+
applyPlanOps,
|
|
6
|
+
validatePlan,
|
|
7
|
+
PlanDraftStepSchema,
|
|
8
|
+
type PlanDraftStep,
|
|
9
|
+
PlanDraftTextSchema,
|
|
10
|
+
type PlanDraftText,
|
|
11
|
+
PlanDraftSectionSchema,
|
|
12
|
+
type PlanDraftSection,
|
|
13
|
+
PlanDraftItemSchema,
|
|
14
|
+
type PlanDraftItem,
|
|
15
|
+
PlanDraftMetaSchema,
|
|
16
|
+
type PlanDraftMeta,
|
|
17
|
+
PlanDraftSchema,
|
|
18
|
+
type PlanDraft,
|
|
19
|
+
parsePlanDraft,
|
|
20
|
+
type PlanCreateOptions,
|
|
21
|
+
createPlan
|
|
22
|
+
} from './apply.ts';
|
|
23
|
+
export { planToMarkdown, parsePlanMarkdown } from './markdown.ts';
|
|
24
|
+
export { type PlanVerdict, canApply, canDeletePlan } from './permissions.ts';
|
|
25
|
+
export { PLAN_STATE_MARKERS, PLAN_LEGEND, type PlanTextOptions, progressText, renderPlanText } from './text.ts';
|
|
26
|
+
export {
|
|
27
|
+
type PlanRefusalCode,
|
|
28
|
+
type PlanRefusal,
|
|
29
|
+
refuse,
|
|
30
|
+
type PlanLocation,
|
|
31
|
+
locateItem,
|
|
32
|
+
findItem,
|
|
33
|
+
allItems,
|
|
34
|
+
allSteps,
|
|
35
|
+
isParentStep,
|
|
36
|
+
effectiveChecks,
|
|
37
|
+
isFinishedOutcome,
|
|
38
|
+
deriveState,
|
|
39
|
+
stepState,
|
|
40
|
+
type PlanProgress,
|
|
41
|
+
planProgress,
|
|
42
|
+
leafSteps,
|
|
43
|
+
activeStepIds,
|
|
44
|
+
holdsPersonState,
|
|
45
|
+
structureProblem
|
|
46
|
+
} from './tree.ts';
|