@quolu/lattice 0.41.0 → 0.42.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/README.ja.md CHANGED
@@ -61,8 +61,9 @@ lattice todo note --plan <key> --task <id> --message "既存parserを使い、fa
61
61
  lattice todo show --plan <key> --task <id> --json
62
62
  ```
63
63
 
64
- ローカル/公開の動的工程表は、選択したToDoの右ペインへ初期設計メモを表示します。追記note本文は
65
- ローカルだけに表示し、公開dashboardには含めません。project別HTMLの生成や再生成は不要です。
64
+ 動的工程表は、選択したToDoの右ペインへ初期設計メモと追記noteの本文を表示します。あわせて前提工程・
65
+ 後続工程それぞれの状態(未着手/作業中/完了/ブロック中)と、その工程の並列可否を示します。
66
+ note本文を落とすのはrepo外へHTMLを出す公開配信面だけです。project別HTMLの生成や再生成は不要です。
66
67
 
67
68
  ### 変換の受入五条件
68
69
 
package/README.md CHANGED
@@ -74,9 +74,11 @@ lattice todo note --plan <key> --task <id> --message "Use the existing parser; d
74
74
  lattice todo show --plan <key> --task <id> --json
75
75
  ```
76
76
 
77
- The initial design memo appears in the selected ToDo's detail pane on both local and public dynamic
78
- dashboards. Public rendering excludes append-only note bodies by contract. Static per-project HTML is
79
- not generated; `lattice todo gantt serve` and the shared dashboard read the store dynamically.
77
+ The selected ToDo's detail pane shows the initial design memo and the append-only note bodies, plus the
78
+ status of each prerequisite and dependent ToDo (pending, in progress, done, blocked) and whether the ToDo
79
+ can run in parallel. Only a public distribution surface HTML served outside the repository — drops note
80
+ bodies. Static per-project HTML is not generated; `lattice todo gantt serve` and the shared dashboard
81
+ read the store dynamically.
80
82
 
81
83
  ### The five acceptance conditions
82
84
 
@@ -5,7 +5,7 @@ import path from 'node:path';
5
5
 
6
6
  import { readTodoStoreStable } from '../src/todo-store.mjs';
7
7
  import { projectTodoStatus } from '../src/todo-status.mjs';
8
- import { ganttLiveHeadDigest, renderPublicTodoGanttForProject } from '../src/todo-cli.mjs';
8
+ import { ganttLiveHeadDigest, renderTodoGanttForProject } from '../src/todo-cli.mjs';
9
9
  import {
10
10
  readVisibleTodoDashboardProjects,
11
11
  todoDashboardMemberNeedsVisibility,
@@ -90,7 +90,7 @@ async function synchronize() {
90
90
  displayName: entry.display_name,
91
91
  render: async ({ displayName }) => {
92
92
  const store = await readCachedStore(entry.repo_root);
93
- const result = await renderPublicTodoGanttForProject({ repoRoot: entry.repo_root,
93
+ const result = await renderTodoGanttForProject({ repoRoot: entry.repo_root,
94
94
  stable: true, displayName, readModel: store });
95
95
  return {
96
96
  html: result.rendered.html,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.41.0",
3
+ "version": "0.42.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,18 +30,14 @@ 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> [--todo-plan <key>]
33
+ compile --request <request.json>
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へ束縛して発行する
43
40
  start --request <request.json> --executor <adapter>
44
- # artifactを消費しない既存runtime互換入口
45
41
  start --schema --json # lattice.run_request.v1 の JSON Schema を出す
46
42
  adapter register --input <descriptor.json>
47
43
  adapter register --schema --json # 登録入力の JSON Schema を出す
@@ -81,8 +81,6 @@ 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';
86
84
  import {
87
85
  AdapterRegistryError,
88
86
  listRuntimeAdapters,
@@ -132,47 +130,6 @@ const CLI_ERROR_SCHEMA = 'lattice.cli_error.v2';
132
130
  const RUN_STORE_ROOT = ['.lattice', 'runs'];
133
131
  const RUN_REF = /^\.lattice\/runs\/([0-9A-Za-z](?:[0-9A-Za-z._-]{0,127}))$/u;
134
132
  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
- }
176
133
  /**
177
134
  * 自動escalationがlifecycle lockを待つ上限(ADR 0143)。
178
135
  *
@@ -439,27 +396,9 @@ async function compileFromRepo({ request, cwd, planRef, planEpoch, predecessorRe
439
396
  });
440
397
  }
441
398
 
442
- async function planCompile({ requestPath, todoPlanKey = null, cwd, stdout }) {
399
+ async function planCompile({ requestPath, cwd, stdout }) {
443
400
  const request = await loadRequest(requestPath);
444
401
  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
- }
463
402
  const result = await compileFromRepo({
464
403
  request,
465
404
  cwd,
@@ -477,7 +416,6 @@ async function planCompile({ requestPath, todoPlanKey = null, cwd, stdout }) {
477
416
  manifests: result.manifests,
478
417
  schedule: result.schedule,
479
418
  graph_digest: result.graph_digest,
480
- todo_plan_binding: todoPlanBinding,
481
419
  };
482
420
  artifact.result_digest = digestArtifact(artifact);
483
421
  stdout.write(`${JSON.stringify(artifact)}\n`);
@@ -588,8 +526,7 @@ async function readRunStore(runDir) {
588
526
  path.join(runDir, 'plan-compile-result.json'), 'plan compile result',
589
527
  );
590
528
  const request = await readBoundedJson(path.join(runDir, 'request.json'), 'run request');
591
- const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest',
592
- 'todo_plan_binding', 'result_digest'];
529
+ const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest', 'result_digest'];
593
530
  const metaKeys = ['schema', 'run_id', 'executor_adapter', 'plan_digest'];
594
531
  const { result_digest: claimedCompileDigest, ...compileBody } = compileArtifact ?? {};
595
532
  const legacyMetaValid = meta !== null && typeof meta === 'object' && !Array.isArray(meta)
@@ -1153,76 +1090,7 @@ function isDistributedScriptedControllerActivation(activation) {
1153
1090
  ));
1154
1091
  }
1155
1092
 
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 }) {
1093
+ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
1226
1094
  // --executor省略時の暗黙fallbackは持たない(Decision 8)。未知adapterはtyped reject。
1227
1095
  if (!KNOWN_ADAPTERS.includes(executorAdapter)) {
1228
1096
  throw new CliContractError('UNKNOWN_ADAPTER', `未知のexecutor adapter: ${executorAdapter}`);
@@ -1236,32 +1104,30 @@ async function runStart({ requestPath, planPath = null, executorAdapter, cwd, st
1236
1104
  await requireSafeRunAncestors(repoRoot);
1237
1105
  await requireIgnoredRunStore(repoRoot);
1238
1106
  await resolveRepoBinding(repoRoot, request);
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 });
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);
1259
1116
  }
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);
1260
1126
  const events = initializeRunEvents({
1261
1127
  runId: request.request_id,
1262
1128
  request,
1263
- plan: compileArtifact.plan,
1264
- manifests: compileArtifact.manifests,
1129
+ plan: result.plan,
1130
+ manifests: result.manifests,
1265
1131
  recordedAt: new Date().toISOString().replace(/\.\d+Z$/u, '.000Z'),
1266
1132
  });
1267
1133
  const runDir = runStorePath(repoRoot, request.request_id);
@@ -1279,7 +1145,7 @@ async function runStart({ requestPath, planPath = null, executorAdapter, cwd, st
1279
1145
  schema: 'lattice.run_meta.v1',
1280
1146
  run_id: request.request_id,
1281
1147
  executor_adapter: executorAdapter,
1282
- plan_digest: compileArtifact.plan.plan_digest,
1148
+ plan_digest: result.plan.plan_digest,
1283
1149
  };
1284
1150
  try {
1285
1151
  await writeJsonFile(path.join(temporaryDir, 'request.json'), request);
@@ -1299,7 +1165,7 @@ async function runStart({ requestPath, planPath = null, executorAdapter, cwd, st
1299
1165
  run_id: request.request_id,
1300
1166
  run_dir: path.relative(repoRoot, runDir),
1301
1167
  executor_adapter: executorAdapter,
1302
- plan_digest: compileArtifact.plan.plan_digest,
1168
+ plan_digest: result.plan.plan_digest,
1303
1169
  events_digest: digestArtifact(events.map(({ event_digest: digest }) => digest)),
1304
1170
  };
1305
1171
  output.result_digest = digestArtifact(output);
@@ -4035,13 +3901,6 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
4035
3901
  && argv[0] === 'plan' && argv[1] === 'compile' && argv[2] === '--request'
4036
3902
  && typeof argv[3] === 'string' && argv[3].length > 0) {
4037
3903
  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
- });
4045
3904
  } else if (argv.length === 6
4046
3905
  && argv[0] === 'plan' && argv[1] === 'verify'
4047
3906
  && argv[2] === '--request' && typeof argv[3] === 'string' && argv[3].length > 0
@@ -4062,15 +3921,6 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
4062
3921
  cwd,
4063
3922
  stdout,
4064
3923
  });
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
- });
4074
3924
  } else if (argv.length === 4
4075
3925
  && argv[0] === 'run' && argv[1] === 'activate'
4076
3926
  && argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0) {
@@ -215,8 +215,7 @@ 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',
219
- 'todo_plan_binding', 'result_digest'];
218
+ const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest', 'result_digest'];
220
219
  const metaKeys = ['schema', 'run_id', 'executor_adapter', 'plan_digest'];
221
220
  const compileBody = { ...compileArtifact };
222
221
  delete compileBody.result_digest;
package/src/todo-cli.mjs CHANGED
@@ -2292,7 +2292,12 @@ export async function renderTodoGanttForProject({
2292
2292
  return { store, metadata, memberBindings, rendered };
2293
2293
  }
2294
2294
 
2295
- /** 公開配信面はToDo本体の設計メモを表示し、append-only作業記録は除外する。 */
2295
+ /**
2296
+ * 公開配信面はToDo本体の設計メモを表示し、append-only作業記録は除外する。
2297
+ *
2298
+ * loopbackのdashboardと`gantt serve`は公開配信面ではない。作業者本人が読む面なので
2299
+ * 記録込みで描く。この入口はrepo外へHTMLを出す面が生えた時に使う。
2300
+ */
2296
2301
  export async function renderPublicTodoGanttForProject(options = {}) {
2297
2302
  return renderTodoGanttForProject({ ...options, includeNotes: false });
2298
2303
  }
@@ -2306,7 +2311,7 @@ async function serveGantt({ repoRoot, port, stdout, env, scope = DEFAULT_GANTT_S
2306
2311
  port,
2307
2312
  render: async () => {
2308
2313
  const store = await readTodoStoreStable({ repoRoot });
2309
- const { rendered } = await renderPublicTodoGanttForProject({
2314
+ const { rendered } = await renderTodoGanttForProject({
2310
2315
  repoRoot, stable: true, displayName: identity.displayName, scope, readModel: store,
2311
2316
  });
2312
2317
  return { html: rendered.html, head_digest: await ganttLiveHeadDigest({ repoRoot, store }) };
@@ -60,20 +60,59 @@ export function dispatchBasis(summary) {
60
60
  return `${parts.join('、')}。未検査は依存線が無くても並列可の根拠になりません。`;
61
61
  }
62
62
 
63
+ const COVERAGE_REASON = Object.freeze({
64
+ missing: 'このplanには独立性の記録がまだありません。',
65
+ stale: '記録はありますが、その後のdiffで失効しています。',
66
+ superseded: '記録は旧plan versionのもので、現在のtopologyには読めません。',
67
+ });
68
+
69
+ /**
70
+ * 未検査を黙って消さない。記録が無い状態こそ最も警告が要る(ADR 0127)。
71
+ *
72
+ * 枠ごと消すと「競合なし」と読まれる。判定していないことと、次の一手を必ず言う。
73
+ */
74
+ function unverifiedIndependence(planKey, coverage) {
75
+ const reason = COVERAGE_REASON[coverage] ?? COVERAGE_REASON.missing;
76
+ return `<p class="readiness-note"><strong>並列可否:</strong> 未検査です。競合が無いのではなく、まだ判定していません。${escapeHtmlText(reason)}宣言を書いて <code>lattice todo independence compile --plan ${escapeHtmlText(planKey)} --input &lt;ref&gt;</code> を通すと判定できます。</p>`;
77
+ }
78
+
79
+ function conflictItems(pairs) {
80
+ return pairs.map((pair) => `<li>${escapeHtmlText(pair.other)} — ${escapeHtmlText(SEVERABILITY_LABEL[pair.severability] ?? pair.severability)}(資源 ${escapeHtmlText(pair.detail)})</li>`).join('');
81
+ }
82
+
83
+ /**
84
+ * 作業中の工程は自分がready frontierに居ないため、conflictの相手側として現れる。
85
+ * 「今これを動かしている間、何が同時に始められないか」がその工程での並列可否である。
86
+ */
87
+ function activeIndependence(plan, taskId) {
88
+ const blocked = plan.conflicts_with_active
89
+ .filter((entry) => entry.active_task_id === taskId)
90
+ .map((entry) => ({
91
+ other: entry.ready_task_id, severability: entry.severability, detail: entry.detail,
92
+ }));
93
+ if (blocked.length > 0) {
94
+ return `<p class="readiness-note"><strong>並列可否:</strong> この工程が作業中のあいだ、次の着手候補は同時に始められません。</p><ul class="independence-conflicts">${conflictItems(blocked)}</ul>`;
95
+ }
96
+ if (plan.coverage !== 'verified') return unverifiedIndependence(plan.plan_key, plan.coverage);
97
+ return '<p class="readiness-note"><strong>並列可否:</strong> 着手済のため同時着手の判定対象ではありません。現在の着手候補に、この工程と競合するものはありません。</p>';
98
+ }
99
+
63
100
  /** 個別ToDoについて、競合相手と切断可能性を言葉で示す。 */
64
- export function renderIndependenceNote(ref, node, summary) {
65
- if (summary === null || node === undefined) return '';
66
- const plan = summary.byPlan.get(ref.plan_key);
67
- if (plan === undefined) return '';
101
+ export function renderIndependenceNote(ref, node, summary, status) {
102
+ // 完了した工程は同時着手の判断材料ではない。ここだけは黙ってよい。
103
+ if (status === 'done') return '';
104
+ const plan = summary === null ? undefined : summary.byPlan.get(ref.plan_key);
105
+ if (plan === undefined) return unverifiedIndependence(ref.plan_key, 'missing');
68
106
  const taskId = ref.task_id;
69
- const state = node.visibility.independence;
70
- if (state === null) return '';
107
+ const state = node?.visibility?.independence ?? null;
108
+ if (state === null) {
109
+ if (status === 'in-progress') return activeIndependence(plan, taskId);
110
+ return '<p class="readiness-note"><strong>並列可否:</strong> まだ着手候補ではないため判定していません。前提工程が片付いて着手候補に入った時点で判定します。</p>';
111
+ }
71
112
  if (state === 'verified') {
72
113
  return '<p class="readiness-note"><strong>並列可否:</strong> 独立検証済です。記録時点の宣言境界では他のready工程と干渉しません。</p>';
73
114
  }
74
- if (state === 'unknown') {
75
- return '<p class="readiness-note"><strong>並列可否:</strong> 未検査です。競合が無いのではなく、まだ判定していません。</p>';
76
- }
115
+ if (state === 'unknown') return unverifiedIndependence(plan.plan_key, plan.coverage);
77
116
  const pairs = [
78
117
  ...plan.serialize_pairs
79
118
  .filter((pair) => pair.task_ids.includes(taskId))
@@ -90,8 +129,7 @@ export function renderIndependenceNote(ref, node, summary) {
90
129
  detail: entry.detail,
91
130
  })),
92
131
  ];
93
- const items = pairs.map((pair) => `<li>${escapeHtmlText(pair.other)} — ${escapeHtmlText(SEVERABILITY_LABEL[pair.severability] ?? pair.severability)}(資源 ${escapeHtmlText(pair.detail)})</li>`).join('');
94
- return `<p class="readiness-note"><strong>並列可否:</strong> 要直列です。</p><ul class="independence-conflicts">${items}</ul>`;
132
+ return `<p class="readiness-note"><strong>並列可否:</strong> 要直列です。</p><ul class="independence-conflicts">${conflictItems(pairs)}</ul>`;
95
133
  }
96
134
 
97
135
  export function renderRightPane(
@@ -157,7 +195,9 @@ export function renderRightPane(
157
195
  const readiness = node?.visibility.next_ready
158
196
  ? `<p class="readiness-note">ready frontierの一員です。${ready.length > 1 ? '他のready工程と同時着手できるかは下の並列可否で判断してください。' : '現在の唯一の着手候補です。'}</p>`
159
197
  : incoming.get(key).length === 0 ? '<p class="readiness-note">登録済みの前提工程はありません。図だけではdispatch可否を判定しません。</p>' : '';
160
- const independenceNote = renderIndependenceNote(section.ref, node, independenceSummary);
198
+ const independenceNote = renderIndependenceNote(
199
+ section.ref, node, independenceSummary, section.state.status,
200
+ );
161
201
  // Say it plainly when the reader will not find this ToDo on the diagram.
162
202
  const foldedNote = !folds.has(key) ? ''
163
203
  : expandable
@@ -105,7 +105,10 @@ export function renderRelationList(relations, sectionByKey, lookup, emptyText, f
105
105
  // Say which ones the diagram does not draw, so the reader stops looking.
106
106
  const reference = folds.has(targetKey)
107
107
  ? `${taskReference(target, lookup)}(図では非表示)` : taskReference(target, lookup);
108
- return `<li><button type="button" data-select-node-key="${escapeHtmlAttribute(targetKey)}"><strong>${escapeHtmlText(reference)}</strong><span>${escapeHtmlText(target.task.title)}</span></button>${join}</li>`;
108
+ // 前提が終わっているか、後続が動き出せるかは、相手の状態を見ないと判断できない。
109
+ const status = DOCUMENT_STATUS[target.state.status] ?? { mark: '?', label: '状態不明' };
110
+ const badge = `<span class="relation-status status-${escapeHtmlAttribute(target.state.status)}">${escapeHtmlText(`${status.mark} ${status.label}`)}</span>`;
111
+ return `<li><button type="button" data-select-node-key="${escapeHtmlAttribute(targetKey)}"><span class="relation-head">${badge}<strong>${escapeHtmlText(reference)}</strong></span><span>${escapeHtmlText(target.task.title)}</span></button>${join}</li>`;
109
112
  }).join('')}</ul>`;
110
113
  }
111
114
 
@@ -63,6 +63,7 @@ body{display:grid;grid-template-rows:minmax(0,1fr);height:100vh;margin:0;backgro
63
63
  .work-log-origin,.work-log-head,.note-warning{color:var(--text-secondary);font-size:12px;overflow-wrap:anywhere}.note-warning{color:var(--critical)}
64
64
  .gantt-warning{margin:16px 0;padding:12px;border:1px solid var(--critical);background:var(--surface-2)}.gantt-warning h2{margin:0 0 8px}.gantt-warning ul{margin:0;padding-left:20px}
65
65
  .relation-list button{display:flex;width:100%;flex-direction:column}.relation-list button span{font-weight:400}.relation-kind{display:block;margin-top:4px;color:var(--text-secondary);font-size:12px}
66
+ .relation-head{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap}.relation-status{color:var(--text-secondary);font-size:12px;font-weight:600;white-space:nowrap}.relation-status.status-in-progress{color:var(--accent)}.relation-status.status-done{color:var(--good)}.relation-status.status-blocked{color:var(--critical)}
66
67
  .task-diagnostics{margin:16px 0;color:var(--text-secondary);font-size:12px}.task-diagnostics summary{cursor:pointer;color:var(--text-primary);font-weight:600}.task-diagnostics dl{display:grid;grid-template-columns:auto 1fr;gap:4px 12px}.task-diagnostics dd{margin:0;overflow-wrap:anywhere}
67
68
  .task-index>h1{margin:0 0 8px;font-size:19px;font-weight:650;line-height:1.45}.task-index>p{margin:0 0 24px;color:var(--text-secondary)}
68
69
  .task-index-plan+.task-index-plan{margin-top:32px}.task-index-plan h2{margin:0 0 12px;font-size:16px;font-weight:600}.task-index-plan h2 code{font-size:inherit}