@quolu/lattice 0.40.1 → 0.41.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.40.1",
3
+ "version": "0.41.0",
4
4
  "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
5
5
  "author": {
6
6
  "name": "Quo / クオ at kitepon.dev",
package/src/cli-help.mjs CHANGED
@@ -30,14 +30,18 @@ Commands:
30
30
  create --schema --json # 既定は最新版(v4)のJSON Schemaを返す
31
31
  create --schema-version <1|2|3|4> --json
32
32
  show <plan_key> --json # task・依存・phase・状態をplan本体から1コマンドで投影する
33
- compile --request <request.json>
33
+ compile --request <request.json> [--todo-plan <key>]
34
34
  compile --schema --json # lattice.run_request.v1 の JSON Schema を出す
35
35
  verify --request <request.json> --plan <plan.json>
36
36
  `,
37
37
  run: `Usage: lattice run <command> [options]
38
38
 
39
39
  Commands:
40
+ start --request <request.json> --plan <compile-artifact.json> --executor <adapter>
41
+ # TODO storeと連携して同一repo writerを並列起動する場合のgate付き入口。
42
+ # artifactはplan compile --todo-plan <key>で現revisionへ束縛して発行する
40
43
  start --request <request.json> --executor <adapter>
44
+ # artifactを消費しない既存runtime互換入口
41
45
  start --schema --json # lattice.run_request.v1 の JSON Schema を出す
42
46
  adapter register --input <descriptor.json>
43
47
  adapter register --schema --json # 登録入力の JSON Schema を出す
@@ -81,6 +81,8 @@ import { createRuntimeControlRequest, validateRuntimeControlResponse } from './r
81
81
  import { createRuntimeControlStore } from './runtime-control-store.mjs';
82
82
  import { createRuntimeGateStore } from './runtime-gate-store.mjs';
83
83
  import { acquireRuntimeLifecycleLock } from './runtime-lifecycle-lock.mjs';
84
+ import { readTodoIndependenceArtifact, readTodoStore } from './todo-store.mjs';
85
+ import { projectIndependenceFrontier } from './todo-independence.mjs';
84
86
  import {
85
87
  AdapterRegistryError,
86
88
  listRuntimeAdapters,
@@ -130,6 +132,47 @@ const CLI_ERROR_SCHEMA = 'lattice.cli_error.v2';
130
132
  const RUN_STORE_ROOT = ['.lattice', 'runs'];
131
133
  const RUN_REF = /^\.lattice\/runs\/([0-9A-Za-z](?:[0-9A-Za-z._-]{0,127}))$/u;
132
134
  const KNOWN_ADAPTERS = Object.freeze(['scripted', 'isolated-worktree', 'actual-agent']);
135
+
136
+ function sameTextSet(left, right) {
137
+ return Array.isArray(left) && Array.isArray(right)
138
+ && left.length === right.length
139
+ && [...left].sort().every((value, index) => value === [...right].sort()[index]);
140
+ }
141
+
142
+ function compileArtifactBodyIsValid(artifact, request) {
143
+ const keys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest',
144
+ 'todo_plan_binding', 'result_digest'];
145
+ if (artifact === null || typeof artifact !== 'object' || Array.isArray(artifact)
146
+ || Object.keys(artifact).sort().join('\0') !== keys.sort().join('\0')
147
+ || artifact.schema !== COMPILE_RESULT_SCHEMA) return false;
148
+ const { result_digest: claimedDigest, ...body } = artifact;
149
+ if (claimedDigest !== digestArtifact(body) || artifact.request_digest !== request.request_digest
150
+ || !validateRuntimePlan(artifact.plan)
151
+ || !verifyRuntimePlanBinding({ plan: artifact.plan, request })) return false;
152
+ const nodeIds = artifact.plan.nodes.map((node) => node.todo_id);
153
+ if (artifact.manifests === null || typeof artifact.manifests !== 'object'
154
+ || Array.isArray(artifact.manifests)
155
+ || !sameTextSet(Object.keys(artifact.manifests), nodeIds)) return false;
156
+ return nodeIds.every((todoId) => validateRuntimeBoundaryManifest(artifact.manifests[todoId])
157
+ && artifact.manifests[todoId].manifest_digest === artifact.plan.manifest_digests[todoId]);
158
+ }
159
+
160
+ function todoPlanBindingFor(member) {
161
+ return {
162
+ project_id: member.plan.project_id,
163
+ plan_key: member.plan.plan_key,
164
+ plan_version: member.plan.plan_version,
165
+ topology_digest: member.plan.topology_digest,
166
+ plan_digest: member.plan.plan_digest,
167
+ };
168
+ }
169
+
170
+ function sameTodoPlanBinding(left, right) {
171
+ return left !== null && typeof left === 'object' && !Array.isArray(left)
172
+ && Object.keys(left).sort().join('\0') === ['project_id', 'plan_key', 'plan_version',
173
+ 'topology_digest', 'plan_digest'].sort().join('\0')
174
+ && Object.entries(right).every(([key, value]) => left[key] === value);
175
+ }
133
176
  /**
134
177
  * 自動escalationがlifecycle lockを待つ上限(ADR 0143)。
135
178
  *
@@ -396,9 +439,27 @@ async function compileFromRepo({ request, cwd, planRef, planEpoch, predecessorRe
396
439
  });
397
440
  }
398
441
 
399
- async function planCompile({ requestPath, cwd, stdout }) {
442
+ async function planCompile({ requestPath, todoPlanKey = null, cwd, stdout }) {
400
443
  const request = await loadRequest(requestPath);
401
444
  await resolveRepoBinding(cwd, request);
445
+ let todoPlanBinding = null;
446
+ if (todoPlanKey !== null) {
447
+ const store = await readTodoStore({ repoRoot: cwd });
448
+ const member = store.members.find(({ descriptor }) => descriptor.plan_key === todoPlanKey);
449
+ if (member === undefined) {
450
+ throw new CliContractError('TODO_PLAN_NOT_ACTIVE', '指定されたTODO planはactiveではない', {
451
+ plan_key: todoPlanKey, next_action: 'select_an_active_todo_plan',
452
+ });
453
+ }
454
+ if (!sameTextSet(member.plan.tasks.map(({ task_id: taskId }) => taskId),
455
+ request.todos.map(({ todo_id: todoId }) => todoId))) {
456
+ throw new CliContractError('TODO_PLAN_TASK_MISMATCH',
457
+ 'run requestのTODO集合がactive TODO planと一致しない', {
458
+ plan_key: todoPlanKey, next_action: 'compile_a_request_for_the_current_plan_revision',
459
+ });
460
+ }
461
+ todoPlanBinding = todoPlanBindingFor(member);
462
+ }
402
463
  const result = await compileFromRepo({
403
464
  request,
404
465
  cwd,
@@ -416,6 +477,7 @@ async function planCompile({ requestPath, cwd, stdout }) {
416
477
  manifests: result.manifests,
417
478
  schedule: result.schedule,
418
479
  graph_digest: result.graph_digest,
480
+ todo_plan_binding: todoPlanBinding,
419
481
  };
420
482
  artifact.result_digest = digestArtifact(artifact);
421
483
  stdout.write(`${JSON.stringify(artifact)}\n`);
@@ -526,7 +588,8 @@ async function readRunStore(runDir) {
526
588
  path.join(runDir, 'plan-compile-result.json'), 'plan compile result',
527
589
  );
528
590
  const request = await readBoundedJson(path.join(runDir, 'request.json'), 'run request');
529
- const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest', 'result_digest'];
591
+ const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest',
592
+ 'todo_plan_binding', 'result_digest'];
530
593
  const metaKeys = ['schema', 'run_id', 'executor_adapter', 'plan_digest'];
531
594
  const { result_digest: claimedCompileDigest, ...compileBody } = compileArtifact ?? {};
532
595
  const legacyMetaValid = meta !== null && typeof meta === 'object' && !Array.isArray(meta)
@@ -1090,7 +1153,76 @@ function isDistributedScriptedControllerActivation(activation) {
1090
1153
  ));
1091
1154
  }
1092
1155
 
1093
- async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
1156
+ async function verifyRunStartGate({ request, compileArtifact, repoRoot }) {
1157
+ if (!compileArtifactBodyIsValid(compileArtifact, request)) {
1158
+ throw new CliContractError('INVALID_PLAN_ARTIFACT',
1159
+ 'compile artifactのdigest・request binding・plan/manifest bindingが不正', {
1160
+ next_action: 'rerun_lattice_plan_compile_then_pass_its_artifact',
1161
+ });
1162
+ }
1163
+ if (compileArtifact.todo_plan_binding === null) {
1164
+ throw new CliContractError('COMPILE_ARTIFACT_UNBOUND',
1165
+ 'compile artifactがTODO plan revisionへ束縛されていない', {
1166
+ next_action: 'rerun_lattice_plan_compile_with_todo_plan',
1167
+ });
1168
+ }
1169
+ const store = await readTodoStore({ repoRoot });
1170
+ const binding = compileArtifact.todo_plan_binding;
1171
+ const member = store.members.find(({ descriptor }) => descriptor.plan_key === binding.plan_key);
1172
+ if (member === undefined || !sameTodoPlanBinding(binding, todoPlanBindingFor(member))) {
1173
+ throw new CliContractError('STALE_TODO_PLAN_BINDING',
1174
+ 'compile artifactのTODO plan version/digestが現在のactive revisionと一致しない', {
1175
+ plan_key: binding.plan_key,
1176
+ next_action: 'recompile_for_the_current_todo_plan_revision',
1177
+ });
1178
+ }
1179
+ const taskIds = compileArtifact.plan.nodes.map(({ todo_id: todoId }) => todoId);
1180
+ if (!sameTextSet(member.plan.tasks.map(({ task_id: taskId }) => taskId), taskIds)) {
1181
+ throw new CliContractError('TODO_PLAN_TASK_MISMATCH',
1182
+ 'compile artifactのtask集合が現在のTODO planと一致しない', {
1183
+ plan_key: binding.plan_key,
1184
+ next_action: 'recompile_for_the_current_todo_plan_revision',
1185
+ });
1186
+ }
1187
+ const independenceArtifact = await readTodoIndependenceArtifact({
1188
+ repoRoot, store, planKey: binding.plan_key,
1189
+ });
1190
+ const currentHead = await runGit(['rev-parse', 'HEAD'], repoRoot);
1191
+ if (currentHead.code !== 0) throw new CliContractError('REPO_UNRESOLVED', 'cwdのgit HEADを解決できない');
1192
+ const currentBaseSha = currentHead.stdout.trim();
1193
+ const projected = projectIndependenceFrontier({
1194
+ artifact: independenceArtifact,
1195
+ readyTaskIds: taskIds,
1196
+ activeTaskIds: [],
1197
+ plan: member.plan,
1198
+ currentBaseSha,
1199
+ changedPaths: null,
1200
+ });
1201
+ if (projected.coverage !== 'verified') {
1202
+ throw new CliContractError('PARALLEL_GROUP_UNVERIFIED',
1203
+ 'TODO independence recordがverifiedではないため並列writerを起動できない', {
1204
+ coverage: projected.coverage,
1205
+ next_action: 'lattice todo independence compile --plan ' + binding.plan_key + ' --input <witness.json>',
1206
+ });
1207
+ }
1208
+ for (const wave of compileArtifact.schedule.waves) {
1209
+ if (wave.todo_ids.length < 2) continue;
1210
+ const verified = projected.frontier.parallel_groups.some((group) => (
1211
+ sameTextSet(group.task_ids, wave.todo_ids)
1212
+ ));
1213
+ if (!verified) {
1214
+ throw new CliContractError('PARALLEL_GROUP_UNVERIFIED',
1215
+ 'compile artifactが同時dispatchするtask群はverified parallel groupではない', {
1216
+ task_ids: wave.todo_ids,
1217
+ coverage: projected.coverage,
1218
+ unknown: projected.frontier.unknown,
1219
+ next_action: 'compile_independence_for_the_current_plan_then_recompile_the_run_plan',
1220
+ });
1221
+ }
1222
+ }
1223
+ }
1224
+
1225
+ async function runStart({ requestPath, planPath = null, executorAdapter, cwd, stdout }) {
1094
1226
  // --executor省略時の暗黙fallbackは持たない(Decision 8)。未知adapterはtyped reject。
1095
1227
  if (!KNOWN_ADAPTERS.includes(executorAdapter)) {
1096
1228
  throw new CliContractError('UNKNOWN_ADAPTER', `未知のexecutor adapter: ${executorAdapter}`);
@@ -1104,30 +1236,32 @@ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
1104
1236
  await requireSafeRunAncestors(repoRoot);
1105
1237
  await requireIgnoredRunStore(repoRoot);
1106
1238
  await resolveRepoBinding(repoRoot, request);
1107
- const result = await compileFromRepo({
1108
- request,
1109
- cwd: repoRoot,
1110
- planRef: `plan-${request.request_id}-e1`,
1111
- planEpoch: 1,
1112
- predecessorRefs: [],
1113
- });
1114
- if (result.outcome !== 'dispatchable') {
1115
- throw new CliContractError(result.code, 'dispatchable planを発行できない', result.detail);
1239
+ let compileArtifact;
1240
+ if (planPath === null) {
1241
+ // 旧public入口はartifactを消費しないため、このgateの対象ではない。互換入口を
1242
+ // 改変せず残し、TODO storeを持つ統合経路は下の--plan必須入口を使う。
1243
+ const result = await compileFromRepo({
1244
+ request, cwd: repoRoot, planRef: `plan-${request.request_id}-e1`, planEpoch: 1,
1245
+ predecessorRefs: [],
1246
+ });
1247
+ if (result.outcome !== 'dispatchable') {
1248
+ throw new CliContractError(result.code, 'dispatchable planを発行できない', result.detail);
1249
+ }
1250
+ compileArtifact = {
1251
+ schema: COMPILE_RESULT_SCHEMA, request_digest: request.request_digest, plan: result.plan,
1252
+ manifests: result.manifests, schedule: result.schedule, graph_digest: result.graph_digest,
1253
+ todo_plan_binding: null,
1254
+ };
1255
+ compileArtifact.result_digest = digestArtifact(compileArtifact);
1256
+ } else {
1257
+ compileArtifact = await readBoundedJson(planPath, 'plan artifact');
1258
+ await verifyRunStartGate({ request, compileArtifact, repoRoot });
1116
1259
  }
1117
- const compileArtifact = {
1118
- schema: COMPILE_RESULT_SCHEMA,
1119
- request_digest: request.request_digest,
1120
- plan: result.plan,
1121
- manifests: result.manifests,
1122
- schedule: result.schedule,
1123
- graph_digest: result.graph_digest,
1124
- };
1125
- compileArtifact.result_digest = digestArtifact(compileArtifact);
1126
1260
  const events = initializeRunEvents({
1127
1261
  runId: request.request_id,
1128
1262
  request,
1129
- plan: result.plan,
1130
- manifests: result.manifests,
1263
+ plan: compileArtifact.plan,
1264
+ manifests: compileArtifact.manifests,
1131
1265
  recordedAt: new Date().toISOString().replace(/\.\d+Z$/u, '.000Z'),
1132
1266
  });
1133
1267
  const runDir = runStorePath(repoRoot, request.request_id);
@@ -1145,7 +1279,7 @@ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
1145
1279
  schema: 'lattice.run_meta.v1',
1146
1280
  run_id: request.request_id,
1147
1281
  executor_adapter: executorAdapter,
1148
- plan_digest: result.plan.plan_digest,
1282
+ plan_digest: compileArtifact.plan.plan_digest,
1149
1283
  };
1150
1284
  try {
1151
1285
  await writeJsonFile(path.join(temporaryDir, 'request.json'), request);
@@ -1165,7 +1299,7 @@ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
1165
1299
  run_id: request.request_id,
1166
1300
  run_dir: path.relative(repoRoot, runDir),
1167
1301
  executor_adapter: executorAdapter,
1168
- plan_digest: result.plan.plan_digest,
1302
+ plan_digest: compileArtifact.plan.plan_digest,
1169
1303
  events_digest: digestArtifact(events.map(({ event_digest: digest }) => digest)),
1170
1304
  };
1171
1305
  output.result_digest = digestArtifact(output);
@@ -3901,6 +4035,13 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
3901
4035
  && argv[0] === 'plan' && argv[1] === 'compile' && argv[2] === '--request'
3902
4036
  && typeof argv[3] === 'string' && argv[3].length > 0) {
3903
4037
  action = () => planCompile({ requestPath: path.resolve(cwd, argv[3]), cwd, stdout });
4038
+ } else if (argv.length === 6
4039
+ && argv[0] === 'plan' && argv[1] === 'compile' && argv[2] === '--request'
4040
+ && typeof argv[3] === 'string' && argv[3].length > 0
4041
+ && argv[4] === '--todo-plan' && typeof argv[5] === 'string' && argv[5].length > 0) {
4042
+ action = () => planCompile({
4043
+ requestPath: path.resolve(cwd, argv[3]), todoPlanKey: argv[5], cwd, stdout,
4044
+ });
3904
4045
  } else if (argv.length === 6
3905
4046
  && argv[0] === 'plan' && argv[1] === 'verify'
3906
4047
  && argv[2] === '--request' && typeof argv[3] === 'string' && argv[3].length > 0
@@ -3921,6 +4062,15 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
3921
4062
  cwd,
3922
4063
  stdout,
3923
4064
  });
4065
+ } else if (argv.length === 8
4066
+ && argv[0] === 'run' && argv[1] === 'start'
4067
+ && argv[2] === '--request' && typeof argv[3] === 'string' && argv[3].length > 0
4068
+ && argv[4] === '--plan' && typeof argv[5] === 'string' && argv[5].length > 0
4069
+ && argv[6] === '--executor' && typeof argv[7] === 'string' && argv[7].length > 0) {
4070
+ action = () => runStart({
4071
+ requestPath: path.resolve(cwd, argv[3]), planPath: path.resolve(cwd, argv[5]),
4072
+ executorAdapter: argv[7], cwd, stdout,
4073
+ });
3924
4074
  } else if (argv.length === 4
3925
4075
  && argv[0] === 'run' && argv[1] === 'activate'
3926
4076
  && argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0) {
@@ -215,7 +215,8 @@ async function replaceDurableJson(directory, name, value) {
215
215
  }
216
216
 
217
217
  function validateLegacyInputs({ request, compileArtifact, legacyMeta }) {
218
- const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest', 'result_digest'];
218
+ const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest',
219
+ 'todo_plan_binding', 'result_digest'];
219
220
  const metaKeys = ['schema', 'run_id', 'executor_adapter', 'plan_digest'];
220
221
  const compileBody = { ...compileArtifact };
221
222
  delete compileBody.result_digest;