@quolu/lattice 0.13.0 → 0.15.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 CHANGED
@@ -14,6 +14,11 @@ if (help !== null) {
14
14
  process.stdout.write(help);
15
15
  } else if (args.length === 1 && args[0] === '--version') {
16
16
  process.stdout.write(`${packageJson.version}\n`);
17
+ } else if (args.length === 2 && args[0] === 'session-context' && args[1] === '--json') {
18
+ const { runSessionContext } = await import('../src/project-cli.mjs');
19
+ process.exitCode = await runSessionContext({
20
+ cwd: process.cwd(), stdout: process.stdout, cliVersion: packageJson.version,
21
+ });
17
22
  } else if (args.length === 2 && args[0] === 'status' && args[1] === '--json') {
18
23
  const { projectStatusFailure, runProjectStatus } = await import('../src/project-cli.mjs');
19
24
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -60,7 +60,7 @@
60
60
  "test": "node scripts/run-product-tests.mjs",
61
61
  "test:sensor": "npm --prefix sensor test",
62
62
  "precheck": "node --check src/bridge-launch-agent.mjs",
63
- "check": "node --check bin/lattice.mjs && node --check bin/lattice-mcp.mjs && node --check bin/lattice-dashboard.mjs && node --check bin/lattice-bridge.mjs && node --check bin/lattice-scripted-adapter.mjs && node --check src/cli-stdio.mjs && node --check src/bridge-address.mjs && node --check src/bridge-registrar.mjs && node --check src/todo-independence-guidance.mjs && node --check src/todo-independence.mjs && node --check src/todo-independence-contracts.mjs && node --check src/todo-gantt-layout.mjs && node --check src/todo-gantt-scope.mjs && node --check src/bridge-config.mjs && node --check src/bridge-server.mjs && node --check src/bridge-daemon.mjs && node --check src/bridge-cli.mjs && node --check src/project-cli.mjs && node --check src/sensor-cli.mjs && node --check src/sensor-runtime.mjs && node --check src/sensor-adapter.mjs && node --check src/factory-diagnostics.mjs && node --check src/runtime-errors.mjs && node --check src/runtime-contracts.mjs && node --check src/runtime-event-store.mjs && node --check src/runtime-adapter-registry.mjs && node --check src/runtime-scripted-adapter-controller.mjs && node --check src/hash-chain.mjs && node --check src/dag-chain.mjs && node --check src/todo-contracts.mjs && node --check src/todo-chain.mjs && node --check src/todo-store.mjs && node --check src/todo-migration.mjs && node --check src/todo-revision.mjs && node --check src/todo-status.mjs && node --check src/todo-cli.mjs && node --check src/todo-dashboard-registry.mjs && node --check src/todo-gantt-presentation.mjs && node --check src/todo-gantt-live.mjs && node --check src/bounded-seam.mjs && node --check src/todo-narrative-anchor.mjs && node --check src/todo-markdown-renderer.mjs && node --check src/todo-gantt-svg.mjs && node --check src/todo-gantt-html.mjs && node --check src/runtime-projection.mjs && node --check src/runtime-decision-verifier.mjs && node --check src/runtime-front-end.mjs && node --check src/runtime-cli.mjs && node --check src/rc3-dogfood-scaffold.mjs && node --check src/runtime-engine.mjs && node --check src/runtime-scripted-executor.mjs && node --check src/runtime-diff-observer.mjs && node --check src/runtime-worktree-executor.mjs && node --check src/runtime-hold-recompile.mjs && node --check src/rc3-scripted-campaign.mjs && node --check src/rc3-actual-dogfood.mjs && node --check src/rc4-stage1-dogfood.mjs && node --check research/fixtures/dispatch-record/src/dispatch-record.mjs && node --check test/research-dispatch-record.test.mjs",
63
+ "check": "node --check bin/lattice.mjs && node --check bin/lattice-mcp.mjs && node --check bin/lattice-dashboard.mjs && node --check bin/lattice-bridge.mjs && node --check bin/lattice-scripted-adapter.mjs && node --check src/cli-stdio.mjs && node --check src/bridge-address.mjs && node --check src/bridge-registrar.mjs && node --check src/todo-independence-guidance.mjs && node --check src/todo-independence.mjs && node --check src/todo-independence-contracts.mjs && node --check src/seam-proposal-contracts.mjs && node --check src/seam-proposal-queries.mjs && node --check src/seam-proposal.mjs && node --check src/todo-gantt-layout.mjs && node --check src/todo-gantt-scope.mjs && node --check src/bridge-config.mjs && node --check src/bridge-server.mjs && node --check src/bridge-daemon.mjs && node --check src/bridge-cli.mjs && node --check src/project-cli.mjs && node --check src/sensor-cli.mjs && node --check src/sensor-runtime.mjs && node --check src/sensor-adapter.mjs && node --check src/factory-diagnostics.mjs && node --check src/runtime-errors.mjs && node --check src/runtime-contracts.mjs && node --check src/runtime-event-store.mjs && node --check src/runtime-adapter-registry.mjs && node --check src/runtime-scripted-adapter-controller.mjs && node --check src/hash-chain.mjs && node --check src/dag-chain.mjs && node --check src/todo-contracts.mjs && node --check src/todo-chain.mjs && node --check src/todo-store.mjs && node --check src/todo-migration.mjs && node --check src/todo-revision.mjs && node --check src/todo-status.mjs && node --check src/todo-cli.mjs && node --check src/todo-dashboard-registry.mjs && node --check src/todo-gantt-presentation.mjs && node --check src/todo-gantt-live.mjs && node --check src/bounded-seam.mjs && node --check src/todo-narrative-anchor.mjs && node --check src/todo-markdown-renderer.mjs && node --check src/todo-gantt-svg.mjs && node --check src/todo-gantt-html.mjs && node --check src/runtime-projection.mjs && node --check src/runtime-decision-verifier.mjs && node --check src/runtime-front-end.mjs && node --check src/runtime-cli.mjs && node --check src/rc3-dogfood-scaffold.mjs && node --check src/runtime-engine.mjs && node --check src/runtime-scripted-executor.mjs && node --check src/runtime-diff-observer.mjs && node --check src/runtime-worktree-executor.mjs && node --check src/runtime-hold-recompile.mjs && node --check src/rc3-scripted-campaign.mjs && node --check src/rc3-actual-dogfood.mjs && node --check src/rc4-stage1-dogfood.mjs && node --check research/fixtures/dispatch-record/src/dispatch-record.mjs && node --check test/research-dispatch-record.test.mjs",
64
64
  "check:project-identity": "node --check src/project-identity.mjs",
65
65
  "ci": "npm run test && npm run test:sensor && npm run check && npm run check:project-identity"
66
66
  }
package/src/cli-help.mjs CHANGED
@@ -4,6 +4,7 @@ const ROOT_HELP = `Usage: lattice <command> [options]
4
4
 
5
5
  Commands:
6
6
  status --json Discover the current project and next action
7
+ session-context --json Session開始時の現在地を1プロセスで返す(工程状態+並列可否)
7
8
  plan <command> Create, compile, or verify plans
8
9
  run <command> Start or observe compiled runs
9
10
  event verify Verify a runtime event log
@@ -52,6 +53,7 @@ Read commands:
52
53
  status [--json]
53
54
  bindings [--plan <key>] [--json] # compile_binding付きTaskをTODO identityつきで投影する
54
55
  independence [--plan <key>] [--json] # readyを検証済み並列・要直列・未検査へ分けて投影する
56
+ seam-proposal [--plan <key>] [--json] # 記録済みseam提案をsensor無しで投影する
55
57
  verify [--plan <key>] [--json]
56
58
  snapshot --rebuild --plan <key>
57
59
  gantt [--out <file>] [--scope live|all] # 既定live: 完走した工程を図から除く(一覧には残る)
@@ -68,6 +70,7 @@ Write commands:
68
70
  evidence promote --plan <key> --task <id> --evidence <file>
69
71
  independence compile --plan <key> --input <file> # witness setとsensorから並列可否を記録する
70
72
  independence witness migrate --plan <key> # revision後の宣言をtask migrationで写す
73
+ seam-proposal compile --plan <key> # 並列可否記録と実sensorからseam提案を記録する
71
74
  revise --plan <key> --input <file>
72
75
  revise-phase --plan <key> --input <file>
73
76
  revise-set --input <file>
@@ -129,6 +132,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
129
132
  'todo status': 'todo status [--json]',
130
133
  'todo bindings': 'todo bindings [--plan <key>] [--json]',
131
134
  'todo independence': 'todo independence [--plan <key>] [--json] | compile --plan <key> --input <file> | witness migrate --plan <key>',
135
+ 'todo seam-proposal': 'todo seam-proposal [--plan <key>] [--json] | compile --plan <key>',
132
136
  'todo verify': 'todo verify [--plan <key>] [--json]',
133
137
  'todo snapshot': 'todo snapshot --rebuild --plan <key>',
134
138
  'todo gantt': 'todo gantt [--out <file>] [--scope live|all] | status [--out <file>] | serve --port <port> [--scope live|all]',
@@ -13,12 +13,16 @@ import {
13
13
  todoSelfDigest,
14
14
  } from './todo-contracts.mjs';
15
15
  import { projectTodoStatus } from './todo-status.mjs';
16
+ import { projectIndependenceFrontier } from './todo-independence.mjs';
17
+ import { isTodoIndependenceLegacyMarker } from './todo-independence-contracts.mjs';
18
+ import { selectIndependenceGuidance } from './todo-independence-guidance.mjs';
16
19
  import { ensureTodoDashboardActivity } from './todo-dashboard-registry.mjs';
17
20
  import { resolveProjectIdentity } from './project-identity.mjs';
18
21
  import {
19
22
  buildTodoPlan,
20
23
  createTodoStoreWriter,
21
24
  initializeAuthoredTodoStore,
25
+ readTodoIndependenceArtifact,
22
26
  readTodoStore,
23
27
  TodoStoreError,
24
28
  } from './todo-store.mjs';
@@ -27,6 +31,9 @@ const STORE_REF = '.lattice/todo';
27
31
  const MANIFEST_REF = `${STORE_REF}/manifest.json`;
28
32
  const MAX_INPUT_BYTES = 8_388_608;
29
33
  const STATUS_SCHEMA = 'lattice.project_status.v1';
34
+ const SESSION_CONTEXT_SCHEMA = 'lattice.session_context.v1';
35
+ /** HEADが読めない環境でも投影を組めるようにする。記録があるときは実HEADで置き換わる。 */
36
+ const PLACEHOLDER_SHA = '0'.repeat(40);
30
37
  const CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v1';
31
38
  const PHASE_CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v2';
32
39
  const DECOUPLED_PHASE_CREATE_INPUT_SCHEMA = 'lattice.plan_create_input.v3';
@@ -106,6 +113,74 @@ export function validateProjectStatus(value) {
106
113
  } catch { return false; }
107
114
  }
108
115
 
116
+ /**
117
+ * readyのあるplanについてだけ並列可否を要約する(ADR 0131 Decision 5)。
118
+ *
119
+ * store読みは呼び出し側が済ませている。ここが払うのはplanごとの小さな記録ファイルと、
120
+ * 記録があるときだけのHEAD照合である。readyが無いplanは述べる対象が無いので載せない。
121
+ */
122
+ async function summarizeIndependence({ repoRoot, store, todo }) {
123
+ const readyPlanKeys = [...new Set(todo.next_ready.map(({ plan_key: key }) => key))].sort();
124
+ if (readyPlanKeys.length === 0) return [];
125
+ const activeByPlan = new Map();
126
+ for (const task of todo.active_set) {
127
+ if (!activeByPlan.has(task.plan_key)) activeByPlan.set(task.plan_key, []);
128
+ activeByPlan.get(task.plan_key).push(task.task_id);
129
+ }
130
+ let currentBaseSha = null;
131
+ const summaries = [];
132
+ for (const planKey of readyPlanKeys) {
133
+ const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
134
+ if (member === undefined) continue;
135
+ let artifact = null;
136
+ try {
137
+ artifact = await readTodoIndependenceArtifact({ repoRoot, store, planKey });
138
+ } catch (error) {
139
+ // 読めない記録を「記録なし」へ丸めない。理由を載せて先へ進む。
140
+ summaries.push({
141
+ plan_key: planKey, coverage: null,
142
+ guidance: { code: 'independence_unrecorded', message: null, next_action: 'none' },
143
+ unreadable_reason: error instanceof TodoStoreError
144
+ ? `${error.code}:${error.detail?.reason ?? error.message}` : 'independence_unreadable',
145
+ parallel_groups: [], serialize_pair_count: 0,
146
+ conflict_with_active_count: 0, unknown_task_ids: [],
147
+ });
148
+ continue;
149
+ }
150
+ if (currentBaseSha === null && artifact !== null) currentBaseSha = gitHead(repoRoot);
151
+ const projected = projectIndependenceFrontier({
152
+ artifact,
153
+ readyTaskIds: todo.next_ready.filter((task) => task.plan_key === planKey)
154
+ .map(({ task_id: taskId }) => taskId),
155
+ activeTaskIds: activeByPlan.get(planKey) ?? [],
156
+ plan: member.plan,
157
+ currentBaseSha: currentBaseSha ?? PLACEHOLDER_SHA,
158
+ changedPaths: null,
159
+ });
160
+ summaries.push({
161
+ plan_key: planKey,
162
+ coverage: projected.coverage,
163
+ guidance: selectIndependenceGuidance({
164
+ coverage: projected.coverage,
165
+ contractSuperseded: isTodoIndependenceLegacyMarker(artifact),
166
+ readyCount: todo.next_ready.filter((task) => task.plan_key === planKey).length,
167
+ taskDeclared: projected.frontier.unknown
168
+ .every(({ unknowns }) => !unknowns.some(({ kind }) => kind === 'witness_missing')),
169
+ taskStale: projected.frontier.unknown
170
+ .some(({ unknowns }) => unknowns.some(({ kind }) => kind === 'record_stale')),
171
+ conflictWithActive: projected.frontier.conflicts_with_active[0]?.severability ?? null,
172
+ conflictBetweenReady: projected.frontier.serialize_pairs[0]?.severability ?? null,
173
+ }),
174
+ unreadable_reason: null,
175
+ parallel_groups: projected.frontier.parallel_groups.map(({ task_ids: ids }) => [...ids]),
176
+ serialize_pair_count: projected.frontier.serialize_pairs.length,
177
+ conflict_with_active_count: projected.frontier.conflicts_with_active.length,
178
+ unknown_task_ids: projected.frontier.unknown.map(({ task_id: taskId }) => taskId),
179
+ });
180
+ }
181
+ return summaries;
182
+ }
183
+
109
184
  function statusResult(fields) {
110
185
  const result = { schema: STATUS_SCHEMA, ...fields, result_digest: '' };
111
186
  result.result_digest = resultDigest(result);
@@ -124,12 +199,18 @@ function invalidStatus({ cliVersion, repoRoot, reason }) {
124
199
  });
125
200
  }
126
201
 
127
- export async function runProjectStatus({ cwd, stdout, cliVersion, env = process.env,
128
- ensureDashboardActivity = ensureTodoDashboardActivity }) {
202
+ /**
203
+ * discoveryとstore読みを1回で済ませ、status結果と(読めたなら)storeを返す。
204
+ *
205
+ * `runProjectStatus`と`runSessionContext`が同じ判定を二度書かないための共有点。
206
+ * store読みはここでしか行わない——session開始経路が同じstoreを二度払っていたのが
207
+ * ADR 0131で直した欠陥である。
208
+ */
209
+ async function resolveProjectState({ cwd, cliVersion }) {
129
210
  const repoRoot = resolveRepoRoot(cwd);
130
211
  if (repoRoot === null) {
131
- stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason: 'git_repository_unresolved' }))}\n`);
132
- return 1;
212
+ return { exitCode: 1, repoRoot: null, store: null, todo: null,
213
+ result: invalidStatus({ cliVersion, repoRoot: null, reason: 'git_repository_unresolved' }) };
133
214
  }
134
215
  const storeAbsolute = path.join(repoRoot, STORE_REF);
135
216
  const manifestAbsolute = path.join(repoRoot, MANIFEST_REF);
@@ -137,30 +218,24 @@ export async function runProjectStatus({ cwd, stdout, cliVersion, env = process.
137
218
  let latticeState;
138
219
  let storeState;
139
220
  let manifestState;
221
+ const invalid = (reason) => ({
222
+ exitCode: 1, repoRoot, store: null, todo: null,
223
+ result: invalidStatus({ cliVersion, repoRoot, reason }),
224
+ });
140
225
  try { latticeState = await lstat(latticeAbsolute); } catch (error) {
141
- if (error?.code !== 'ENOENT') {
142
- stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason: 'lattice_root_unreadable' }))}\n`);
143
- return 1;
144
- }
226
+ if (error?.code !== 'ENOENT') return invalid('lattice_root_unreadable');
145
227
  }
146
228
  if (latticeState !== undefined && (latticeState.isSymbolicLink() || !latticeState.isDirectory())) {
147
- stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason: 'lattice_root_invalid' }))}\n`);
148
- return 1;
229
+ return invalid('lattice_root_invalid');
149
230
  }
150
231
  try { storeState = await lstat(storeAbsolute); } catch (error) {
151
- if (error?.code !== 'ENOENT') {
152
- stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason: 'store_unreadable' }))}\n`);
153
- return 1;
154
- }
232
+ if (error?.code !== 'ENOENT') return invalid('store_unreadable');
155
233
  }
156
234
  try { manifestState = await lstat(manifestAbsolute); } catch (error) {
157
- if (error?.code !== 'ENOENT') {
158
- stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason: 'manifest_unreadable' }))}\n`);
159
- return 1;
160
- }
235
+ if (error?.code !== 'ENOENT') return invalid('manifest_unreadable');
161
236
  }
162
237
  if (storeState === undefined && manifestState === undefined) {
163
- const result = statusResult({
238
+ return { exitCode: 0, repoRoot, store: null, todo: null, result: statusResult({
164
239
  cli: { available: true, version: cliVersion },
165
240
  project: { root: repoRoot, git_head: gitHead(repoRoot), project_id: null },
166
241
  state: 'uninitialized',
@@ -171,14 +246,11 @@ export async function runProjectStatus({ cwd, stdout, cliVersion, env = process.
171
246
  input_schema: CURRENT_CREATE_INPUT_SCHEMA,
172
247
  schema_command: CURRENT_CREATE_SCHEMA_COMMAND,
173
248
  },
174
- });
175
- stdout.write(`${JSON.stringify(result)}\n`);
176
- return 0;
249
+ }) };
177
250
  }
178
251
  if (storeState?.isSymbolicLink() || !storeState?.isDirectory()
179
252
  || manifestState?.isSymbolicLink() || !manifestState?.isFile()) {
180
- stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason: 'store_layout_invalid' }))}\n`);
181
- return 1;
253
+ return invalid('store_layout_invalid');
182
254
  }
183
255
  try {
184
256
  const store = await readTodoStore({ repoRoot });
@@ -205,21 +277,60 @@ export async function runProjectStatus({ cwd, stdout, cliVersion, env = process.
205
277
  can_create_plan: false,
206
278
  next_action: next,
207
279
  });
208
- if (env.LATTICE_DASHBOARD_AUTOSTART !== '0') {
209
- const identity = await resolveProjectIdentity({ repoRoot, projectId: store.project_id, env });
210
- const actorSession = env.LATTICE_TODO_ACTOR_SESSION;
211
- await ensureDashboardActivity({
212
- repoRoot, projectId: store.project_id, displayName: identity.displayName,
213
- sessionId: isTodoIdentifier(actorSession) ? actorSession : `status-${process.pid}`, env,
214
- });
215
- }
216
- stdout.write(`${JSON.stringify(result)}\n`);
217
- return 0;
280
+ return { exitCode: 0, repoRoot, store, todo, result };
218
281
  } catch (error) {
219
- const reason = error instanceof TodoStoreError ? `${error.code}:${error.detail?.reason ?? error.message}` : 'store_validation_failed';
220
- stdout.write(`${JSON.stringify(invalidStatus({ cliVersion, repoRoot, reason }))}\n`);
221
- return 1;
282
+ const reason = error instanceof TodoStoreError
283
+ ? `${error.code}:${error.detail?.reason ?? error.message}` : 'store_validation_failed';
284
+ return invalid(reason);
285
+ }
286
+ }
287
+
288
+ export async function runProjectStatus({ cwd, stdout, cliVersion, env = process.env,
289
+ ensureDashboardActivity = ensureTodoDashboardActivity }) {
290
+ const state = await resolveProjectState({ cwd, cliVersion });
291
+ // dashboard活動の登録はdiscovery面の副作用として維持する(ADR 0131 Decision 4で
292
+ // session-context側だけが持たない、と決めた面である)。
293
+ if (state.store !== null && env.LATTICE_DASHBOARD_AUTOSTART !== '0') {
294
+ const identity = await resolveProjectIdentity({
295
+ repoRoot: state.repoRoot, projectId: state.store.project_id, env,
296
+ });
297
+ const actorSession = env.LATTICE_TODO_ACTOR_SESSION;
298
+ await ensureDashboardActivity({
299
+ repoRoot: state.repoRoot, projectId: state.store.project_id,
300
+ displayName: identity.displayName,
301
+ sessionId: isTodoIdentifier(actorSession) ? actorSession : `status-${process.pid}`, env,
302
+ });
222
303
  }
304
+ stdout.write(`${JSON.stringify(state.result)}\n`);
305
+ return state.exitCode;
306
+ }
307
+
308
+ /**
309
+ * session開始時の現在地を1プロセス・1 store読みで返す(ADR 0131)。
310
+ *
311
+ * `lattice status`と`lattice todo status`は同じ`readTodoStore`を別プロセスで二重に払う。
312
+ * hostのSessionStartはその両方を呼ぶため、storeが育ったprojectでは実行枠を超えて
313
+ * 案内ごと捨てられていた。ここは合成であって置き換えではない——既存2面は不変で、
314
+ * それぞれの消費者を持ち続ける。
315
+ *
316
+ * dashboard活動の登録は行わない。現在地を知るために呼ぶ面であり、常駐面を起こす面ではない。
317
+ */
318
+ export async function runSessionContext({ cwd, stdout, cliVersion }) {
319
+ const state = await resolveProjectState({ cwd, cliVersion });
320
+ const independence = state.store === null || state.todo === null
321
+ ? [] : await summarizeIndependence({ repoRoot: state.repoRoot, store: state.store, todo: state.todo });
322
+ const result = {
323
+ schema: SESSION_CONTEXT_SCHEMA,
324
+ // project discoveryの答えをそのまま埋める。hostは既存の検証器を再利用できる。
325
+ status: state.result,
326
+ // todoは`todo_status_result.v4`そのもの。新しい意味論を発明しない。
327
+ todo: state.todo,
328
+ independence,
329
+ result_digest: '',
330
+ };
331
+ result.result_digest = todoSelfDigest(result, 'result_digest');
332
+ stdout.write(`${JSON.stringify(result)}\n`);
333
+ return state.exitCode;
223
334
  }
224
335
 
225
336
  async function readCanonicalInput(repoRoot, inputRef) {