@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/runtime-engine.mjs
CHANGED
|
@@ -32,18 +32,10 @@ import {
|
|
|
32
32
|
|
|
33
33
|
const ENGINE_ACTOR = 'lattice-runtime';
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* `commit`は含めない。自分の隔離worktreeでdetached HEADへ進めるcommitは、canonical branchを
|
|
38
|
-
* 動かさず外部へ効果を出さない一方で、進行中の成果を耐久化し、diff観測が生み出した木そのものを
|
|
39
|
-
* 縛れるようにする(ADR 0139)。
|
|
40
|
-
*
|
|
41
|
-
* 禁止のままにするのは、HEADをbaseの子孫から外す操作と、外部へ効果を出す操作である。
|
|
42
|
-
* 前者は観測の前提を壊し、後者は承認なしに行わないという公開契約に当たる。
|
|
35
|
+
* v1 packetの互換field。操作権限はhostへ委ね、Latticeからは禁止操作を配らない。
|
|
36
|
+
* worktree観測に必要な前提は、実際のHEADとdiffを観測して判定する(ADR 0178)。
|
|
43
37
|
*/
|
|
44
|
-
const FORBIDDEN_OPERATIONS = Object.freeze([
|
|
45
|
-
'push', 'branch', 'merge', 'rebase', 'reset', 'stash',
|
|
46
|
-
]);
|
|
38
|
+
const FORBIDDEN_OPERATIONS = Object.freeze([]);
|
|
47
39
|
|
|
48
40
|
function fail(reason) {
|
|
49
41
|
throw new TypeError(`runtime engine契約違反: ${reason}`);
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
BOUNDARY_MANIFEST_SCHEMA, selfDigest, validateRuntimeBoundaryManifest, validateRuntimePlan,
|
|
19
19
|
} from './runtime-contracts.mjs';
|
|
20
20
|
import { TODO_INDEPENDENCE_SCHEMA } from './todo-independence-contracts.mjs';
|
|
21
|
+
import { pullIntakeReadinessGuidance } from './todo-independence-guidance.mjs';
|
|
21
22
|
import {
|
|
22
23
|
readTodoIndependenceArtifact, readTodoStore, readTodoWitnessSet,
|
|
23
24
|
} from './todo-store.mjs';
|
|
@@ -315,6 +316,29 @@ function activeMember(store, planKey) {
|
|
|
315
316
|
return member;
|
|
316
317
|
}
|
|
317
318
|
|
|
319
|
+
async function pullPlanReadiness({ repoRoot, planKey }) {
|
|
320
|
+
const store = await readTodoStore({ repoRoot });
|
|
321
|
+
const member = activeMember(store, planKey);
|
|
322
|
+
const [artifact, witnessSet] = await Promise.all([
|
|
323
|
+
readTodoIndependenceArtifact({ repoRoot, store, planKey }),
|
|
324
|
+
readTodoWitnessSet({ repoRoot, planKey }),
|
|
325
|
+
]);
|
|
326
|
+
const ready = artifact !== null
|
|
327
|
+
&& artifact.schema === TODO_INDEPENDENCE_SCHEMA
|
|
328
|
+
&& artifact.project_id === store.project_id
|
|
329
|
+
&& artifact.plan_key === planKey
|
|
330
|
+
&& artifact.plan_version === member.plan.plan_version
|
|
331
|
+
&& artifact.topology_digest === member.plan.topology_digest
|
|
332
|
+
&& artifact.outcome === 'compiled'
|
|
333
|
+
&& witnessSet?.project_id === store.project_id
|
|
334
|
+
&& witnessSet?.plan_key === planKey
|
|
335
|
+
&& witnessSet?.witness_set_digest === artifact.witness_set_digest;
|
|
336
|
+
return pullIntakeReadinessGuidance({
|
|
337
|
+
coordinationMode: member.coordination?.mode ?? null,
|
|
338
|
+
ready,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
318
342
|
function literalEvent(member, { kind, taskId, actor }) {
|
|
319
343
|
return member.journal?.events?.findLast((event) => (
|
|
320
344
|
event.kind === kind && event.task_id === taskId
|
|
@@ -354,6 +378,10 @@ function pathTouches(boundary, changed) {
|
|
|
354
378
|
}
|
|
355
379
|
|
|
356
380
|
async function boundaryVerdict({ repoRoot, store, member, meta, taskId, intakeBase }) {
|
|
381
|
+
const pullRequired = pullIntakeReadinessGuidance({
|
|
382
|
+
coordinationMode: member.coordination?.mode ?? null,
|
|
383
|
+
ready: false,
|
|
384
|
+
});
|
|
357
385
|
let artifact;
|
|
358
386
|
let witnessSet;
|
|
359
387
|
try {
|
|
@@ -362,7 +390,7 @@ async function boundaryVerdict({ repoRoot, store, member, meta, taskId, intakeBa
|
|
|
362
390
|
readTodoWitnessSet({ repoRoot, planKey: meta.plan_key }),
|
|
363
391
|
]);
|
|
364
392
|
} catch (error) {
|
|
365
|
-
return { state: 'hold', reason: 'boundary_unverified', next_action:
|
|
393
|
+
return { state: 'hold', reason: 'boundary_unverified', next_action: pullRequired.next_action,
|
|
366
394
|
detail: { cause: error?.code ?? 'artifact_read_failed' }, artifact: null, witnessSet: null };
|
|
367
395
|
}
|
|
368
396
|
const bindingValid = artifact !== null
|
|
@@ -375,11 +403,11 @@ async function boundaryVerdict({ repoRoot, store, member, meta, taskId, intakeBa
|
|
|
375
403
|
&& witnessSet?.plan_key === meta.plan_key
|
|
376
404
|
&& witnessSet?.witness_set_digest === artifact.witness_set_digest;
|
|
377
405
|
if (!bindingValid) {
|
|
378
|
-
return { state: 'hold', reason: 'boundary_unverified', next_action:
|
|
406
|
+
return { state: 'hold', reason: 'boundary_unverified', next_action: pullRequired.next_action,
|
|
379
407
|
detail: { cause: 'artifact_binding_mismatch' }, artifact, witnessSet };
|
|
380
408
|
}
|
|
381
409
|
if (artifact.outcome !== 'compiled') {
|
|
382
|
-
return { state: 'hold', reason: 'boundary_unverified', next_action:
|
|
410
|
+
return { state: 'hold', reason: 'boundary_unverified', next_action: pullRequired.next_action,
|
|
383
411
|
detail: { cause: 'independence_outcome_unknown' }, artifact, witnessSet };
|
|
384
412
|
}
|
|
385
413
|
const boundary = boundaryFor(artifact, taskId);
|
|
@@ -603,6 +631,7 @@ export async function startPullRun({ repoRoot, runDir, runId, planKey, equipment
|
|
|
603
631
|
|| !identifier(runId) || !identifier(planKey) || equipment !== 'detached-worktree') {
|
|
604
632
|
fail('INVALID_PULL_START', 'pull start入力が不正');
|
|
605
633
|
}
|
|
634
|
+
const intakeReadiness = await pullPlanReadiness({ repoRoot, planKey });
|
|
606
635
|
const temporary = await mkdtemp(path.join(path.dirname(runDir), `.${runId}.pull-tmp-`));
|
|
607
636
|
const meta = {
|
|
608
637
|
schema: PULL_RUN_META_SCHEMA,
|
|
@@ -634,9 +663,10 @@ export async function startPullRun({ repoRoot, runDir, runId, planKey, equipment
|
|
|
634
663
|
throw error;
|
|
635
664
|
}
|
|
636
665
|
return {
|
|
637
|
-
schema: 'lattice.pull_run_start_result.
|
|
666
|
+
schema: 'lattice.pull_run_start_result.v2',
|
|
638
667
|
outcome: 'started', run_id: runId, plan_key: planKey,
|
|
639
668
|
selection: 'pull', equipment, intake_count: 0,
|
|
669
|
+
intake_readiness: intakeReadiness,
|
|
640
670
|
};
|
|
641
671
|
}
|
|
642
672
|
|
|
@@ -57,7 +57,7 @@ export function validateRunWorkOrder(value) {
|
|
|
57
57
|
&& GIT_SHA1.test(value.base_sha ?? '')
|
|
58
58
|
&& stringArray(value.scope_writes, { paths: true })
|
|
59
59
|
&& stringArray(value.verifier_refs)
|
|
60
|
-
&& stringArray(value.forbidden_operations
|
|
60
|
+
&& stringArray(value.forbidden_operations)
|
|
61
61
|
&& SHA256.test(value.packet_digest ?? '')
|
|
62
62
|
&& SHA256.test(value.order_digest ?? '')
|
|
63
63
|
&& selfDigest(value, 'order_digest') === value.order_digest;
|
package/src/todo-cli.mjs
CHANGED
|
@@ -88,6 +88,7 @@ import { compileTodoStructureOverlay } from './todo-structure-overlay.mjs';
|
|
|
88
88
|
import {
|
|
89
89
|
buildTodoStructureCompileArtifact,
|
|
90
90
|
projectTodoStructureEffective,
|
|
91
|
+
readTodoStructureArtifactDiagnostics,
|
|
91
92
|
readTodoStructureFinalizationState,
|
|
92
93
|
readTodoStructureFinalizationsForStatus,
|
|
93
94
|
readTodoStructureState,
|
|
@@ -97,7 +98,9 @@ import {
|
|
|
97
98
|
appendTodoExtraction,
|
|
98
99
|
compileTodoExtraction,
|
|
99
100
|
explainTodoExtraction,
|
|
101
|
+
isTodoExtractionConnectionOnly,
|
|
100
102
|
TODO_EXTRACTION_SCHEMA_V3,
|
|
103
|
+
TODO_EXTRACTION_SCHEMA_V4,
|
|
101
104
|
validateTodoExtraction,
|
|
102
105
|
} from './todo-migration.mjs';
|
|
103
106
|
import {
|
|
@@ -139,6 +142,7 @@ import {
|
|
|
139
142
|
} from './seam-proposal-contracts.mjs';
|
|
140
143
|
import { compileSeamProposalArtifact, declaredConcernSymbols } from './seam-proposal.mjs';
|
|
141
144
|
import { collectSensorEvidence } from './sensor-adapter.mjs';
|
|
145
|
+
import { collectTodoIndependenceAuthoritativeObservation } from './todo-independence-authoritative-observation.mjs';
|
|
142
146
|
import { applySeamProposal } from './seam-apply.mjs';
|
|
143
147
|
import { todoPlanPrecedences } from './seam-verification.mjs';
|
|
144
148
|
import {
|
|
@@ -188,7 +192,7 @@ const TODO_SCHEMA_COMMANDS = Object.freeze({
|
|
|
188
192
|
'revise-phase': {
|
|
189
193
|
title: 'lattice.phase_todo_revision.v3', file: 'lattice.phase_todo_revision.v3.schema.json',
|
|
190
194
|
},
|
|
191
|
-
migrate: { title: 'lattice.todo_extraction.
|
|
195
|
+
migrate: { title: 'lattice.todo_extraction.v4', file: 'lattice.todo_extraction.v4.schema.json' },
|
|
192
196
|
structure: {
|
|
193
197
|
title: 'lattice.todo_structure_set.v1', file: 'lattice.todo_structure_set.v1.schema.json',
|
|
194
198
|
},
|
|
@@ -418,9 +422,9 @@ async function readMigrationInput(repoRoot, inputRef, { requireValid = true } =
|
|
|
418
422
|
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
419
423
|
}
|
|
420
424
|
if (!requireValid) return extraction;
|
|
421
|
-
if (extraction?.schema
|
|
422
|
-
throw new TodoStoreError('
|
|
423
|
-
|
|
425
|
+
if (![TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(extraction?.schema)) {
|
|
426
|
+
throw new TodoStoreError('INVALID_TODO_EXTRACTION', 'todo_extraction_schema_unsupported', undefined, {
|
|
427
|
+
expected: TODO_EXTRACTION_SCHEMA_V4,
|
|
424
428
|
next_action: 'lattice todo migrate --schema --json',
|
|
425
429
|
});
|
|
426
430
|
}
|
|
@@ -1256,21 +1260,42 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
|
|
|
1256
1260
|
});
|
|
1257
1261
|
}
|
|
1258
1262
|
const registered = extraction.tasks.filter(({ disposition }) => disposition.startsWith('register_'));
|
|
1259
|
-
|
|
1263
|
+
const connectionOnly = isTodoExtractionConnectionOnly(extraction);
|
|
1264
|
+
if (registered.length === 0 && !connectionOnly) {
|
|
1265
|
+
throw new TodoStoreError('MIGRATION_EMPTY', 'no_registered_tasks');
|
|
1266
|
+
}
|
|
1260
1267
|
|
|
1261
|
-
const dispatchShape = computeTodoDispatchShapeForPlan({
|
|
1268
|
+
const dispatchShape = connectionOnly ? null : computeTodoDispatchShapeForPlan({
|
|
1262
1269
|
projectId: extraction.project_id,
|
|
1263
1270
|
planKey: extraction.plan_key,
|
|
1264
1271
|
taskIds: registered.map(({ task_id: taskId }) => taskId),
|
|
1265
1272
|
hardDependencies: extraction.hard_dependencies,
|
|
1266
1273
|
joins: extraction.joins,
|
|
1267
1274
|
});
|
|
1268
|
-
|
|
1275
|
+
if (dispatchShape !== null) {
|
|
1276
|
+
assertTodoDispatchShapeReviewed({ shape: dispatchShape, reviewed: serializationReviewed });
|
|
1277
|
+
}
|
|
1269
1278
|
|
|
1270
1279
|
const imported = await appendTodoExtraction({ repoRoot, extraction });
|
|
1280
|
+
let companion = null;
|
|
1281
|
+
if ((imported.crossPlanDependencies ?? []).length > 0) {
|
|
1282
|
+
const connectedStore = await readTodoStore({ repoRoot });
|
|
1283
|
+
const event = imported.crossPlanDependencies[0];
|
|
1284
|
+
companion = {
|
|
1285
|
+
repair: event.payload.from,
|
|
1286
|
+
target: event.payload.to,
|
|
1287
|
+
reason: event.payload.reason,
|
|
1288
|
+
event_digest: event.event_digest,
|
|
1289
|
+
connected_frontier: computeReadyFrontier(connectedStore),
|
|
1290
|
+
next_action: imported.connectionOnly === true ? 'lattice todo status --json'
|
|
1291
|
+
: isPhaselessTodoPlanSchema(imported.plan.schema)
|
|
1292
|
+
? `lattice todo revise-phase --plan ${imported.plan.plan_key} --input <phase-revision.json>`
|
|
1293
|
+
: 'lattice todo status --json',
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1271
1296
|
const result = {
|
|
1272
|
-
//
|
|
1273
|
-
schema: 'lattice.todo_migrate_result.
|
|
1297
|
+
// companionを足す時点でv4へ上げ、通常移行もnullを常在させてresult shapeを固定する。
|
|
1298
|
+
schema: 'lattice.todo_migrate_result.v4',
|
|
1274
1299
|
project_id: imported.plan.project_id,
|
|
1275
1300
|
plan_key: imported.plan.plan_key,
|
|
1276
1301
|
plan_version: imported.plan.plan_version,
|
|
@@ -1282,7 +1307,7 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
|
|
|
1282
1307
|
snapshot_ref: imported.descriptor.snapshot_ref,
|
|
1283
1308
|
topology_digest: imported.plan.topology_digest,
|
|
1284
1309
|
journal_head_digest: imported.events.at(-1).event_digest,
|
|
1285
|
-
dispatch_shape: {
|
|
1310
|
+
dispatch_shape: dispatchShape === null ? null : {
|
|
1286
1311
|
task_count: dispatchShape.task_count,
|
|
1287
1312
|
critical_path_length: dispatchShape.critical_path_length,
|
|
1288
1313
|
max_frontier_width: dispatchShape.max_frontier_width,
|
|
@@ -1291,8 +1316,8 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
|
|
|
1291
1316
|
// ADR 0147裁定3: phase無しplanの作成は拒否せず、終端監査が要ることを結果へ明示するに
|
|
1292
1317
|
// 留める。extraction経由のmigrateは常にphase無しplan(todo_plan.v2)を作るが、将来の
|
|
1293
1318
|
// 拡張に備えisPhaselessTodoPlanSchemaで動的に判定する。
|
|
1294
|
-
terminal_audit_required: isPhaselessTodoPlanSchema(imported.plan.schema),
|
|
1295
|
-
phase_guidance: isPhaselessTodoPlanSchema(imported.plan.schema) ? {
|
|
1319
|
+
terminal_audit_required: imported.connectionOnly !== true && isPhaselessTodoPlanSchema(imported.plan.schema),
|
|
1320
|
+
phase_guidance: imported.connectionOnly !== true && isPhaselessTodoPlanSchema(imported.plan.schema) ? {
|
|
1296
1321
|
capability: 'acquire_phase',
|
|
1297
1322
|
preserves_completed_state: true,
|
|
1298
1323
|
schema_command: 'lattice todo revise-phase --schema --json',
|
|
@@ -1301,11 +1326,12 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
|
|
|
1301
1326
|
} : null,
|
|
1302
1327
|
// ob03: 起票直後のplanは必ず調整方式が未宣言である。ここで案内しないと、選ぶ機会が
|
|
1303
1328
|
// 「誰も呼ぶ動機の無いdrilldown」にしか無くなる——前campaignの監査待ちと同じ形になる。
|
|
1304
|
-
coordination_guidance: {
|
|
1329
|
+
coordination_guidance: imported.connectionOnly === true ? null : {
|
|
1305
1330
|
mode: null,
|
|
1306
1331
|
modes: [...TODO_COORDINATION_MODES],
|
|
1307
1332
|
next_action: `lattice todo independence mode --plan ${imported.plan.plan_key} --set <witness|conversation> --reason <text>`,
|
|
1308
1333
|
},
|
|
1334
|
+
companion,
|
|
1309
1335
|
result_digest: '',
|
|
1310
1336
|
};
|
|
1311
1337
|
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
@@ -1399,8 +1425,9 @@ function extractionAuthoringViolations(extraction, now = new Date()) {
|
|
|
1399
1425
|
}
|
|
1400
1426
|
const registered = new Set(tasks.filter(({ disposition }) => typeof disposition === 'string'
|
|
1401
1427
|
&& disposition.startsWith('register_')).map(({ task_id: taskId }) => taskId));
|
|
1428
|
+
const connectionOnly = isTodoExtractionConnectionOnly(extraction);
|
|
1402
1429
|
const unresolvedLocal = (ref) => ref?.project_id === extraction?.project_id
|
|
1403
|
-
&& ref?.plan_key === extraction?.plan_key && !registered.has(ref?.task_id);
|
|
1430
|
+
&& ref?.plan_key === extraction?.plan_key && !connectionOnly && !registered.has(ref?.task_id);
|
|
1404
1431
|
for (const [index, edge] of edges.entries()) {
|
|
1405
1432
|
for (const side of ['from', 'to']) {
|
|
1406
1433
|
if (unresolvedLocal(edge?.[side])) {
|
|
@@ -1425,12 +1452,15 @@ async function migrateDryRun({ repoRoot, inputRef, serializationReviewed = false
|
|
|
1425
1452
|
const extraction = await readMigrationInput(repoRoot, inputRef, { requireValid: false });
|
|
1426
1453
|
const violations = [];
|
|
1427
1454
|
const tasks = Array.isArray(extraction?.tasks) ? extraction.tasks : [];
|
|
1428
|
-
const
|
|
1455
|
+
const designMemoSchema = [TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(extraction?.schema);
|
|
1456
|
+
const invalidMemos = (designMemoSchema ? tasks : []).map((task, index) => ({
|
|
1429
1457
|
task, index, explained: explainTodoDesignMemo(task?.design_memo),
|
|
1430
1458
|
})).filter(({ explained }) => !explained.valid).slice(0, 64);
|
|
1431
|
-
|
|
1459
|
+
const supportedSchema = [TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4]
|
|
1460
|
+
.includes(extraction?.schema);
|
|
1461
|
+
if (!supportedSchema) {
|
|
1432
1462
|
violations.push({ code: 'schema_retired', path: '/schema', task_ids: [],
|
|
1433
|
-
expected:
|
|
1463
|
+
expected: TODO_EXTRACTION_SCHEMA_V4,
|
|
1434
1464
|
actual: typeof extraction?.schema === 'string' ? extraction.schema
|
|
1435
1465
|
: { type: typeof extraction?.schema },
|
|
1436
1466
|
next_action: 'lattice todo migrate --schema --json' });
|
|
@@ -1441,8 +1471,7 @@ async function migrateDryRun({ repoRoot, inputRef, serializationReviewed = false
|
|
|
1441
1471
|
expected: explained.expected, actual: explained.actual,
|
|
1442
1472
|
prompt: TODO_DESIGN_MEMO_PROMPT, next_action: 'lattice todo migrate --schema --json' });
|
|
1443
1473
|
}
|
|
1444
|
-
const schemaValid =
|
|
1445
|
-
&& validateTodoExtraction(extraction);
|
|
1474
|
+
const schemaValid = supportedSchema && validateTodoExtraction(extraction);
|
|
1446
1475
|
if (!schemaValid) {
|
|
1447
1476
|
const explained = explainTodoExtraction(extraction);
|
|
1448
1477
|
if (!explained.valid && !explained.path.endsWith('/design_memo')) {
|
|
@@ -1467,28 +1496,38 @@ async function migrateDryRun({ repoRoot, inputRef, serializationReviewed = false
|
|
|
1467
1496
|
}
|
|
1468
1497
|
const registered = tasks.filter(({ disposition }) => typeof disposition === 'string'
|
|
1469
1498
|
&& disposition.startsWith('register_'));
|
|
1470
|
-
|
|
1499
|
+
const connectionOnly = schemaValid && isTodoExtractionConnectionOnly(extraction);
|
|
1500
|
+
if (registered.length === 0 && !connectionOnly) {
|
|
1471
1501
|
violations.push({ code: 'no_registered_tasks', path: '/tasks', task_ids: [],
|
|
1472
1502
|
next_action: 'register_at_least_one_task' });
|
|
1473
1503
|
}
|
|
1474
1504
|
|
|
1475
1505
|
let dispatchShape = null;
|
|
1476
1506
|
let plannedPlan = null;
|
|
1477
|
-
|
|
1507
|
+
let plannedConnection = null;
|
|
1508
|
+
if (schemaValid && unresolvedTaskIds.length === 0 && (registered.length > 0 || connectionOnly)) {
|
|
1478
1509
|
try {
|
|
1479
1510
|
const compiled = compileTodoExtraction(extraction, repoRoot);
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1511
|
+
if (connectionOnly) {
|
|
1512
|
+
plannedConnection = {
|
|
1513
|
+
source: compiled.crossPlanDependencies[0].from,
|
|
1514
|
+
target: compiled.crossPlanDependencies[0].to,
|
|
1515
|
+
reason: compiled.crossPlanDependencies[0].reason,
|
|
1516
|
+
};
|
|
1517
|
+
} else {
|
|
1518
|
+
plannedPlan = buildTodoPlan(compiled.plan);
|
|
1519
|
+
dispatchShape = computeTodoDispatchShapeForPlan({
|
|
1520
|
+
projectId: extraction.project_id, planKey: extraction.plan_key,
|
|
1521
|
+
taskIds: registered.map(({ task_id: taskId }) => taskId),
|
|
1522
|
+
hardDependencies: extraction.hard_dependencies, joins: extraction.joins,
|
|
1523
|
+
});
|
|
1524
|
+
try {
|
|
1525
|
+
assertTodoDispatchShapeReviewed({ shape: dispatchShape, reviewed: serializationReviewed });
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
violations.push({ code: error?.detail?.reason ?? 'plan_shape_too_serial', path: '/hard_dependencies',
|
|
1528
|
+
task_ids: error?.detail?.critical_path_task_ids ?? [],
|
|
1529
|
+
next_action: 'reconsider_parallel_seams_or_pass_serialization_reviewed' });
|
|
1530
|
+
}
|
|
1492
1531
|
}
|
|
1493
1532
|
} catch (error) {
|
|
1494
1533
|
plannedPlan = null;
|
|
@@ -1523,13 +1562,19 @@ async function migrateDryRun({ repoRoot, inputRef, serializationReviewed = false
|
|
|
1523
1562
|
}
|
|
1524
1563
|
const bounded = violations.slice(0, 64);
|
|
1525
1564
|
const result = {
|
|
1526
|
-
schema: 'lattice.todo_migrate_dry_run_result.
|
|
1565
|
+
schema: 'lattice.todo_migrate_dry_run_result.v2',
|
|
1527
1566
|
valid: bounded.length === 0,
|
|
1528
1567
|
project_id: isTodoIdentifier(extraction?.project_id) ? extraction.project_id : null,
|
|
1529
1568
|
plan_key: isTodoIdentifier(extraction?.plan_key) ? extraction.plan_key : null,
|
|
1530
1569
|
violations: bounded,
|
|
1531
1570
|
overflow_count: Math.max(0, violations.length - bounded.length),
|
|
1532
|
-
planned:
|
|
1571
|
+
planned: plannedConnection !== null ? {
|
|
1572
|
+
connection_only: true,
|
|
1573
|
+
source: plannedConnection.source,
|
|
1574
|
+
target: plannedConnection.target,
|
|
1575
|
+
reason: plannedConnection.reason,
|
|
1576
|
+
} : plannedPlan === null ? null : {
|
|
1577
|
+
connection_only: false,
|
|
1533
1578
|
plan_schema: plannedPlan.schema,
|
|
1534
1579
|
task_count: registered.length,
|
|
1535
1580
|
topology_digest: plannedPlan.topology_digest,
|
|
@@ -1649,13 +1694,15 @@ async function splitTodo({ repoRoot, env, planKey, inputRef }) {
|
|
|
1649
1694
|
|
|
1650
1695
|
async function status({ repoRoot }) {
|
|
1651
1696
|
const store = await readTodoStore({ repoRoot });
|
|
1652
|
-
|
|
1697
|
+
const result = projectTodoStatus(store, {
|
|
1653
1698
|
planNotes: await readTodoPlanNotesForStatus({ repoRoot, store }),
|
|
1654
1699
|
parallelCandidates: await readTodoParallelCandidatesForStatus({
|
|
1655
1700
|
repoRoot, store, gitHead: currentHeadSha, changedPathsSince,
|
|
1656
1701
|
}),
|
|
1657
1702
|
structureFinalizations: await readTodoStructureFinalizationsForStatus({ repoRoot, store }),
|
|
1658
1703
|
});
|
|
1704
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
1705
|
+
return result;
|
|
1659
1706
|
}
|
|
1660
1707
|
|
|
1661
1708
|
async function adoptDashboardRoot({ repoRoot, env }) {
|
|
@@ -2468,8 +2515,8 @@ async function structure({ repoRoot, requestedPlanKey }) {
|
|
|
2468
2515
|
|
|
2469
2516
|
async function independenceCompile({ repoRoot, planKey, inputRef }) {
|
|
2470
2517
|
const witnessSet = await readWitnessSetInput(repoRoot, inputRef);
|
|
2471
|
-
|
|
2472
|
-
const baseSha =
|
|
2518
|
+
const observation = await collectTodoIndependenceAuthoritativeObservation({ repoRoot, witnessSet });
|
|
2519
|
+
const baseSha = observation.head_sha;
|
|
2473
2520
|
const store = await readTodoStore({ repoRoot });
|
|
2474
2521
|
const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
|
|
2475
2522
|
if (!member) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active', undefined, { plan_key: planKey });
|
|
@@ -2484,7 +2531,7 @@ async function independenceCompile({ repoRoot, planKey, inputRef }) {
|
|
|
2484
2531
|
plan: member.plan,
|
|
2485
2532
|
baseSha,
|
|
2486
2533
|
compiledAt: new Date().toISOString(),
|
|
2487
|
-
sensorEvidence:
|
|
2534
|
+
sensorEvidence: observation.sensor_evidence,
|
|
2488
2535
|
previousArtifact,
|
|
2489
2536
|
});
|
|
2490
2537
|
const { ref } = await writeTodoIndependenceArtifact({ repoRoot, artifact });
|
|
@@ -3555,6 +3602,20 @@ async function verify({ repoRoot, requestedPlanKey }) {
|
|
|
3555
3602
|
},
|
|
3556
3603
|
};
|
|
3557
3604
|
});
|
|
3605
|
+
const structureDiagnostics = [];
|
|
3606
|
+
for (const member of members) {
|
|
3607
|
+
const diagnostics = await readTodoStructureArtifactDiagnostics({
|
|
3608
|
+
repoRoot,
|
|
3609
|
+
store: { ...store, members: [member] },
|
|
3610
|
+
});
|
|
3611
|
+
structureDiagnostics.push(...diagnostics);
|
|
3612
|
+
}
|
|
3613
|
+
if (structureDiagnostics.length > 0) {
|
|
3614
|
+
throw new TodoStoreError(
|
|
3615
|
+
'STRUCTURE_ARTIFACT_INVALID', structureDiagnostics[0].reason, undefined,
|
|
3616
|
+
{ diagnostics: structureDiagnostics },
|
|
3617
|
+
);
|
|
3618
|
+
}
|
|
3558
3619
|
const result = {
|
|
3559
3620
|
schema: 'lattice.todo_verify_result.v3',
|
|
3560
3621
|
project_id: store.project_id,
|
|
@@ -3857,10 +3918,13 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3857
3918
|
action = (repoRoot) => migrateDryRun({
|
|
3858
3919
|
repoRoot, inputRef: argv[2], serializationReviewed: argv.length === 6,
|
|
3859
3920
|
});
|
|
3860
|
-
} else if ((argv.length === 3 || argv.length === 4
|
|
3861
|
-
&&
|
|
3921
|
+
} else if ((argv.length === 3 || argv.length === 4 || argv.length === 5)
|
|
3922
|
+
&& argv[0] === 'migrate' && argv[1] === '--input' && isTodoRef(argv[2])
|
|
3923
|
+
&& (argv.length === 3
|
|
3924
|
+
|| (argv.length === 4 && ['--json', '--serialization-reviewed'].includes(argv[3]))
|
|
3925
|
+
|| (argv.length === 5 && argv[3] === '--serialization-reviewed' && argv[4] === '--json'))) {
|
|
3862
3926
|
action = (repoRoot) => migrate({
|
|
3863
|
-
repoRoot, inputRef: argv[2], serializationReviewed: argv
|
|
3927
|
+
repoRoot, inputRef: argv[2], serializationReviewed: argv[3] === '--serialization-reviewed',
|
|
3864
3928
|
});
|
|
3865
3929
|
} else if (argv.length === 5 && argv[0] === 'revise'
|
|
3866
3930
|
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { gitSpawnSync } from './git-process.mjs';
|
|
6
|
+
import { runSensorCli } from './sensor-cli.mjs';
|
|
7
|
+
import { collectWitnessSensorEvidence } from './todo-independence.mjs';
|
|
8
|
+
|
|
9
|
+
export class TodoIndependenceObservationError extends Error {
|
|
10
|
+
constructor(code, reason, detail = {}) {
|
|
11
|
+
super(reason);
|
|
12
|
+
this.name = 'TodoIndependenceObservationError';
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.detail = { reason, ...detail };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fail(code, reason, detail = {}) {
|
|
19
|
+
throw new TodoIndependenceObservationError(code, reason, detail);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function git({ cwd, args, operation }) {
|
|
23
|
+
const result = gitSpawnSync(args, {
|
|
24
|
+
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
25
|
+
});
|
|
26
|
+
if (result.error !== undefined || result.status !== 0 || result.signal !== null) {
|
|
27
|
+
fail('INDEPENDENCE_OBSERVATION_GIT_FAILED', 'observation_git_command_failed', {
|
|
28
|
+
operation, status: result.status ?? null, signal: result.signal ?? null,
|
|
29
|
+
cause: result.error?.message ?? null,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return result.stdout.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function memoryStream() {
|
|
36
|
+
let value = '';
|
|
37
|
+
return {
|
|
38
|
+
stream: { write: (chunk) => { value += String(chunk); } },
|
|
39
|
+
read: () => value,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function initializeSensor(observationRoot) {
|
|
44
|
+
const stdout = memoryStream();
|
|
45
|
+
const stderr = memoryStream();
|
|
46
|
+
const status = await runSensorCli({
|
|
47
|
+
argv: ['init', observationRoot, '--json'],
|
|
48
|
+
stdout: stdout.stream,
|
|
49
|
+
stderr: stderr.stream,
|
|
50
|
+
});
|
|
51
|
+
if (status !== 0) {
|
|
52
|
+
fail('INDEPENDENCE_OBSERVATION_SENSOR_INIT_FAILED', 'observation_sensor_init_failed', {
|
|
53
|
+
sensor_error: stderr.read(),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 共有repoのdirtyな状態を観測せず、current HEADのclean worktreeでsensorを実行する。 */
|
|
59
|
+
export async function collectTodoIndependenceAuthoritativeObservation({ repoRoot, witnessSet } = {}) {
|
|
60
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0
|
|
61
|
+
|| witnessSet === null || typeof witnessSet !== 'object') {
|
|
62
|
+
fail('INDEPENDENCE_OBSERVATION_INPUT_INVALID', 'observation_input_invalid');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const headSha = git({
|
|
66
|
+
cwd: repoRoot, args: ['rev-parse', '--verify', 'HEAD^{commit}'], operation: 'resolve_head',
|
|
67
|
+
});
|
|
68
|
+
const temporaryRoot = await mkdtemp(path.join(tmpdir(), 'lattice-independence-observation-'));
|
|
69
|
+
const observationRoot = path.join(temporaryRoot, 'worktree');
|
|
70
|
+
let registered = false;
|
|
71
|
+
let operationError = null;
|
|
72
|
+
let result;
|
|
73
|
+
try {
|
|
74
|
+
git({
|
|
75
|
+
cwd: repoRoot,
|
|
76
|
+
args: ['worktree', 'add', '--detach', '--quiet', observationRoot, headSha],
|
|
77
|
+
operation: 'worktree_add',
|
|
78
|
+
});
|
|
79
|
+
registered = true;
|
|
80
|
+
const observedHead = git({
|
|
81
|
+
cwd: observationRoot, args: ['rev-parse', '--verify', 'HEAD^{commit}'],
|
|
82
|
+
operation: 'verify_observation_head',
|
|
83
|
+
});
|
|
84
|
+
if (observedHead !== headSha) {
|
|
85
|
+
fail('INDEPENDENCE_OBSERVATION_HEAD_MISMATCH', 'observation_head_mismatch', {
|
|
86
|
+
expected_head_sha: headSha, actual_head_sha: observedHead,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
await initializeSensor(observationRoot);
|
|
90
|
+
result = await collectWitnessSensorEvidence({ cwd: observationRoot, witnessSet });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
operationError = error;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let cleanupError = null;
|
|
96
|
+
try {
|
|
97
|
+
if (registered) {
|
|
98
|
+
git({
|
|
99
|
+
cwd: repoRoot,
|
|
100
|
+
args: ['worktree', 'remove', '--force', observationRoot],
|
|
101
|
+
operation: 'worktree_remove',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
105
|
+
} catch (error) {
|
|
106
|
+
cleanupError = error;
|
|
107
|
+
}
|
|
108
|
+
if (cleanupError !== null) {
|
|
109
|
+
fail('INDEPENDENCE_OBSERVATION_CLEANUP_FAILED', 'observation_cleanup_failed', {
|
|
110
|
+
operation_error: operationError?.message ?? null,
|
|
111
|
+
cleanup_error: cleanupError?.message ?? String(cleanupError),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (operationError !== null) throw operationError;
|
|
115
|
+
return { head_sha: headSha, sensor_evidence: result };
|
|
116
|
+
}
|
|
@@ -154,6 +154,19 @@ const CATALOG = Object.freeze({
|
|
|
154
154
|
}),
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
const PULL_INTAKE_READINESS = Object.freeze({
|
|
158
|
+
required: Object.freeze({
|
|
159
|
+
code: 'pull_independence_required',
|
|
160
|
+
message: 'pull設備のintakeには有効なindependence artifactが必須である。conversation調整はwitness督促を消すが、この実行前提は満たさない。',
|
|
161
|
+
next_action: 'compile_independence_or_choose_non_pull_execution',
|
|
162
|
+
}),
|
|
163
|
+
ready: Object.freeze({
|
|
164
|
+
code: 'pull_independence_ready',
|
|
165
|
+
message: 'pull設備が読むplan-level independence bindingは有効である。task固有境界とruntime競合はintake時に検査する。',
|
|
166
|
+
next_action: 'none',
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
|
|
157
170
|
/** 切断可能性の言い換え。conflictの案内へ添える。 */
|
|
158
171
|
const SEVERABILITY_HINT = Object.freeze({
|
|
159
172
|
code_seam: 'symbol/pathの衝突なので、境界を分けるrefactorで並列化しうる。記録済みの競合からseam-proposal compileを検討できる。',
|
|
@@ -173,6 +186,18 @@ export function todoIndependenceGuidance(code, { severability = null } = {}) {
|
|
|
173
186
|
};
|
|
174
187
|
}
|
|
175
188
|
|
|
189
|
+
export function pullIntakeReadinessGuidance({ coordinationMode = null, ready }) {
|
|
190
|
+
if (![null, 'witness', 'conversation'].includes(coordinationMode)
|
|
191
|
+
|| typeof ready !== 'boolean') {
|
|
192
|
+
throw new TypeError('pull intake readiness input is invalid');
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
...(ready ? PULL_INTAKE_READINESS.ready : PULL_INTAKE_READINESS.required),
|
|
196
|
+
plan_binding_ready: ready,
|
|
197
|
+
coordination_mode: coordinationMode,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
176
201
|
/**
|
|
177
202
|
* 記録済みの宣言膨張を、AIが分割を検討するための助言へ写す。
|
|
178
203
|
*
|