@quolu/lattice 0.12.21 → 0.12.23

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.12.21",
3
+ "version": "0.12.23",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli-help.mjs CHANGED
@@ -45,6 +45,7 @@ Commands:
45
45
 
46
46
  Read commands:
47
47
  status [--json]
48
+ bindings [--plan <key>] [--json] # compile_binding付きTaskをTODO identityつきで投影する
48
49
  verify [--plan <key>] [--json]
49
50
  snapshot --rebuild --plan <key>
50
51
  gantt [--out <file>] [--scope live|all] # 既定live: 完走した工程を図から除く(一覧には残る)
@@ -110,6 +111,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
110
111
  'run list': 'run list --json',
111
112
  'event verify': 'event verify --run .lattice/runs/<id>',
112
113
  'todo status': 'todo status [--json]',
114
+ 'todo bindings': 'todo bindings [--plan <key>] [--json]',
113
115
  'todo verify': 'todo verify [--plan <key>] [--json]',
114
116
  'todo snapshot': 'todo snapshot --rebuild --plan <key>',
115
117
  'todo gantt': 'todo gantt [--out <file>] [--scope live|all] | status [--out <file>] | serve --port <port> [--scope live|all]',
@@ -1230,6 +1230,20 @@ export function reconstructHoldResultFromJournal({ journal, runId, requestId, in
1230
1230
  return result;
1231
1231
  }
1232
1232
 
1233
+ /** 現在registryが持つadapter kindを返す。registryが無ければ空配列(推測で埋めない)。 */
1234
+ async function registeredAdapterKinds(repoRoot) {
1235
+ try {
1236
+ const registry = JSON.parse(await readFile(
1237
+ path.join(repoRoot, '.lattice/runtime/adapter-registry/registry.json'), 'utf8',
1238
+ ));
1239
+ if (!Array.isArray(registry?.entries)) return [];
1240
+ return registry.entries
1241
+ .map((entry) => entry?.adapter_kind)
1242
+ .filter((kind) => typeof kind === 'string')
1243
+ .sort();
1244
+ } catch { return []; }
1245
+ }
1246
+
1233
1247
  async function runActivate({ runDir, runRef, repoRoot, stdout, requestId = null }) {
1234
1248
  return withLifecycleLock(runDir, async () => {
1235
1249
  const { events, meta, managed } = await readRunStore(runDir);
@@ -1280,6 +1294,17 @@ async function runActivate({ runDir, runRef, repoRoot, stdout, requestId = null
1280
1294
  }
1281
1295
  await new Promise((resolve) => setTimeout(resolve, 20));
1282
1296
  }
1297
+ // ADAPTER_NOT_REGISTEREDは「registryが無い/そのadapterのentryが無い」だけを意味し、
1298
+ // 何をどこへ置けば解決するかをそれ自体は伝えない。行き止まりにしないため、
1299
+ // 必要な成果物と現在registryが持つadapterを返す(ADR 0123と同じdiagnosability規律)。
1300
+ if (code === 'ADAPTER_NOT_REGISTERED') {
1301
+ throw new CliContractError(code, `managed activateが${response.outcome}で終了した: ${response.result?.unmet?.[1] ?? code}`, {
1302
+ adapter_kind: meta?.executor_adapter ?? null,
1303
+ required_artifact: '.lattice/runtime/adapter-registry/registry.json',
1304
+ registered_adapters: await registeredAdapterKinds(repoRoot),
1305
+ reason: 'run activateは登録済みexecutor adapterを要求する。adapter登録は現時点で公開CLI面ではない',
1306
+ });
1307
+ }
1283
1308
  throw new CliContractError(code, `managed activateが${response.outcome}で終了した: ${response.result?.unmet?.[1] ?? code}`);
1284
1309
  }
1285
1310
  // daemonがpointerまでcommitしたことを再読してから成功を返す。
package/src/todo-cli.mjs CHANGED
@@ -46,7 +46,7 @@ import {
46
46
  appendTodoExtraction,
47
47
  validateTodoExtraction,
48
48
  } from './todo-migration.mjs';
49
- import { projectTodoStatus } from './todo-status.mjs';
49
+ import { projectTodoBindings, projectTodoStatus } from './todo-status.mjs';
50
50
  import {
51
51
  parseTodoSourceRef, todoLegacyReconciliationDigest, validatePhaseTodoRevision,
52
52
  validateTodoRevision, validateTodoRevisionSet,
@@ -474,6 +474,10 @@ async function status({ repoRoot }) {
474
474
  return projectTodoStatus(await readTodoStore({ repoRoot }));
475
475
  }
476
476
 
477
+ async function bindings({ repoRoot, requestedPlanKey }) {
478
+ return projectTodoBindings(await readTodoStore({ repoRoot }), { requestedPlanKey });
479
+ }
480
+
477
481
  async function readNarrative(repoRoot, ref) {
478
482
  const canonicalRoot = await realpath(repoRoot);
479
483
  const source = parseTodoSourceRef(ref);
@@ -926,6 +930,13 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
926
930
  if ((argv.length === 1 && argv[0] === 'status')
927
931
  || (argv.length === 2 && argv[0] === 'status' && argv[1] === '--json')) {
928
932
  action = (repoRoot) => status({ repoRoot });
933
+ } else if ((argv.length === 1 && argv[0] === 'bindings')
934
+ || (argv.length === 2 && argv[0] === 'bindings' && argv[1] === '--json')) {
935
+ action = (repoRoot) => bindings({ repoRoot, requestedPlanKey: null });
936
+ } else if ((argv.length === 3 || argv.length === 4) && argv[0] === 'bindings'
937
+ && argv[1] === '--plan' && isTodoIdentifier(argv[2])
938
+ && (argv.length === 3 || argv[3] === '--json')) {
939
+ action = (repoRoot) => bindings({ repoRoot, requestedPlanKey: argv[2] });
929
940
  } else if ((argv.length === 1 && argv[0] === 'verify')
930
941
  || (argv.length === 2 && argv[0] === 'verify' && argv[1] === '--json')) {
931
942
  action = (repoRoot) => verify({ repoRoot, requestedPlanKey: null });
@@ -338,3 +338,63 @@ export function projectTodoStatus(readModel) {
338
338
  }
339
339
  return result;
340
340
  }
341
+
342
+ export const TODO_BINDING_PROJECTION_SCHEMA = 'lattice.todo_binding_projection.v1';
343
+
344
+ /**
345
+ * `compile_binding`が設定されたTaskだけを、TODO正本のidentityつきで投影する(ADR 0124)。
346
+ *
347
+ * これはTODO工程storeとruntime実行を結ぶ唯一の公開読み取り面である。host は
348
+ * `compiled_plan_digest`で`runtime_plan.v1`を、`base_sha`でrun requestのbaseを照合し、
349
+ * plan→`executor_packet.v1`→`executor_receipt.v1`(`packet_digest`帰属)まで辿れる。
350
+ *
351
+ * `todo_status_result.v4`は変更しない。binding投影は加算の別面とし、v4を受理する
352
+ * 既存hostを壊さない。
353
+ */
354
+ export function projectTodoBindings(readModel, { requestedPlanKey = null } = {}) {
355
+ if (!plain(readModel) || readModel.schema !== 'lattice.todo_store_read.v1'
356
+ || !isTodoIdentifier(readModel.project_id) || !Array.isArray(readModel.members)) {
357
+ fail('TODO_STATUS_INVALID_INPUT', 'todo_status_read_model_invalid');
358
+ }
359
+ if (requestedPlanKey !== null && !isTodoIdentifier(requestedPlanKey)) {
360
+ fail('TODO_STATUS_INVALID_INPUT', 'todo_binding_plan_key_invalid');
361
+ }
362
+ const members = [...readModel.members].sort((left, right) => {
363
+ const leftKey = left?.plan?.plan_key ?? '';
364
+ const rightKey = right?.plan?.plan_key ?? '';
365
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
366
+ });
367
+ const bindings = [];
368
+ let matchedPlan = requestedPlanKey === null;
369
+ for (const member of members) {
370
+ const plan = member?.plan;
371
+ if (!plain(plan) || !Array.isArray(plan.tasks)) continue;
372
+ if (requestedPlanKey !== null && plan.plan_key !== requestedPlanKey) continue;
373
+ if (requestedPlanKey !== null) matchedPlan = true;
374
+ for (const task of plan.tasks) {
375
+ if (!plain(task) || task.compile_binding === null || task.compile_binding === undefined) continue;
376
+ bindings.push({
377
+ project_id: plan.project_id,
378
+ plan_key: plan.plan_key,
379
+ plan_version: plan.plan_version,
380
+ task_id: task.task_id,
381
+ compile_binding: task.compile_binding,
382
+ });
383
+ if (bindings.length > TODO_STATUS_LIST_LIMIT) {
384
+ fail('TODO_SCALE_EXCEEDED', 'todo_binding_projection_limit_exceeded', {
385
+ binding_limit: TODO_STATUS_LIST_LIMIT,
386
+ });
387
+ }
388
+ }
389
+ }
390
+ if (!matchedPlan) fail('TODO_STATUS_INVALID_INPUT', 'todo_binding_plan_not_found');
391
+ const result = {
392
+ schema: TODO_BINDING_PROJECTION_SCHEMA,
393
+ project_id: readModel.project_id,
394
+ plan_key: requestedPlanKey,
395
+ bindings,
396
+ result_digest: '',
397
+ };
398
+ result.result_digest = todoSelfDigest(result, 'result_digest');
399
+ return result;
400
+ }