@quolu/lattice 0.52.2 → 0.52.3
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/package.json +1 -1
- package/src/project-cli.mjs +699 -697
- package/src/todo-cli.mjs +3143 -3141
- package/src/todo-store.mjs +4339 -4322
package/src/project-cli.mjs
CHANGED
|
@@ -1,697 +1,699 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { constants as fsConstants } from 'node:fs';
|
|
3
|
-
import { lstat, open, realpath } from 'node:fs/promises';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
TODO_DESIGN_MEMO_PROMPT,
|
|
8
|
-
canonicalizeTodoArtifact,
|
|
9
|
-
exactRecord,
|
|
10
|
-
explainTodoDesignMemo,
|
|
11
|
-
isTodoDesignMemo,
|
|
12
|
-
isStrictTodoTimestamp,
|
|
13
|
-
isTodoDigest,
|
|
14
|
-
isTodoIdentifier,
|
|
15
|
-
isTodoRef,
|
|
16
|
-
todoSelfDigest,
|
|
17
|
-
} from './todo-contracts.mjs';
|
|
18
|
-
import { projectTodoStatus } from './todo-status.mjs';
|
|
19
|
-
import { readTodoPlanNotesForStatus } from './todo-note-store.mjs';
|
|
20
|
-
import { projectIndependenceFrontier } from './todo-independence.mjs';
|
|
21
|
-
import { readTodoParallelCandidatesForStatus } from './todo-parallel-candidates.mjs';
|
|
22
|
-
import { isTodoIndependenceLegacyMarker } from './todo-independence-contracts.mjs';
|
|
23
|
-
import { selectIndependenceGuidance } from './todo-independence-guidance.mjs';
|
|
24
|
-
import { ensureTodoDashboardActivity } from './todo-dashboard-registry.mjs';
|
|
25
|
-
import { resolveProjectIdentity } from './project-identity.mjs';
|
|
26
|
-
import {
|
|
27
|
-
buildTodoPlan,
|
|
28
|
-
createTodoStoreWriter,
|
|
29
|
-
initializeAuthoredTodoStore,
|
|
30
|
-
isPhaselessTodoPlanSchema,
|
|
31
|
-
readTodoIndependenceArtifact,
|
|
32
|
-
readTodoStore,
|
|
33
|
-
TodoStoreError,
|
|
34
|
-
} from './todo-store.mjs';
|
|
35
|
-
import {
|
|
36
|
-
assertTodoDispatchShapeReviewed,
|
|
37
|
-
computeTodoDispatchShapeForPlan,
|
|
38
|
-
} from './todo-dispatch-shape.mjs';
|
|
39
|
-
|
|
40
|
-
const STORE_REF = '.lattice/todo';
|
|
41
|
-
const MANIFEST_REF = `${STORE_REF}/manifest.json`;
|
|
42
|
-
const MAX_INPUT_BYTES = 8_388_608;
|
|
43
|
-
const STATUS_SCHEMA = 'lattice.project_status.v1';
|
|
44
|
-
const SESSION_CONTEXT_SCHEMA = 'lattice.session_context.v1';
|
|
45
|
-
/** HEADが読めない環境でも投影を組めるようにする。記録があるときは実HEADで置き換わる。 */
|
|
46
|
-
const PLACEHOLDER_SHA = '0'.repeat(40);
|
|
47
|
-
const CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v1';
|
|
48
|
-
const PHASE_CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v2';
|
|
49
|
-
const DECOUPLED_PHASE_CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v3';
|
|
50
|
-
const MEMO_PHASE_CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v4';
|
|
51
|
-
const CURRENT_CREATE_INPUT_SCHEMA = MEMO_PHASE_CREATE_INPUT_SCHEMA;
|
|
52
|
-
const CURRENT_CREATE_SCHEMA_COMMAND = 'lattice plan create --schema-version 4 --json';
|
|
53
|
-
const CREATE_RESULT_SCHEMA = 'lattice.plan_create_result.v1';
|
|
54
|
-
|
|
55
|
-
function resolveRepoRoot(cwd) {
|
|
56
|
-
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|| !
|
|
93
|
-
||
|
|
94
|
-
||
|
|
95
|
-
|| !
|
|
96
|
-
||
|
|
97
|
-
|
|
98
|
-
|| !
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
||
|
|
105
|
-
|
|
106
|
-
||
|
|
107
|
-
|
|
108
|
-
||
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
&& value.next_action
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
&&
|
|
123
|
-
&&
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
let
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
//
|
|
287
|
-
// audit_pending
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
captured
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
MEMO_PHASE_CREATE_INPUT_SCHEMA].includes(value?.schema);
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|| !
|
|
446
|
-
|| !
|
|
447
|
-
|| !
|
|
448
|
-
|| value.
|
|
449
|
-
|| !
|
|
450
|
-
|| value.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
:
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
...([DECOUPLED_PHASE_CREATE_INPUT_SCHEMA,
|
|
531
|
-
? {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { constants as fsConstants } from 'node:fs';
|
|
3
|
+
import { lstat, open, realpath } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
TODO_DESIGN_MEMO_PROMPT,
|
|
8
|
+
canonicalizeTodoArtifact,
|
|
9
|
+
exactRecord,
|
|
10
|
+
explainTodoDesignMemo,
|
|
11
|
+
isTodoDesignMemo,
|
|
12
|
+
isStrictTodoTimestamp,
|
|
13
|
+
isTodoDigest,
|
|
14
|
+
isTodoIdentifier,
|
|
15
|
+
isTodoRef,
|
|
16
|
+
todoSelfDigest,
|
|
17
|
+
} from './todo-contracts.mjs';
|
|
18
|
+
import { projectTodoStatus } from './todo-status.mjs';
|
|
19
|
+
import { readTodoPlanNotesForStatus } from './todo-note-store.mjs';
|
|
20
|
+
import { projectIndependenceFrontier } from './todo-independence.mjs';
|
|
21
|
+
import { readTodoParallelCandidatesForStatus } from './todo-parallel-candidates.mjs';
|
|
22
|
+
import { isTodoIndependenceLegacyMarker } from './todo-independence-contracts.mjs';
|
|
23
|
+
import { selectIndependenceGuidance } from './todo-independence-guidance.mjs';
|
|
24
|
+
import { ensureTodoDashboardActivity } from './todo-dashboard-registry.mjs';
|
|
25
|
+
import { resolveProjectIdentity } from './project-identity.mjs';
|
|
26
|
+
import {
|
|
27
|
+
buildTodoPlan,
|
|
28
|
+
createTodoStoreWriter,
|
|
29
|
+
initializeAuthoredTodoStore,
|
|
30
|
+
isPhaselessTodoPlanSchema,
|
|
31
|
+
readTodoIndependenceArtifact,
|
|
32
|
+
readTodoStore,
|
|
33
|
+
TodoStoreError,
|
|
34
|
+
} from './todo-store.mjs';
|
|
35
|
+
import {
|
|
36
|
+
assertTodoDispatchShapeReviewed,
|
|
37
|
+
computeTodoDispatchShapeForPlan,
|
|
38
|
+
} from './todo-dispatch-shape.mjs';
|
|
39
|
+
|
|
40
|
+
const STORE_REF = '.lattice/todo';
|
|
41
|
+
const MANIFEST_REF = `${STORE_REF}/manifest.json`;
|
|
42
|
+
const MAX_INPUT_BYTES = 8_388_608;
|
|
43
|
+
const STATUS_SCHEMA = 'lattice.project_status.v1';
|
|
44
|
+
const SESSION_CONTEXT_SCHEMA = 'lattice.session_context.v1';
|
|
45
|
+
/** HEADが読めない環境でも投影を組めるようにする。記録があるときは実HEADで置き換わる。 */
|
|
46
|
+
const PLACEHOLDER_SHA = '0'.repeat(40);
|
|
47
|
+
const CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v1';
|
|
48
|
+
const PHASE_CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v2';
|
|
49
|
+
const DECOUPLED_PHASE_CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v3';
|
|
50
|
+
const MEMO_PHASE_CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v4';
|
|
51
|
+
const CURRENT_CREATE_INPUT_SCHEMA = MEMO_PHASE_CREATE_INPUT_SCHEMA;
|
|
52
|
+
const CURRENT_CREATE_SCHEMA_COMMAND = 'lattice plan create --schema-version 4 --json';
|
|
53
|
+
const CREATE_RESULT_SCHEMA = 'lattice.plan_create_result.v1';
|
|
54
|
+
|
|
55
|
+
function resolveRepoRoot(cwd) {
|
|
56
|
+
try {
|
|
57
|
+
// gitはWindowsでもforward slashを返すため、OS nativeへ正規化して
|
|
58
|
+
// path.join由来のabsolute_pathと区切りを揃える。末尾newline以外(末尾空白等)は改変しない。
|
|
59
|
+
return path.resolve(execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
60
|
+
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
61
|
+
}).replace(/\r?\n$/u, ''));
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function gitHead(repoRoot) {
|
|
68
|
+
try {
|
|
69
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
70
|
+
cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
71
|
+
}).replace(/\r?\n$/u, '');
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resultDigest(value) {
|
|
78
|
+
return todoSelfDigest(value, 'result_digest');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function plain(value) {
|
|
82
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
83
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function validateProjectStatus(value) {
|
|
87
|
+
try {
|
|
88
|
+
if (!exactRecord(value, [
|
|
89
|
+
'schema', 'cli', 'project', 'state', 'store', 'active_plans', 'active_runs',
|
|
90
|
+
'can_create_plan', 'next_action', 'result_digest',
|
|
91
|
+
]) || value.schema !== STATUS_SCHEMA
|
|
92
|
+
|| !exactRecord(value.cli, ['available', 'version']) || value.cli.available !== true
|
|
93
|
+
|| typeof value.cli.version !== 'string' || value.cli.version.length > 64
|
|
94
|
+
|| !['uninitialized', 'ready', 'active_run', 'invalid'].includes(value.state)
|
|
95
|
+
|| !exactRecord(value.store, ['ref', 'absolute_path']) || value.store.ref !== STORE_REF
|
|
96
|
+
|| (value.store.absolute_path !== null && typeof value.store.absolute_path !== 'string')
|
|
97
|
+
|| !Array.isArray(value.active_plans) || value.active_plans.length > 256
|
|
98
|
+
|| !value.active_plans.every((entry) => exactRecord(entry, ['plan_key', 'plan_version'])
|
|
99
|
+
&& isTodoIdentifier(entry.plan_key) && isTodoIdentifier(entry.plan_version))
|
|
100
|
+
|| !Array.isArray(value.active_runs) || value.active_runs.length > 2_000
|
|
101
|
+
|| !value.active_runs.every((entry) => exactRecord(entry, ['plan_key', 'task_id', 'label'])
|
|
102
|
+
&& isTodoIdentifier(entry.plan_key) && isTodoIdentifier(entry.task_id)
|
|
103
|
+
&& typeof entry.label === 'string' && [...entry.label].length > 0 && [...entry.label].length <= 160)
|
|
104
|
+
|| typeof value.can_create_plan !== 'boolean' || !plain(value.next_action)
|
|
105
|
+
|| typeof value.next_action.command !== 'string' || value.next_action.command.length === 0
|
|
106
|
+
|| !isTodoDigest(value.result_digest) || value.result_digest !== resultDigest(value)) return false;
|
|
107
|
+
if (value.project !== null && (!exactRecord(value.project, ['root', 'git_head', 'project_id'])
|
|
108
|
+
|| typeof value.project.root !== 'string' || value.project.root.length === 0
|
|
109
|
+
|| (value.project.git_head !== null && !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(value.project.git_head))
|
|
110
|
+
|| (value.project.project_id !== null && !isTodoIdentifier(value.project.project_id)))) return false;
|
|
111
|
+
if (value.state === 'uninitialized') {
|
|
112
|
+
return value.project !== null && value.project.project_id === null && value.can_create_plan === true
|
|
113
|
+
&& exactRecord(value.next_action, ['command', 'input_schema', 'schema_command'])
|
|
114
|
+
&& value.next_action.input_schema === CURRENT_CREATE_INPUT_SCHEMA
|
|
115
|
+
&& value.next_action.schema_command === CURRENT_CREATE_SCHEMA_COMMAND;
|
|
116
|
+
}
|
|
117
|
+
if (value.state === 'invalid') {
|
|
118
|
+
return value.can_create_plan === false && exactRecord(value.next_action, ['command', 'reason'])
|
|
119
|
+
&& typeof value.next_action.reason === 'string' && value.next_action.reason.length > 0;
|
|
120
|
+
}
|
|
121
|
+
return value.project !== null && isTodoIdentifier(value.project.project_id)
|
|
122
|
+
&& value.can_create_plan === false && exactRecord(value.next_action, ['command', 'reason'])
|
|
123
|
+
&& typeof value.next_action.reason === 'string' && value.next_action.reason.length > 0
|
|
124
|
+
&& (value.state !== 'active_run' || value.active_runs.length > 0)
|
|
125
|
+
&& (value.state !== 'ready' || value.active_runs.length === 0);
|
|
126
|
+
} catch { return false; }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* readyのあるplanについてだけ並列可否を要約する(ADR 0131 Decision 5)。
|
|
131
|
+
*
|
|
132
|
+
* store読みは呼び出し側が済ませている。ここが払うのはplanごとの小さな記録ファイルと、
|
|
133
|
+
* 記録があるときだけのHEAD照合である。readyが無いplanは述べる対象が無いので載せない。
|
|
134
|
+
*/
|
|
135
|
+
async function summarizeIndependence({ repoRoot, store, todo }) {
|
|
136
|
+
const readyPlanKeys = [...new Set(todo.next_ready.map(({ plan_key: key }) => key))].sort();
|
|
137
|
+
if (readyPlanKeys.length === 0) return [];
|
|
138
|
+
const activeByPlan = new Map();
|
|
139
|
+
for (const task of todo.active_set) {
|
|
140
|
+
if (!activeByPlan.has(task.plan_key)) activeByPlan.set(task.plan_key, []);
|
|
141
|
+
activeByPlan.get(task.plan_key).push(task.task_id);
|
|
142
|
+
}
|
|
143
|
+
let currentBaseSha = null;
|
|
144
|
+
const summaries = [];
|
|
145
|
+
for (const planKey of readyPlanKeys) {
|
|
146
|
+
const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
|
|
147
|
+
if (member === undefined) continue;
|
|
148
|
+
let artifact = null;
|
|
149
|
+
try {
|
|
150
|
+
artifact = await readTodoIndependenceArtifact({ repoRoot, store, planKey });
|
|
151
|
+
} catch (error) {
|
|
152
|
+
// 読めない記録を「記録なし」へ丸めない。理由を載せて先へ進む。
|
|
153
|
+
summaries.push({
|
|
154
|
+
plan_key: planKey, coverage: null,
|
|
155
|
+
guidance: { code: 'independence_unrecorded', message: null, next_action: 'none' },
|
|
156
|
+
unreadable_reason: error instanceof TodoStoreError
|
|
157
|
+
? `${error.code}:${error.detail?.reason ?? error.message}` : 'independence_unreadable',
|
|
158
|
+
parallel_groups: [], serialize_pair_count: 0,
|
|
159
|
+
conflict_with_active_count: 0, unknown_task_ids: [],
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (currentBaseSha === null && artifact !== null) currentBaseSha = gitHead(repoRoot);
|
|
164
|
+
const projected = projectIndependenceFrontier({
|
|
165
|
+
artifact,
|
|
166
|
+
readyTaskIds: todo.next_ready.filter((task) => task.plan_key === planKey)
|
|
167
|
+
.map(({ task_id: taskId }) => taskId),
|
|
168
|
+
activeTaskIds: activeByPlan.get(planKey) ?? [],
|
|
169
|
+
plan: member.plan,
|
|
170
|
+
currentBaseSha: currentBaseSha ?? PLACEHOLDER_SHA,
|
|
171
|
+
changedPaths: null,
|
|
172
|
+
});
|
|
173
|
+
summaries.push({
|
|
174
|
+
plan_key: planKey,
|
|
175
|
+
coverage: projected.coverage,
|
|
176
|
+
guidance: selectIndependenceGuidance({
|
|
177
|
+
coverage: projected.coverage,
|
|
178
|
+
contractSuperseded: isTodoIndependenceLegacyMarker(artifact),
|
|
179
|
+
readyCount: todo.next_ready.filter((task) => task.plan_key === planKey).length,
|
|
180
|
+
taskDeclared: projected.frontier.unknown
|
|
181
|
+
.every(({ unknowns }) => !unknowns.some(({ kind }) => kind === 'witness_missing')),
|
|
182
|
+
taskStale: projected.frontier.unknown
|
|
183
|
+
.some(({ unknowns }) => unknowns.some(({ kind }) => kind === 'record_stale')),
|
|
184
|
+
conflictWithActive: projected.frontier.conflicts_with_active[0]?.severability ?? null,
|
|
185
|
+
conflictBetweenReady: projected.frontier.serialize_pairs[0]?.severability ?? null,
|
|
186
|
+
coordinationMode: member.coordination?.mode ?? null,
|
|
187
|
+
}),
|
|
188
|
+
unreadable_reason: null,
|
|
189
|
+
parallel_groups: projected.frontier.parallel_groups.map(({ task_ids: ids }) => [...ids]),
|
|
190
|
+
serialize_pair_count: projected.frontier.serialize_pairs.length,
|
|
191
|
+
conflict_with_active_count: projected.frontier.conflicts_with_active.length,
|
|
192
|
+
unknown_task_ids: projected.frontier.unknown.map(({ task_id: taskId }) => taskId),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return summaries;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function statusResult(fields) {
|
|
199
|
+
const result = { schema: STATUS_SCHEMA, ...fields, result_digest: '' };
|
|
200
|
+
result.result_digest = resultDigest(result);
|
|
201
|
+
if (!validateProjectStatus(result)) throw new TypeError('project status result contract invalid');
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function invalidStatus({ cliVersion, repoRoot, reason, nextAction = null }) {
|
|
206
|
+
return statusResult({
|
|
207
|
+
cli: { available: true, version: cliVersion },
|
|
208
|
+
project: repoRoot === null ? null : { root: repoRoot, git_head: gitHead(repoRoot), project_id: null },
|
|
209
|
+
state: 'invalid',
|
|
210
|
+
store: { ref: STORE_REF, absolute_path: repoRoot === null ? null : path.join(repoRoot, STORE_REF) },
|
|
211
|
+
active_plans: [], active_runs: [], can_create_plan: false,
|
|
212
|
+
next_action: nextAction ?? {
|
|
213
|
+
command: repoRoot === null ? 'git status' : 'lattice todo verify', reason,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* discoveryとstore読みを1回で済ませ、status結果と(読めたなら)storeを返す。
|
|
220
|
+
*
|
|
221
|
+
* `runProjectStatus`と`runSessionContext`が同じ判定を二度書かないための共有点。
|
|
222
|
+
* store読みはここでしか行わない——session開始経路が同じstoreを二度払っていたのが
|
|
223
|
+
* ADR 0131で直した欠陥である。
|
|
224
|
+
*/
|
|
225
|
+
async function resolveProjectState({ cwd, cliVersion }) {
|
|
226
|
+
const repoRoot = resolveRepoRoot(cwd);
|
|
227
|
+
if (repoRoot === null) {
|
|
228
|
+
return { exitCode: 1, repoRoot: null, store: null, todo: null,
|
|
229
|
+
result: invalidStatus({ cliVersion, repoRoot: null, reason: 'git_repository_unresolved' }) };
|
|
230
|
+
}
|
|
231
|
+
const storeAbsolute = path.join(repoRoot, STORE_REF);
|
|
232
|
+
const manifestAbsolute = path.join(repoRoot, MANIFEST_REF);
|
|
233
|
+
const latticeAbsolute = path.join(repoRoot, '.lattice');
|
|
234
|
+
let latticeState;
|
|
235
|
+
let storeState;
|
|
236
|
+
let manifestState;
|
|
237
|
+
const invalid = (reason) => ({
|
|
238
|
+
exitCode: 1, repoRoot, store: null, todo: null,
|
|
239
|
+
result: invalidStatus({ cliVersion, repoRoot, reason }),
|
|
240
|
+
});
|
|
241
|
+
try { latticeState = await lstat(latticeAbsolute); } catch (error) {
|
|
242
|
+
if (error?.code !== 'ENOENT') return invalid('lattice_root_unreadable');
|
|
243
|
+
}
|
|
244
|
+
if (latticeState !== undefined && (latticeState.isSymbolicLink() || !latticeState.isDirectory())) {
|
|
245
|
+
return invalid('lattice_root_invalid');
|
|
246
|
+
}
|
|
247
|
+
try { storeState = await lstat(storeAbsolute); } catch (error) {
|
|
248
|
+
if (error?.code !== 'ENOENT') return invalid('store_unreadable');
|
|
249
|
+
}
|
|
250
|
+
try { manifestState = await lstat(manifestAbsolute); } catch (error) {
|
|
251
|
+
if (error?.code !== 'ENOENT') return invalid('manifest_unreadable');
|
|
252
|
+
}
|
|
253
|
+
if (storeState === undefined && manifestState === undefined) {
|
|
254
|
+
return { exitCode: 0, repoRoot, store: null, todo: null, result: statusResult({
|
|
255
|
+
cli: { available: true, version: cliVersion },
|
|
256
|
+
project: { root: repoRoot, git_head: gitHead(repoRoot), project_id: null },
|
|
257
|
+
state: 'uninitialized',
|
|
258
|
+
store: { ref: STORE_REF, absolute_path: storeAbsolute },
|
|
259
|
+
active_plans: [], active_runs: [], can_create_plan: true,
|
|
260
|
+
next_action: {
|
|
261
|
+
command: 'lattice plan create --input .lattice/plan-create.json',
|
|
262
|
+
input_schema: CURRENT_CREATE_INPUT_SCHEMA,
|
|
263
|
+
schema_command: CURRENT_CREATE_SCHEMA_COMMAND,
|
|
264
|
+
},
|
|
265
|
+
}) };
|
|
266
|
+
}
|
|
267
|
+
if (storeState?.isSymbolicLink() || !storeState?.isDirectory()
|
|
268
|
+
|| manifestState?.isSymbolicLink() || !manifestState?.isFile()) {
|
|
269
|
+
return invalid('store_layout_invalid');
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const store = await readTodoStore({ repoRoot });
|
|
273
|
+
const todo = projectTodoStatus(store, {
|
|
274
|
+
planNotes: await readTodoPlanNotesForStatus({ repoRoot, store }),
|
|
275
|
+
parallelCandidates: await readTodoParallelCandidatesForStatus({ repoRoot, store, gitHead }),
|
|
276
|
+
});
|
|
277
|
+
const activeRuns = todo.active_set.map((entry) => ({
|
|
278
|
+
plan_key: entry.plan_key, task_id: entry.task_id, label: entry.label,
|
|
279
|
+
}));
|
|
280
|
+
const state = activeRuns.length > 0 ? 'active_run' : 'ready';
|
|
281
|
+
const next = activeRuns.length > 0
|
|
282
|
+
? { command: 'lattice todo status', reason: 'active_run_present' }
|
|
283
|
+
: todo.next_ready.length > 0
|
|
284
|
+
? { command: `lattice todo start --plan ${todo.next_ready[0].plan_key} --task ${todo.next_ready[0].task_id}${todo.next_ready.length > 1 ? ' --parallel-frontier' : ''}`,
|
|
285
|
+
reason: todo.next_ready.length > 1 ? 'parallel_frontier_present' : 'next_ready_present' }
|
|
286
|
+
// 監査待ちが在る限り「残作業なし」と答えない。優先順位はactive_run > next_ready >
|
|
287
|
+
// audit_pending > なしで、ready frontierが在る間はADR 0063の並列開始コマンドが勝つ
|
|
288
|
+
// ——ここを監査で上書きするとdispatchが再直列化する。監査待ちはtodo statusの
|
|
289
|
+
// audit_pending欄に常在するので、順位を下げても消えない。
|
|
290
|
+
// commandはverbatim実行可能で読み取り専用のものにする。`phase review`は
|
|
291
|
+
// `--reason <text>`のplaceholderを含みjournalを書き換えるので置かない
|
|
292
|
+
// (`todo phase status`の結果には既に両分岐のguidanceが載っている)。
|
|
293
|
+
: todo.audit_pending.length > 0
|
|
294
|
+
? { command: `lattice todo phase status --plan ${todo.audit_pending[0].plan_key}`,
|
|
295
|
+
reason: 'audit_pending' }
|
|
296
|
+
: { command: 'lattice todo status', reason: 'no_ready_task' };
|
|
297
|
+
const result = statusResult({
|
|
298
|
+
cli: { available: true, version: cliVersion },
|
|
299
|
+
project: { root: repoRoot, git_head: gitHead(repoRoot), project_id: store.project_id },
|
|
300
|
+
state,
|
|
301
|
+
store: { ref: STORE_REF, absolute_path: storeAbsolute },
|
|
302
|
+
active_plans: todo.member_heads.map((entry) => ({
|
|
303
|
+
plan_key: entry.plan_key, plan_version: entry.plan_version,
|
|
304
|
+
})),
|
|
305
|
+
active_runs: activeRuns,
|
|
306
|
+
can_create_plan: false,
|
|
307
|
+
next_action: next,
|
|
308
|
+
});
|
|
309
|
+
return { exitCode: 0, repoRoot, store, todo, result };
|
|
310
|
+
} catch (error) {
|
|
311
|
+
const reason = error instanceof TodoStoreError
|
|
312
|
+
? `${error.code}:${error.detail?.reason ?? error.message}` : 'store_validation_failed';
|
|
313
|
+
return invalid(reason);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function runProjectStatus({ cwd, stdout, cliVersion, env = process.env,
|
|
318
|
+
ensureDashboardActivity = ensureTodoDashboardActivity }) {
|
|
319
|
+
const state = await resolveProjectState({ cwd, cliVersion });
|
|
320
|
+
// dashboard活動の登録はdiscovery面の副作用として維持する(ADR 0131 Decision 4で
|
|
321
|
+
// session-context側だけが持たない、と決めた面である)。
|
|
322
|
+
if (state.store !== null && env.LATTICE_DASHBOARD_AUTOSTART !== '0') {
|
|
323
|
+
const identity = await resolveProjectIdentity({
|
|
324
|
+
repoRoot: state.repoRoot, projectId: state.store.project_id, env,
|
|
325
|
+
});
|
|
326
|
+
const actorSession = env.LATTICE_TODO_ACTOR_SESSION;
|
|
327
|
+
await ensureDashboardActivity({
|
|
328
|
+
repoRoot: state.repoRoot, projectId: state.store.project_id,
|
|
329
|
+
displayName: identity.displayName,
|
|
330
|
+
sessionId: isTodoIdentifier(actorSession) ? actorSession : `status-${process.pid}`, env,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
stdout.write(`${JSON.stringify(state.result)}\n`);
|
|
334
|
+
return state.exitCode;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* session開始時の現在地を1プロセス・1 store読みで返す(ADR 0131)。
|
|
339
|
+
*
|
|
340
|
+
* `lattice status`と`lattice todo status`は同じ`readTodoStore`を別プロセスで二重に払う。
|
|
341
|
+
* hostのSessionStartはその両方を呼ぶため、storeが育ったprojectでは実行枠を超えて
|
|
342
|
+
* 案内ごと捨てられていた。ここは合成であって置き換えではない——既存2面は不変で、
|
|
343
|
+
* それぞれの消費者を持ち続ける。
|
|
344
|
+
*
|
|
345
|
+
* dashboard活動の登録は行わない。現在地を知るために呼ぶ面であり、常駐面を起こす面ではない。
|
|
346
|
+
*/
|
|
347
|
+
export async function runSessionContext({ cwd, stdout, cliVersion }) {
|
|
348
|
+
const state = await resolveProjectState({ cwd, cliVersion });
|
|
349
|
+
const independence = state.store === null || state.todo === null
|
|
350
|
+
? [] : await summarizeIndependence({ repoRoot: state.repoRoot, store: state.store, todo: state.todo });
|
|
351
|
+
const result = {
|
|
352
|
+
schema: SESSION_CONTEXT_SCHEMA,
|
|
353
|
+
// project discoveryの答えをそのまま埋める。hostは既存の検証器を再利用できる。
|
|
354
|
+
status: state.result,
|
|
355
|
+
// todoは`lattice todo status`のresultそのもの。新しい意味論を発明しない。
|
|
356
|
+
todo: state.todo,
|
|
357
|
+
independence,
|
|
358
|
+
result_digest: '',
|
|
359
|
+
};
|
|
360
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
361
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
362
|
+
return state.exitCode;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function readCanonicalInput(repoRoot, inputRef) {
|
|
366
|
+
if (!isTodoRef(inputRef)) throw new TodoStoreError('INPUT_INVALID', 'input_ref_invalid');
|
|
367
|
+
const root = await realpath(repoRoot);
|
|
368
|
+
const absolute = path.resolve(root, inputRef);
|
|
369
|
+
if (!absolute.startsWith(`${root}${path.sep}`)) throw new TodoStoreError('INPUT_INVALID', 'input_outside_repo');
|
|
370
|
+
let cursor = root;
|
|
371
|
+
try {
|
|
372
|
+
for (const part of path.relative(root, absolute).split(path.sep)) {
|
|
373
|
+
cursor = path.join(cursor, part);
|
|
374
|
+
const entry = await lstat(cursor);
|
|
375
|
+
if (entry.isSymbolicLink()) throw new TodoStoreError('INPUT_INVALID', 'input_path_symlink');
|
|
376
|
+
}
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (error instanceof TodoStoreError) throw error;
|
|
379
|
+
throw new TodoStoreError('INPUT_INVALID', 'input_unreadable');
|
|
380
|
+
}
|
|
381
|
+
const state = await lstat(absolute);
|
|
382
|
+
if (!state.isFile() || await realpath(absolute) !== absolute) {
|
|
383
|
+
throw new TodoStoreError('INPUT_INVALID', 'input_path_invalid');
|
|
384
|
+
}
|
|
385
|
+
if (state.size > MAX_INPUT_BYTES) throw new TodoStoreError('INPUT_INVALID', 'input_too_large');
|
|
386
|
+
let handle;
|
|
387
|
+
let bytes;
|
|
388
|
+
try {
|
|
389
|
+
handle = await open(absolute, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
390
|
+
const opened = await handle.stat();
|
|
391
|
+
if (opened.dev !== state.dev || opened.ino !== state.ino || opened.size !== state.size
|
|
392
|
+
|| opened.mtimeMs !== state.mtimeMs || opened.ctimeMs !== state.ctimeMs || !opened.isFile()) {
|
|
393
|
+
throw new TodoStoreError('INPUT_INVALID', 'input_changed_during_validation');
|
|
394
|
+
}
|
|
395
|
+
if (opened.size > MAX_INPUT_BYTES) throw new TodoStoreError('INPUT_INVALID', 'input_too_large');
|
|
396
|
+
const capture = Buffer.allocUnsafe(MAX_INPUT_BYTES + 1);
|
|
397
|
+
let captured = 0;
|
|
398
|
+
while (captured < capture.length) {
|
|
399
|
+
const { bytesRead } = await handle.read(capture, captured, capture.length - captured, null);
|
|
400
|
+
if (bytesRead === 0) break;
|
|
401
|
+
captured += bytesRead;
|
|
402
|
+
}
|
|
403
|
+
if (captured > MAX_INPUT_BYTES) throw new TodoStoreError('INPUT_INVALID', 'input_too_large');
|
|
404
|
+
bytes = capture.subarray(0, captured);
|
|
405
|
+
const after = await lstat(absolute);
|
|
406
|
+
if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size
|
|
407
|
+
|| after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs
|
|
408
|
+
|| await realpath(absolute) !== absolute) {
|
|
409
|
+
throw new TodoStoreError('INPUT_INVALID', 'input_changed_during_validation');
|
|
410
|
+
}
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (error instanceof TodoStoreError) throw error;
|
|
413
|
+
throw new TodoStoreError('INPUT_INVALID', 'input_unreadable');
|
|
414
|
+
} finally {
|
|
415
|
+
await handle?.close();
|
|
416
|
+
}
|
|
417
|
+
if (bytes.length > MAX_INPUT_BYTES) throw new TodoStoreError('INPUT_INVALID', 'input_too_large');
|
|
418
|
+
let text;
|
|
419
|
+
try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes); }
|
|
420
|
+
catch { throw new TodoStoreError('INPUT_INVALID', 'input_utf8_invalid'); }
|
|
421
|
+
if (!text.endsWith('\n') || text.includes('\r') || text.slice(0, -1).includes('\n')) {
|
|
422
|
+
throw new TodoStoreError('INPUT_INVALID', 'input_bytes_noncanonical');
|
|
423
|
+
}
|
|
424
|
+
let value;
|
|
425
|
+
try { value = JSON.parse(text.slice(0, -1)); } catch { throw new TodoStoreError('INPUT_INVALID', 'input_json_invalid'); }
|
|
426
|
+
if (`${canonicalizeTodoArtifact(value)}\n` !== text) throw new TodoStoreError('INPUT_INVALID', 'input_bytes_noncanonical');
|
|
427
|
+
return value;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function validateCreateInput(value) {
|
|
431
|
+
const phaseInput = [PHASE_CREATE_INPUT_SCHEMA, DECOUPLED_PHASE_CREATE_INPUT_SCHEMA,
|
|
432
|
+
MEMO_PHASE_CREATE_INPUT_SCHEMA].includes(value?.schema);
|
|
433
|
+
const decoupledPhaseInput = [DECOUPLED_PHASE_CREATE_INPUT_SCHEMA,
|
|
434
|
+
MEMO_PHASE_CREATE_INPUT_SCHEMA].includes(value?.schema);
|
|
435
|
+
const keys = [
|
|
436
|
+
'schema', 'project_id', 'plan_key', 'plan_version', 'actor', 'recorded_at',
|
|
437
|
+
'tasks', 'hard_dependencies', 'joins', 'input_digest',
|
|
438
|
+
];
|
|
439
|
+
if (phaseInput) keys.push('phases');
|
|
440
|
+
if (decoupledPhaseInput) keys.push('phase_accept_dependencies');
|
|
441
|
+
if (!exactRecord(value, keys) || ![
|
|
442
|
+
CREATE_INPUT_SCHEMA, PHASE_CREATE_INPUT_SCHEMA, DECOUPLED_PHASE_CREATE_INPUT_SCHEMA,
|
|
443
|
+
MEMO_PHASE_CREATE_INPUT_SCHEMA,
|
|
444
|
+
].includes(value.schema)
|
|
445
|
+
|| !isTodoIdentifier(value.project_id)
|
|
446
|
+
|| !isTodoIdentifier(value.plan_key) || !isTodoIdentifier(value.plan_version)
|
|
447
|
+
|| !exactRecord(value.actor, ['host', 'session', 'agent'])
|
|
448
|
+
|| ![value.actor.host, value.actor.session, value.actor.agent].every(isTodoIdentifier)
|
|
449
|
+
|| !isStrictTodoTimestamp(value.recorded_at) || !isTodoDigest(value.input_digest)
|
|
450
|
+
|| value.input_digest !== todoSelfDigest(value, 'input_digest')
|
|
451
|
+
|| !Array.isArray(value.tasks)
|
|
452
|
+
|| value.tasks.some((task) => task?.narrative_anchor !== null || task?.compile_binding !== null)) return false;
|
|
453
|
+
try {
|
|
454
|
+
buildTodoPlan({
|
|
455
|
+
schema: value.schema === MEMO_PHASE_CREATE_INPUT_SCHEMA ? 'lattice.todo_plan.v7'
|
|
456
|
+
: decoupledPhaseInput ? 'lattice.todo_plan.v5'
|
|
457
|
+
: phaseInput ? 'lattice.todo_plan.v4' : 'lattice.todo_plan.v3', project_id: value.project_id,
|
|
458
|
+
plan_key: value.plan_key, plan_version: value.plan_version,
|
|
459
|
+
predecessor_plan_digest: null, tasks: value.tasks,
|
|
460
|
+
hard_dependencies: value.hard_dependencies, joins: value.joins,
|
|
461
|
+
...(phaseInput ? { phases: value.phases } : {}),
|
|
462
|
+
...(decoupledPhaseInput ? { phase_accept_dependencies: value.phase_accept_dependencies } : {}),
|
|
463
|
+
});
|
|
464
|
+
return true;
|
|
465
|
+
} catch { return false; }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export async function runPlanCreate({ cwd, inputRef, stdout, serializationReviewed = false }) {
|
|
469
|
+
const repoRoot = resolveRepoRoot(cwd);
|
|
470
|
+
if (repoRoot === null) throw new TodoStoreError('REPO_UNRESOLVED', 'git_toplevel_unresolved');
|
|
471
|
+
const input = await readCanonicalInput(repoRoot, inputRef);
|
|
472
|
+
if (input?.schema !== MEMO_PHASE_CREATE_INPUT_SCHEMA) {
|
|
473
|
+
throw new TodoStoreError('INPUT_INVALID', 'plan_create_schema_retired', undefined, {
|
|
474
|
+
violation_kind: 'const_mismatch', pointer: '/schema',
|
|
475
|
+
expected: MEMO_PHASE_CREATE_INPUT_SCHEMA,
|
|
476
|
+
actual: typeof input?.schema === 'string' ? input.schema : { type: typeof input?.schema },
|
|
477
|
+
next_action: CURRENT_CREATE_SCHEMA_COMMAND,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
if (!Array.isArray(input.tasks)) {
|
|
481
|
+
throw new TodoStoreError('INPUT_INVALID', 'plan_create_schema_invalid', undefined, {
|
|
482
|
+
violation_kind: 'type', pointer: '/tasks', expected: { type: 'array', min_items: 1 },
|
|
483
|
+
actual: { type: input.tasks === null ? 'null' : typeof input.tasks },
|
|
484
|
+
next_action: CURRENT_CREATE_SCHEMA_COMMAND,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const invalidMemoIndex = input.tasks.findIndex((task) => !isTodoDesignMemo(task?.design_memo));
|
|
488
|
+
if (invalidMemoIndex >= 0) {
|
|
489
|
+
const explained = explainTodoDesignMemo(input.tasks[invalidMemoIndex]?.design_memo);
|
|
490
|
+
throw new TodoStoreError('DESIGN_MEMO_REQUIRED', 'plan_create_design_memo_required', undefined, {
|
|
491
|
+
violation_kind: explained.reason, pointer: `/tasks/${invalidMemoIndex}/design_memo`,
|
|
492
|
+
expected: explained.expected, actual: explained.actual,
|
|
493
|
+
prompt: TODO_DESIGN_MEMO_PROMPT, next_action: CURRENT_CREATE_SCHEMA_COMMAND,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
if (!validateCreateInput(input)) {
|
|
497
|
+
const digestValid = isTodoDigest(input.input_digest)
|
|
498
|
+
&& input.input_digest === todoSelfDigest(input, 'input_digest');
|
|
499
|
+
throw new TodoStoreError('INPUT_INVALID', 'plan_create_schema_invalid', undefined, {
|
|
500
|
+
violation_kind: digestValid ? 'schema_or_topology_invalid' : 'input_digest_mismatch',
|
|
501
|
+
pointer: digestValid ? '/' : '/input_digest',
|
|
502
|
+
expected: digestValid ? { schema: MEMO_PHASE_CREATE_INPUT_SCHEMA }
|
|
503
|
+
: todoSelfDigest(input, 'input_digest'),
|
|
504
|
+
actual: digestValid ? { validation: 'failed' }
|
|
505
|
+
: { type: typeof input.input_digest, matches_canonical_input: false },
|
|
506
|
+
next_action: 'correct_the_reported_pointer_then_rerun_plan_create',
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
// dispatch_shapeのgateはstore初期化より前に判定する(拒否時にstoreへ何も書かないため、
|
|
510
|
+
// 再考後の再実行がplan_key_already_existsで詰まらない)。
|
|
511
|
+
const dispatchShape = computeTodoDispatchShapeForPlan({
|
|
512
|
+
projectId: input.project_id,
|
|
513
|
+
planKey: input.plan_key,
|
|
514
|
+
taskIds: input.tasks.map(({ task_id: taskId }) => taskId),
|
|
515
|
+
hardDependencies: input.hard_dependencies,
|
|
516
|
+
joins: input.joins,
|
|
517
|
+
});
|
|
518
|
+
assertTodoDispatchShapeReviewed({ shape: dispatchShape, reviewed: serializationReviewed });
|
|
519
|
+
const store = await initializeAuthoredTodoStore({
|
|
520
|
+
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }),
|
|
521
|
+
projectId: input.project_id, repositories: [{ repo_id: 'self', path: '.' }],
|
|
522
|
+
plan: {
|
|
523
|
+
schema: input.schema === MEMO_PHASE_CREATE_INPUT_SCHEMA ? 'lattice.todo_plan.v7'
|
|
524
|
+
: input.schema === DECOUPLED_PHASE_CREATE_INPUT_SCHEMA ? 'lattice.todo_plan.v5'
|
|
525
|
+
: input.schema === PHASE_CREATE_INPUT_SCHEMA
|
|
526
|
+
? 'lattice.todo_plan.v4' : 'lattice.todo_plan.v3', project_id: input.project_id,
|
|
527
|
+
plan_key: input.plan_key, plan_version: input.plan_version,
|
|
528
|
+
predecessor_plan_digest: null, tasks: input.tasks,
|
|
529
|
+
hard_dependencies: input.hard_dependencies, joins: input.joins,
|
|
530
|
+
...([PHASE_CREATE_INPUT_SCHEMA, DECOUPLED_PHASE_CREATE_INPUT_SCHEMA,
|
|
531
|
+
MEMO_PHASE_CREATE_INPUT_SCHEMA].includes(input.schema) ? { phases: input.phases } : {}),
|
|
532
|
+
...([DECOUPLED_PHASE_CREATE_INPUT_SCHEMA, MEMO_PHASE_CREATE_INPUT_SCHEMA].includes(input.schema)
|
|
533
|
+
? { phase_accept_dependencies: input.phase_accept_dependencies } : {}),
|
|
534
|
+
},
|
|
535
|
+
genesis: { actor: input.actor, recorded_at: input.recorded_at, provenance: null },
|
|
536
|
+
});
|
|
537
|
+
const member = store.members[0];
|
|
538
|
+
const result = {
|
|
539
|
+
schema: CREATE_RESULT_SCHEMA, project_id: store.project_id,
|
|
540
|
+
plan_key: member.plan.plan_key, plan_version: member.plan.plan_version,
|
|
541
|
+
store_ref: STORE_REF, plan_ref: member.descriptor.plan_ref,
|
|
542
|
+
journal_ref: member.descriptor.journal_ref, snapshot_ref: member.descriptor.snapshot_ref,
|
|
543
|
+
plan_digest: member.plan.plan_digest,
|
|
544
|
+
dispatch_shape: {
|
|
545
|
+
task_count: dispatchShape.task_count,
|
|
546
|
+
critical_path_length: dispatchShape.critical_path_length,
|
|
547
|
+
max_frontier_width: dispatchShape.max_frontier_width,
|
|
548
|
+
serialization_ratio: dispatchShape.serialization_ratio,
|
|
549
|
+
},
|
|
550
|
+
// ADR 0147裁定3: phase無し(v3)のplan createは拒否せず、終端監査が要ることを結果へ
|
|
551
|
+
// 明示するに留める。phase入力(v4/v5)ならfalse——既存のPhase gateがそのまま重監査を担う。
|
|
552
|
+
terminal_audit_required: isPhaselessTodoPlanSchema(member.plan.schema),
|
|
553
|
+
result_digest: '',
|
|
554
|
+
};
|
|
555
|
+
result.result_digest = resultDigest(result);
|
|
556
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* `--schema --json`(版指定なし)の既定はCURRENT_CREATE_INPUT_SCHEMAと同じv4にする。
|
|
562
|
+
* 既定がv1のままだと、素の`--schema`を叩いたAIが古いv1(Phaseを表現できない)を
|
|
563
|
+
* 受け取り、実運用で通らない入力を作ってしまう(実際に踏んだ)。
|
|
564
|
+
* `--schema-version 1`は互換のため引き続き取得できる(bin/lattice.mjs側で許可)。
|
|
565
|
+
*
|
|
566
|
+
* 「どの版を返したか」は返すJSON Schema自身の`title`(例: `lattice.plan_create_input.v4`)が
|
|
567
|
+
* 既に機械可読に持っている。壊さずに追加のkeyを足す理由が無いので足さない。
|
|
568
|
+
*/
|
|
569
|
+
export async function runPlanCreateSchema({ stdout, version = 4 }) {
|
|
570
|
+
if (![1, 2, 3, 4].includes(version)) throw new TypeError('unsupported plan create schema version');
|
|
571
|
+
const expected = version === 4 ? MEMO_PHASE_CREATE_INPUT_SCHEMA
|
|
572
|
+
: version === 3 ? DECOUPLED_PHASE_CREATE_INPUT_SCHEMA
|
|
573
|
+
: version === 2 ? PHASE_CREATE_INPUT_SCHEMA : CREATE_INPUT_SCHEMA;
|
|
574
|
+
const schemaUrl = new URL(`../docs/schemas/lattice.plan_create_input.v${version}.schema.json`, import.meta.url);
|
|
575
|
+
const handle = await open(schemaUrl, fsConstants.O_RDONLY);
|
|
576
|
+
try {
|
|
577
|
+
const schema = JSON.parse(await handle.readFile('utf8'));
|
|
578
|
+
if (schema?.title !== expected) throw new TypeError('bundled plan create schema invalid');
|
|
579
|
+
stdout.write(`${JSON.stringify(schema)}\n`);
|
|
580
|
+
return 0;
|
|
581
|
+
} finally {
|
|
582
|
+
await handle.close();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const PLAN_SHOW_RESULT_SCHEMA = 'lattice.plan_show_result.v1';
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* `todo bindings`はcompile_binding付きtaskだけを投影するので、通常planでは空配列を返す。
|
|
590
|
+
* それを見て「planが空だ」と誤読された実績があるため、plan本体(task・依存・phase・状態)を
|
|
591
|
+
* 1コマンドで読める面を別に持つ。読み出しは既存store readerとtodo status projectionの
|
|
592
|
+
* 再利用に留め、journalを独自に再実装しない。
|
|
593
|
+
*/
|
|
594
|
+
export async function runPlanShow({ cwd, planKey, stdout }) {
|
|
595
|
+
const repoRoot = resolveRepoRoot(cwd);
|
|
596
|
+
if (repoRoot === null) throw new TodoStoreError('REPO_UNRESOLVED', 'git_toplevel_unresolved');
|
|
597
|
+
const store = await readTodoStore({ repoRoot });
|
|
598
|
+
const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
|
|
599
|
+
if (member === undefined) {
|
|
600
|
+
// 既存read commands(independence compile等)と同じcode/reasonを踏襲する。
|
|
601
|
+
// 未知のplan_keyを「store不整合」と同じ扱いにするのはこのCLI全体の既定であり、
|
|
602
|
+
// ここだけ別codeへ逸れると呼び出し側の分岐が増える。
|
|
603
|
+
throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active', undefined, {
|
|
604
|
+
plan_key: planKey, next_action: 'check_active_plans_via_status',
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
const { plan, tasks, phases } = member;
|
|
608
|
+
const phaseInput = ['lattice.todo_plan.v4', 'lattice.todo_plan.v5', 'lattice.todo_plan.v7']
|
|
609
|
+
.includes(plan.schema);
|
|
610
|
+
const stateByTaskId = new Map(tasks.map((state) => [state.task_id, state]));
|
|
611
|
+
// snapshot artifactの形式(v1にはphasesキーが無い)には縛られない導出ビューを読む
|
|
612
|
+
// (readTodoStoreが常に member.phases として埋める。ADR 0147)。
|
|
613
|
+
const phaseStatusById = new Map((phases ?? []).map((phase) => [phase.phase_id, phase.status]));
|
|
614
|
+
|
|
615
|
+
// 依存の本数: このplanの中でそのtaskへ入ってくるhard_dependencies辺と、joinで
|
|
616
|
+
// 合流するafter辺の合計。cross-plan参照は数えない(plan showは単一planの投影のため)。
|
|
617
|
+
const dependsOnCount = new Map(plan.tasks.map((task) => [task.task_id, 0]));
|
|
618
|
+
for (const edge of plan.hard_dependencies) {
|
|
619
|
+
if (edge.to.plan_key === planKey && dependsOnCount.has(edge.to.task_id)) {
|
|
620
|
+
dependsOnCount.set(edge.to.task_id, dependsOnCount.get(edge.to.task_id) + 1);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
for (const join of plan.joins) {
|
|
624
|
+
if (join.before.plan_key === planKey && dependsOnCount.has(join.before.task_id)) {
|
|
625
|
+
dependsOnCount.set(join.before.task_id, dependsOnCount.get(join.before.task_id) + join.after.length);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const taskList = plan.tasks.map((task) => ({
|
|
630
|
+
task_id: task.task_id,
|
|
631
|
+
title: task.title,
|
|
632
|
+
lane: task.lane,
|
|
633
|
+
phase_id: phaseInput ? task.phase_id : null,
|
|
634
|
+
state: stateByTaskId.get(task.task_id).status,
|
|
635
|
+
depends_on_count: dependsOnCount.get(task.task_id) ?? 0,
|
|
636
|
+
}));
|
|
637
|
+
|
|
638
|
+
const phaseList = phaseInput ? plan.phases.map((phase) => ({
|
|
639
|
+
phase_id: phase.phase_id,
|
|
640
|
+
title: phase.title,
|
|
641
|
+
gate_policy: phase.gate_policy,
|
|
642
|
+
predecessor_phase_ids: phase.predecessor_phase_ids,
|
|
643
|
+
status: phaseStatusById.get(phase.phase_id) ?? null,
|
|
644
|
+
})) : [];
|
|
645
|
+
|
|
646
|
+
// dispatch形状はplan create時に既に計算している同じ関数を再利用する。
|
|
647
|
+
// critical path長・frontier幅を独自に計算し直さない。
|
|
648
|
+
const dispatchShape = computeTodoDispatchShapeForPlan({
|
|
649
|
+
projectId: plan.project_id, planKey,
|
|
650
|
+
taskIds: plan.tasks.map(({ task_id: taskId }) => taskId),
|
|
651
|
+
hardDependencies: plan.hard_dependencies, joins: plan.joins,
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const result = {
|
|
655
|
+
schema: PLAN_SHOW_RESULT_SCHEMA,
|
|
656
|
+
project_id: plan.project_id,
|
|
657
|
+
plan_key: plan.plan_key,
|
|
658
|
+
plan_version: plan.plan_version,
|
|
659
|
+
plan_schema: plan.schema,
|
|
660
|
+
has_phases: phaseInput,
|
|
661
|
+
phases: phaseList,
|
|
662
|
+
tasks: taskList,
|
|
663
|
+
topology: {
|
|
664
|
+
task_count: dispatchShape.task_count,
|
|
665
|
+
critical_path_length: dispatchShape.critical_path_length,
|
|
666
|
+
max_frontier_width: dispatchShape.max_frontier_width,
|
|
667
|
+
serialization_ratio: dispatchShape.serialization_ratio,
|
|
668
|
+
},
|
|
669
|
+
result_digest: '',
|
|
670
|
+
};
|
|
671
|
+
result.result_digest = resultDigest(result);
|
|
672
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
673
|
+
return 0;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export function projectStatusFailure({ cwd, stdout, cliVersion, error }) {
|
|
677
|
+
const projectRootConflict = error?.code === 'PROJECT_ROOT_CONFLICT';
|
|
678
|
+
const result = invalidStatus({
|
|
679
|
+
cliVersion, repoRoot: resolveRepoRoot(cwd),
|
|
680
|
+
reason: projectRootConflict
|
|
681
|
+
? 'project_root_conflict'
|
|
682
|
+
: `status_internal_failure:${error?.constructor?.name ?? 'Error'}`,
|
|
683
|
+
...(projectRootConflict ? { nextAction: {
|
|
684
|
+
command: 'lattice todo dashboard adopt --json', reason: 'project_root_conflict',
|
|
685
|
+
} } : {}),
|
|
686
|
+
});
|
|
687
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
688
|
+
return 1;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function projectCliFailure(stderr, error) {
|
|
692
|
+
const payload = {
|
|
693
|
+
schema: 'lattice.cli_error.v2', code: error?.code ?? 'INTERNAL_FAILURE',
|
|
694
|
+
message: error?.message ?? 'project command failed',
|
|
695
|
+
};
|
|
696
|
+
if (error?.detail && Object.keys(error.detail).length > 0) payload.detail = error.detail;
|
|
697
|
+
stderr.write(`${JSON.stringify(payload)}\n`);
|
|
698
|
+
return 1;
|
|
699
|
+
}
|