@quolu/lattice 0.59.0 → 0.60.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/bin/lattice.mjs +20 -0
- package/docs/schemas/lattice.executor_packet.v1.schema.json +2 -2
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +1 -1
- package/docs/schemas/lattice.plan_scope_review.v1.schema.json +55 -0
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +6 -2
- package/docs/schemas/lattice.todo_extraction.v4.schema.json +161 -0
- package/package.json +3 -1
- package/src/cli-help.mjs +3 -2
- package/src/plan-scope-review.mjs +214 -0
- package/src/project-cli.mjs +26 -3
- package/src/runtime-contracts.mjs +1 -1
- package/src/runtime-engine.mjs +3 -11
- package/src/runtime-pull-intake.mjs +34 -4
- package/src/runtime-work-order-contracts.mjs +1 -1
- package/src/todo-cli.mjs +106 -42
- package/src/todo-independence-authoritative-observation.mjs +116 -0
- package/src/todo-independence-guidance.mjs +25 -0
- package/src/todo-migration.mjs +124 -48
- package/src/todo-store.mjs +517 -40
- package/src/todo-structure-store.mjs +28 -0
package/src/todo-migration.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
export const TODO_EXTRACTION_SCHEMA = 'lattice.todo_extraction.v1';
|
|
18
18
|
export const TODO_EXTRACTION_SCHEMA_V2 = 'lattice.todo_extraction.v2';
|
|
19
19
|
export const TODO_EXTRACTION_SCHEMA_V3 = 'lattice.todo_extraction.v3';
|
|
20
|
+
export const TODO_EXTRACTION_SCHEMA_V4 = 'lattice.todo_extraction.v4';
|
|
20
21
|
|
|
21
22
|
const V1_DISPOSITIONS = new Set([
|
|
22
23
|
'register_pending',
|
|
@@ -95,17 +96,17 @@ function historicalStart(value) {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
function extractionTask(value, schema) {
|
|
98
|
-
const v2 = [TODO_EXTRACTION_SCHEMA_V2, TODO_EXTRACTION_SCHEMA_V3].includes(schema);
|
|
99
|
-
const
|
|
99
|
+
const v2 = [TODO_EXTRACTION_SCHEMA_V2, TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(schema);
|
|
100
|
+
const designMemo = [TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(schema);
|
|
100
101
|
const keys = [
|
|
101
102
|
'task_id', 'title', 'lane', 'narrative_ref', 'compile_binding', 'disposition',
|
|
102
103
|
'completion', 'source', 'migration_context',
|
|
103
104
|
];
|
|
104
105
|
if (v2) keys.push('start');
|
|
105
|
-
if (
|
|
106
|
+
if (designMemo) keys.push('design_memo');
|
|
106
107
|
if (!exactRecord(value, keys) || !isTodoIdentifier(value.task_id) || !boundedText(value.title)
|
|
107
108
|
|| !isTodoIdentifier(value.lane) || (value.narrative_ref !== null && !isTodoRef(value.narrative_ref))
|
|
108
|
-
|| (
|
|
109
|
+
|| (designMemo && !isTodoDesignMemo(value.design_memo))
|
|
109
110
|
|| value.compile_binding !== null || !(v2 ? V2_DISPOSITIONS : V1_DISPOSITIONS).has(value.disposition)
|
|
110
111
|
|| !sourceLocation(value.source) || !migrationContext(value.migration_context)) return false;
|
|
111
112
|
if (!v2) return value.disposition === 'register_done' ? completion(value.completion) : value.completion === null;
|
|
@@ -116,9 +117,23 @@ function extractionTask(value, schema) {
|
|
|
116
117
|
return value.start === null && value.completion === null;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
function
|
|
120
|
+
function isCrossPlanEdge(edge) {
|
|
121
|
+
return edge?.from?.project_id !== edge?.to?.project_id
|
|
122
|
+
|| edge?.from?.plan_key !== edge?.to?.plan_key;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validateEdges(value, schema) {
|
|
120
126
|
return Array.isArray(value) && value.length <= TODO_LIMITS.edgesPerPlan
|
|
121
|
-
&& value.every((edge) =>
|
|
127
|
+
&& value.every((edge) => {
|
|
128
|
+
const basic = exactRecord(edge, ['from', 'to']);
|
|
129
|
+
const refsValid = nodeRef(edge?.from) && nodeRef(edge?.to);
|
|
130
|
+
const crossPlan = refsValid && isCrossPlanEdge(edge);
|
|
131
|
+
const withReason = [TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(schema) && crossPlan
|
|
132
|
+
&& exactRecord(edge, ['from', 'to', 'reason'])
|
|
133
|
+
&& boundedText(edge.reason);
|
|
134
|
+
return (basic || withReason) && refsValid
|
|
135
|
+
&& (!crossPlan || ![TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(schema) || withReason);
|
|
136
|
+
})
|
|
122
137
|
&& sortedStrictly(value, (edge) => `${refKey(edge.from)}\0${refKey(edge.to)}`);
|
|
123
138
|
}
|
|
124
139
|
|
|
@@ -131,6 +146,23 @@ function validateJoins(value) {
|
|
|
131
146
|
&& sortedStrictly(value, (join) => join.id);
|
|
132
147
|
}
|
|
133
148
|
|
|
149
|
+
/**
|
|
150
|
+
* taskを新規登録しない入力は、既存source planの1 taskから別planの1 taskへ張る
|
|
151
|
+
* 明示dependencyだけに限定する。top-level identityはsource planを指し、両端の
|
|
152
|
+
* topology digestで、古い観測のまま接続しない。
|
|
153
|
+
*/
|
|
154
|
+
export function isTodoExtractionConnectionOnly(value) {
|
|
155
|
+
const edge = Array.isArray(value?.hard_dependencies) && value.hard_dependencies.length === 1
|
|
156
|
+
? value.hard_dependencies[0] : null;
|
|
157
|
+
return value?.schema === TODO_EXTRACTION_SCHEMA_V4
|
|
158
|
+
&& Array.isArray(value.tasks) && value.tasks.length === 0
|
|
159
|
+
&& Array.isArray(value.joins) && value.joins.length === 0
|
|
160
|
+
&& edge !== null && nodeRef(edge.from) && nodeRef(edge.to) && isCrossPlanEdge(edge)
|
|
161
|
+
&& edge.from.project_id === value.project_id && edge.from.plan_key === value.plan_key
|
|
162
|
+
&& isTodoDigest(edge.from.expected_topology_digest)
|
|
163
|
+
&& isTodoDigest(edge.to.expected_topology_digest);
|
|
164
|
+
}
|
|
165
|
+
|
|
134
166
|
const reject = (reason, path = '', detail = {}) => ({ valid: false, reason, path, ...detail });
|
|
135
167
|
|
|
136
168
|
/** value自体がexactRecordの対象になれる素朴なobjectかどうか(配列・nullを除く)。 */
|
|
@@ -180,13 +212,15 @@ export function explainTodoExtraction(value) {
|
|
|
180
212
|
try {
|
|
181
213
|
if (!plainObject(value)) return reject('not_an_object', '');
|
|
182
214
|
const schema = value.schema;
|
|
183
|
-
if (![TODO_EXTRACTION_SCHEMA, TODO_EXTRACTION_SCHEMA_V2, TODO_EXTRACTION_SCHEMA_V3
|
|
215
|
+
if (![TODO_EXTRACTION_SCHEMA, TODO_EXTRACTION_SCHEMA_V2, TODO_EXTRACTION_SCHEMA_V3,
|
|
216
|
+
TODO_EXTRACTION_SCHEMA_V4].includes(schema)) {
|
|
184
217
|
return reject('schema_mismatch', '/schema', {
|
|
185
|
-
expected:
|
|
218
|
+
expected: TODO_EXTRACTION_SCHEMA_V4, actual: typeof schema === 'string' ? schema : null,
|
|
186
219
|
});
|
|
187
220
|
}
|
|
188
|
-
const v2 = [TODO_EXTRACTION_SCHEMA_V2, TODO_EXTRACTION_SCHEMA_V3].includes(schema);
|
|
189
|
-
const
|
|
221
|
+
const v2 = [TODO_EXTRACTION_SCHEMA_V2, TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(schema);
|
|
222
|
+
const designMemo = [TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(schema);
|
|
223
|
+
const v4 = schema === TODO_EXTRACTION_SCHEMA_V4;
|
|
190
224
|
const topKeys = [
|
|
191
225
|
'schema', 'project_id', 'plan_key', 'plan_version', 'actor', 'recorded_at',
|
|
192
226
|
'tasks', 'hard_dependencies', 'joins', 'extraction_digest',
|
|
@@ -198,8 +232,8 @@ export function explainTodoExtraction(value) {
|
|
|
198
232
|
if (!isTodoIdentifier(value.plan_version)) return reject('invalid_identifier', '/plan_version');
|
|
199
233
|
if (!actor(value.actor)) return reject('invalid_actor', '/actor');
|
|
200
234
|
if (!isStrictTodoTimestamp(value.recorded_at)) return reject('invalid_timestamp', '/recorded_at');
|
|
201
|
-
if (!Array.isArray(value.tasks) || value.tasks.length
|
|
202
|
-
|| value.tasks.length
|
|
235
|
+
if (!Array.isArray(value.tasks) || value.tasks.length > TODO_LIMITS.tasksPerPlan
|
|
236
|
+
|| (value.tasks.length === 0 && !v4)) {
|
|
203
237
|
return reject('bounded_collection_violation', '/tasks');
|
|
204
238
|
}
|
|
205
239
|
const taskKeys = v2
|
|
@@ -207,11 +241,11 @@ export function explainTodoExtraction(value) {
|
|
|
207
241
|
'start', 'completion', 'source', 'migration_context']
|
|
208
242
|
: ['task_id', 'title', 'lane', 'narrative_ref', 'compile_binding', 'disposition',
|
|
209
243
|
'completion', 'source', 'migration_context'];
|
|
210
|
-
if (
|
|
244
|
+
if (designMemo) taskKeys.push('design_memo');
|
|
211
245
|
for (const [index, task] of value.tasks.entries()) {
|
|
212
246
|
const taskKeyCheck = explainKeys(task, taskKeys, `/tasks/${index}`);
|
|
213
247
|
if (!taskKeyCheck.valid) return taskKeyCheck;
|
|
214
|
-
if (
|
|
248
|
+
if (designMemo && !isTodoDesignMemo(task.design_memo)) {
|
|
215
249
|
return reject('design_memo_required', `/tasks/${index}/design_memo`, {
|
|
216
250
|
task_id: isTodoIdentifier(task.task_id) ? task.task_id : null,
|
|
217
251
|
expected: 'non-empty Markdown or NO_PLAN',
|
|
@@ -241,7 +275,7 @@ export function explainTodoExtraction(value) {
|
|
|
241
275
|
if (new Set(value.tasks.map(({ task_id: taskId }) => taskId)).size !== value.tasks.length) {
|
|
242
276
|
return reject('duplicate_task_id', '/tasks');
|
|
243
277
|
}
|
|
244
|
-
if (!validateEdges(value.hard_dependencies)) return reject('hard_dependencies_invalid', '/hard_dependencies');
|
|
278
|
+
if (!validateEdges(value.hard_dependencies, schema)) return reject('hard_dependencies_invalid', '/hard_dependencies');
|
|
245
279
|
if (!validateJoins(value.joins)) return reject('joins_invalid', '/joins');
|
|
246
280
|
if (!isTodoDigest(value.extraction_digest)) return reject('invalid_digest', '/extraction_digest');
|
|
247
281
|
const expectedDigest = todoSelfDigest(value, 'extraction_digest');
|
|
@@ -250,33 +284,44 @@ export function explainTodoExtraction(value) {
|
|
|
250
284
|
expected: expectedDigest, actual: value.extraction_digest,
|
|
251
285
|
});
|
|
252
286
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|| (task.source.parent_task_id !== null && !taskIds.has(task.source.parent_task_id)));
|
|
256
|
-
if (badParent !== undefined) {
|
|
257
|
-
return reject('parent_task_id_unresolved',
|
|
258
|
-
`/tasks/${value.tasks.indexOf(badParent)}/source/parent_task_id`);
|
|
259
|
-
}
|
|
260
|
-
const excluded = new Set(value.tasks
|
|
261
|
-
.filter((task) => task.disposition.startsWith('exclude_'))
|
|
262
|
-
.map(({ task_id }) => task_id));
|
|
263
|
-
const excludedParent = value.tasks.find((task) => task.disposition.startsWith('register_')
|
|
264
|
-
&& task.source.parent_task_id !== null && excluded.has(task.source.parent_task_id));
|
|
265
|
-
if (excludedParent !== undefined) {
|
|
266
|
-
return reject('registered_parent_task_id_unresolved',
|
|
267
|
-
`/tasks/${value.tasks.indexOf(excludedParent)}/source/parent_task_id`, {
|
|
268
|
-
task_id: excludedParent.task_id,
|
|
269
|
-
expected: 'task_id not excluded from the compiled plan',
|
|
270
|
-
actual: excludedParent.source.parent_task_id,
|
|
271
|
-
});
|
|
287
|
+
if (v4 && value.hard_dependencies.filter(isCrossPlanEdge).length > 1) {
|
|
288
|
+
return reject('cross_plan_dependency_limit_exceeded', '/hard_dependencies', { maximum: 1 });
|
|
272
289
|
}
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
275
|
-
return reject('
|
|
276
|
-
|
|
277
|
-
expected: 'registered task_id', actual: localRefViolation.task_id,
|
|
290
|
+
const connectionOnly = isTodoExtractionConnectionOnly(value);
|
|
291
|
+
if (value.tasks.length === 0 && !connectionOnly) {
|
|
292
|
+
return reject('connection_only_shape_invalid', '/tasks', {
|
|
293
|
+
expected: 'one reasoned cross-plan dependency from the top-level source plan',
|
|
278
294
|
});
|
|
279
295
|
}
|
|
296
|
+
if (!connectionOnly) {
|
|
297
|
+
const taskIds = new Set(value.tasks.map(({ task_id: taskId }) => taskId));
|
|
298
|
+
const badParent = value.tasks.find((task) => task.source.parent_task_id === task.task_id
|
|
299
|
+
|| (task.source.parent_task_id !== null && !taskIds.has(task.source.parent_task_id)));
|
|
300
|
+
if (badParent !== undefined) {
|
|
301
|
+
return reject('parent_task_id_unresolved',
|
|
302
|
+
`/tasks/${value.tasks.indexOf(badParent)}/source/parent_task_id`);
|
|
303
|
+
}
|
|
304
|
+
const excluded = new Set(value.tasks
|
|
305
|
+
.filter((task) => task.disposition.startsWith('exclude_'))
|
|
306
|
+
.map(({ task_id }) => task_id));
|
|
307
|
+
const excludedParent = value.tasks.find((task) => task.disposition.startsWith('register_')
|
|
308
|
+
&& task.source.parent_task_id !== null && excluded.has(task.source.parent_task_id));
|
|
309
|
+
if (excludedParent !== undefined) {
|
|
310
|
+
return reject('registered_parent_task_id_unresolved',
|
|
311
|
+
`/tasks/${value.tasks.indexOf(excludedParent)}/source/parent_task_id`, {
|
|
312
|
+
task_id: excludedParent.task_id,
|
|
313
|
+
expected: 'task_id not excluded from the compiled plan',
|
|
314
|
+
actual: excludedParent.source.parent_task_id,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const localRefViolation = firstUnregisteredLocalRef(value);
|
|
318
|
+
if (localRefViolation !== null) {
|
|
319
|
+
return reject('local_ref_unresolved', localRefViolation.path, {
|
|
320
|
+
task_id: localRefViolation.task_id,
|
|
321
|
+
expected: 'registered task_id', actual: localRefViolation.task_id,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
280
325
|
// ここまでの個別検査を全て通過したのに`validateTodoExtraction`がfalseを返す状況は、
|
|
281
326
|
// このexplainがまだ言い当てられない違反があるということ。捏造せず未特定と申告する。
|
|
282
327
|
return { valid: true };
|
|
@@ -320,19 +365,26 @@ export function validateTodoExtraction(value) {
|
|
|
320
365
|
if (!exactRecord(value, [
|
|
321
366
|
'schema', 'project_id', 'plan_key', 'plan_version', 'actor', 'recorded_at',
|
|
322
367
|
'tasks', 'hard_dependencies', 'joins', 'extraction_digest',
|
|
323
|
-
]) || ![TODO_EXTRACTION_SCHEMA, TODO_EXTRACTION_SCHEMA_V2, TODO_EXTRACTION_SCHEMA_V3
|
|
368
|
+
]) || ![TODO_EXTRACTION_SCHEMA, TODO_EXTRACTION_SCHEMA_V2, TODO_EXTRACTION_SCHEMA_V3,
|
|
369
|
+
TODO_EXTRACTION_SCHEMA_V4].includes(schema)
|
|
324
370
|
|| !isTodoIdentifier(value.project_id)
|
|
325
371
|
|| !isTodoIdentifier(value.plan_key) || !isTodoIdentifier(value.plan_version)
|
|
326
372
|
|| !actor(value.actor) || !isStrictTodoTimestamp(value.recorded_at)
|
|
327
|
-
|| !Array.isArray(value.tasks) || value.tasks.length
|
|
328
|
-
|| value.tasks.length
|
|
373
|
+
|| !Array.isArray(value.tasks) || value.tasks.length > TODO_LIMITS.tasksPerPlan
|
|
374
|
+
|| (value.tasks.length === 0 && schema !== TODO_EXTRACTION_SCHEMA_V4)
|
|
329
375
|
|| !value.tasks.every((task) => extractionTask(task, schema))
|
|
330
376
|
|| !sortedStrictly(value.tasks, (task) => task.task_id)
|
|
331
377
|
|| new Set(value.tasks.map(({ task_id }) => task_id)).size !== value.tasks.length
|
|
332
|
-
|| !validateEdges(value.hard_dependencies) || !validateJoins(value.joins)
|
|
378
|
+
|| !validateEdges(value.hard_dependencies, schema) || !validateJoins(value.joins)
|
|
333
379
|
|| !isTodoDigest(value.extraction_digest)
|
|
334
380
|
|| value.extraction_digest !== todoSelfDigest(value, 'extraction_digest')) return false;
|
|
335
381
|
|
|
382
|
+
if (schema === TODO_EXTRACTION_SCHEMA_V4 && value.hard_dependencies.filter(isCrossPlanEdge).length > 1) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
const connectionOnly = isTodoExtractionConnectionOnly(value);
|
|
386
|
+
if (value.tasks.length === 0 && !connectionOnly) return false;
|
|
387
|
+
if (connectionOnly) return true;
|
|
336
388
|
const taskIds = new Set(value.tasks.map(({ task_id }) => task_id));
|
|
337
389
|
if (value.tasks.some((task) => task.source.parent_task_id === task.task_id
|
|
338
390
|
|| (task.source.parent_task_id !== null && !taskIds.has(task.source.parent_task_id)))) return false;
|
|
@@ -381,9 +433,32 @@ export function compileTodoExtraction(value, repoRoot) {
|
|
|
381
433
|
}
|
|
382
434
|
|
|
383
435
|
const registered = value.tasks.filter(({ disposition }) => disposition.startsWith('register_'));
|
|
384
|
-
|
|
436
|
+
const connectionOnly = isTodoExtractionConnectionOnly(value);
|
|
437
|
+
if (registered.length === 0 && !connectionOnly) {
|
|
385
438
|
throw new TodoStoreError('MIGRATION_EMPTY', 'no_registered_tasks');
|
|
386
439
|
}
|
|
440
|
+
const hardDependencies = value.hard_dependencies.map(({ from, to }) => ({ from, to }));
|
|
441
|
+
const crossPlanDependencies = [TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(value.schema)
|
|
442
|
+
? value.hard_dependencies
|
|
443
|
+
.filter(isCrossPlanEdge)
|
|
444
|
+
.map(({ from, to, reason }) => ({ from, to, reason }))
|
|
445
|
+
: [];
|
|
446
|
+
if (connectionOnly) {
|
|
447
|
+
return {
|
|
448
|
+
repoRoot,
|
|
449
|
+
writer: createTodoStoreWriter({ caller: 'g4-migration' }),
|
|
450
|
+
connectionOnly: true,
|
|
451
|
+
connectionPlan: {
|
|
452
|
+
project_id: value.project_id, plan_key: value.plan_key, plan_version: value.plan_version,
|
|
453
|
+
},
|
|
454
|
+
crossPlanDependencies,
|
|
455
|
+
genesis: {
|
|
456
|
+
actor: value.actor,
|
|
457
|
+
recorded_at: value.recorded_at,
|
|
458
|
+
provenance: null,
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
}
|
|
387
462
|
return {
|
|
388
463
|
repoRoot,
|
|
389
464
|
writer: createTodoStoreWriter({ caller: 'g4-migration' }),
|
|
@@ -392,7 +467,7 @@ export function compileTodoExtraction(value, repoRoot) {
|
|
|
392
467
|
repositories: [{ repo_id: 'self', path: '.' }],
|
|
393
468
|
},
|
|
394
469
|
plan: {
|
|
395
|
-
schema: value.schema
|
|
470
|
+
schema: [TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(value.schema)
|
|
396
471
|
? 'lattice.todo_plan.v6' : 'lattice.todo_plan.v2',
|
|
397
472
|
project_id: value.project_id,
|
|
398
473
|
plan_key: value.plan_key,
|
|
@@ -402,17 +477,18 @@ export function compileTodoExtraction(value, repoRoot) {
|
|
|
402
477
|
task_id: task.task_id,
|
|
403
478
|
title: task.title,
|
|
404
479
|
lane: task.lane,
|
|
405
|
-
...(value.schema
|
|
480
|
+
...([TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(value.schema)
|
|
406
481
|
? { design_memo: task.design_memo } : {}),
|
|
407
482
|
narrative_ref: task.narrative_ref,
|
|
408
483
|
narrative_anchor: null,
|
|
409
484
|
compile_binding: null,
|
|
410
|
-
...(value.schema
|
|
485
|
+
...([TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(value.schema)
|
|
411
486
|
? { parent_task_id: task.source.parent_task_id } : {}),
|
|
412
487
|
})),
|
|
413
|
-
hard_dependencies:
|
|
488
|
+
hard_dependencies: hardDependencies,
|
|
414
489
|
joins: value.joins,
|
|
415
490
|
},
|
|
491
|
+
crossPlanDependencies,
|
|
416
492
|
narrativeAnchorSources: registered.map((task) => ({
|
|
417
493
|
task_id: task.task_id,
|
|
418
494
|
origin_plan_ref: task.source.origin_plan_ref,
|