@quolu/lattice 0.57.1 → 0.57.3

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.
@@ -140,12 +140,15 @@ timer = setInterval(async () => {
140
140
  // the terminal identity file) reaches this catch.
141
141
  try {
142
142
  const heartbeat = await hubHeartbeat.tick({ config });
143
- hubHeartbeatError = null;
143
+ // 直前に書いた指紋は、健全な状態へ戻った時にだけ捨てる。判定より前に捨てると
144
+ // 比較相手が毎周期nullになり、同じ結果をpoll周期ごとに書き続ける。stderrの
145
+ // 消費者が居ない常駐(WindowsのStartup launcher、LaunchAgent)では、この
146
+ // 書込みが積み上がること自体が常駐を不安定にする。
144
147
  if (['unreachable', 'rejected', 'partial'].includes(heartbeat?.state)) {
145
148
  const fingerprint = JSON.stringify(heartbeat);
146
149
  if (fingerprint !== hubHeartbeatError) process.stderr.write(`${fingerprint}\n`);
147
150
  hubHeartbeatError = fingerprint;
148
- }
151
+ } else hubHeartbeatError = null;
149
152
  } catch (error) {
150
153
  const failure = { schema: 'lattice.bridge_daemon_error.v1',
151
154
  code: error?.code ?? 'BRIDGE_HUB_HEARTBEAT_FAILED',
@@ -54,6 +54,11 @@ bridgeが無効な間は以下すべてnullで、bridgeを有効にしている
54
54
  | `node_path`・`node_exists` | 起動するNode実行体と、それが今も存在するか |
55
55
  | `bridge_path`・`bridge_exists` | 起動するbridge scriptと、それが今も存在するか |
56
56
  | `error` | `unreadable`のときだけtyped code(例`BRIDGE_LAUNCH_AGENT_PLIST_UNSAFE`) |
57
+ | `error=BRIDGE_PERSISTENCE_STATE_SPLIT` | launcherまたはplistの片割れだけが残った状態。手でfileを消さず、`lattice bridge reconfigure --json`で常駐設定を揃える(0.57.2以降) |
58
+
59
+ `BRIDGE_PERSISTENCE_STATE_SPLIT`は、常駐設定の一方(macOSのplist、またはWindowsのlauncher/descriptor)だけが
60
+ 存在することを示す。これは復旧対象そのものが壊れている状態なので、手作業でfileを削除せず、
61
+ `lattice bridge reconfigure --json`を実行して正規のinstall経路で両方を再生成する。
57
62
 
58
63
  **`runtime`** — いま応答しているprocess自身の申告。`state`は`running`/`not_running`/`unattested`/
59
64
  `descriptor_invalid`で、`running`以外では各値がnullになる。`running`でも、identityを返さない
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.57.1",
3
+ "version": "0.57.3",
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",
@@ -112,6 +112,10 @@ async function bridgePersistence({ launchAgent, env }) {
112
112
  // `loaded` is launchd-specific; the Windows Startup folder has no such
113
113
  // concept and reports null rather than pretending to know.
114
114
  const loaded = typeof snapshot?.loaded === 'boolean' ? snapshot.loaded : null;
115
+ if (snapshot?.split === true) {
116
+ return { state: 'unreadable', ...UNREADABLE_PERSISTENCE, loaded,
117
+ error: 'BRIDGE_PERSISTENCE_STATE_SPLIT' };
118
+ }
115
119
  if (described === null) return { state: 'not_installed', ...UNREADABLE_PERSISTENCE, loaded, error: null };
116
120
  return { state: 'installed', loaded, ...described, error: null };
117
121
  } catch (error) {
@@ -247,10 +247,8 @@ export async function snapshotBridgeLaunchAgent({ env = process.env,
247
247
  await prepareDirectory(refs.directory, uid);
248
248
  const content = await strictPlist(refs.plist, uid);
249
249
  const loaded = await loadedState(runner, uid);
250
- if (loaded && content === null) {
251
- throw fail('BRIDGE_LAUNCH_AGENT_STATE_INVALID', 'loaded bridge LaunchAgent has no owned plist');
252
- }
253
- return Object.freeze({ installed: content !== null, loaded, content });
250
+ const split = loaded && content === null;
251
+ return Object.freeze({ installed: content !== null, loaded, split, content });
254
252
  }
255
253
 
256
254
  const PROGRAM_ARGUMENTS_PATTERN =
@@ -346,6 +344,10 @@ export async function restoreBridgeLaunchAgent({ snapshot, listen = null, env =
346
344
  await prepareDirectory(refs.directory, uid);
347
345
  const stopped = await bootoutIfLoaded({ runner, uid });
348
346
  if (stopped) await waitStopped({ listen, env });
347
+ if (snapshot.split === true) {
348
+ await rm(refs.plist, { force: true });
349
+ return snapshot;
350
+ }
349
351
  if (snapshot.installed) await atomicPlist(refs.plist, snapshot.content);
350
352
  else await rm(refs.plist, { force: true });
351
353
  if (snapshot.loaded) {
@@ -239,10 +239,9 @@ export async function snapshotBridgeStartupFolder({ env = process.env } = {}) {
239
239
  await prepareDirectory(refs.runtimeDirectory);
240
240
  const launcherContent = await strictFile(refs.launcher);
241
241
  const descriptorContent = await strictFile(refs.descriptor);
242
- if ((launcherContent === null) !== (descriptorContent === null)) {
243
- throw fail('BRIDGE_STARTUP_FOLDER_STATE_INVALID', 'startup launcher and descriptor disagree on installed state');
244
- }
245
- return Object.freeze({ installed: launcherContent !== null, launcherContent, descriptorContent });
242
+ const split = (launcherContent === null) !== (descriptorContent === null);
243
+ return Object.freeze({ installed: launcherContent !== null && descriptorContent !== null,
244
+ split, launcherContent, descriptorContent });
246
245
  }
247
246
 
248
247
  async function pathPresent(ref) {
@@ -681,8 +681,10 @@ export async function intakePullTask({ repoRoot, runDir, taskId, environment = p
681
681
  affected_tests: [], graph_evidence: [], witness_provenance: {}, manifest_digest: '',
682
682
  };
683
683
  manifest.manifest_digest = selfDigest(manifest, 'manifest_digest');
684
- verdict.state = 'hold'; verdict.reason = 'boundary_unverified';
685
- verdict.next_action = 'declare_and_recompile'; verdict.detail = { cause: error.code };
684
+ if (verdict.state !== 'hold') {
685
+ verdict.state = 'hold'; verdict.reason = 'boundary_unverified';
686
+ verdict.next_action = 'declare_and_recompile'; verdict.detail = { cause: error.code };
687
+ }
686
688
  }
687
689
  const constraint = verdict.state === 'none'
688
690
  ? planningConstraint(verdict.artifact, taskId, state, member) : null;
@@ -760,8 +762,10 @@ export async function intakePullTask({ repoRoot, runDir, taskId, environment = p
760
762
  affected_tests: [], graph_evidence: [], witness_provenance: {}, manifest_digest: '',
761
763
  };
762
764
  manifest.manifest_digest = selfDigest(manifest, 'manifest_digest');
763
- verdict.state = 'hold'; verdict.reason = 'boundary_unverified';
764
- verdict.next_action = 'declare_and_recompile'; verdict.detail = { cause: error.code };
765
+ if (verdict.state !== 'hold') {
766
+ verdict.state = 'hold'; verdict.reason = 'boundary_unverified';
767
+ verdict.next_action = 'declare_and_recompile'; verdict.detail = { cause: error.code };
768
+ }
765
769
  }
766
770
  const packet = packetFor({ meta: current.meta, taskId, baseSha, manifest });
767
771
  const worktreePath = await ensureScriptedWorktree({ repoRoot, runDir, packet });
package/src/todo-cli.mjs CHANGED
@@ -1759,6 +1759,9 @@ async function independenceCompile({ repoRoot, planKey, inputRef }) {
1759
1759
  previousArtifact,
1760
1760
  });
1761
1761
  const { ref } = await writeTodoIndependenceArtifact({ repoRoot, artifact });
1762
+ // compile成功後にだけ、受理した宣言をruntime readerが要求するcanonical bytesで固定する。
1763
+ // inputがstore自身でも外部draftでも、唯一の正規writerを通して同じ契約へ収束させる。
1764
+ await writeTodoWitnessSet({ repoRoot, witnessSet });
1762
1765
 
1763
1766
  const result = {
1764
1767
  schema: 'lattice.todo_independence_compile_result.v2',
@@ -2286,8 +2286,19 @@ function localTaskRef(ref, plan, taskId) {
2286
2286
  && ref.task_id === taskId;
2287
2287
  }
2288
2288
 
2289
+ function removedTaskIdsFor(revision) {
2290
+ return new Set(revision.task_migration
2291
+ .filter(({ to_task_id, state_policy }) => to_task_id === 'removed' && state_policy === 'removed')
2292
+ .map(({ from_task_id }) => from_task_id));
2293
+ }
2294
+
2295
+ function removedTaskRef(ref, plan, removedTaskIds) {
2296
+ return ref.project_id === plan.project_id && ref.plan_key === plan.plan_key
2297
+ && removedTaskIds.has(ref.task_id);
2298
+ }
2299
+
2289
2300
  function taskSemantics(plan, taskId, idMap, {
2290
- reconciliationMetadata = false, includeDesignMemo = false,
2301
+ reconciliationMetadata = false, includeDesignMemo = false, removedTaskIds = new Set(),
2291
2302
  } = {}) {
2292
2303
  const task = plan.tasks.find(({ task_id }) => task_id === taskId);
2293
2304
  if (!task) return null;
@@ -2302,17 +2313,23 @@ function taskSemantics(plan, taskId, idMap, {
2302
2313
  compile_binding: task.compile_binding, parent_task_id: mapId(task.parent_task_id ?? null),
2303
2314
  };
2304
2315
  const edges = plan.hard_dependencies
2305
- .filter(({ from, to }) => localTaskRef(from, plan, taskId) || localTaskRef(to, plan, taskId))
2316
+ .filter(({ from, to }) => !removedTaskRef(from, plan, removedTaskIds)
2317
+ && !removedTaskRef(to, plan, removedTaskIds)
2318
+ && (localTaskRef(from, plan, taskId) || localTaskRef(to, plan, taskId)))
2306
2319
  .map(({ from, to }) => ({ from: mappedNodeRef(from, plan, idMap), to: mappedNodeRef(to, plan, idMap) }))
2307
2320
  .sort((left, right) => canonicalizeTodoArtifact(left) < canonicalizeTodoArtifact(right) ? -1 : 1);
2308
2321
  const joins = plan.joins
2309
- .filter(({ after, before }) => localTaskRef(before, plan, taskId)
2310
- || after.some((ref) => localTaskRef(ref, plan, taskId)))
2311
- .map((join) => ({ ...join,
2312
- after: join.after.map((ref) => mappedNodeRef(ref, plan, idMap))
2322
+ .flatMap((join) => {
2323
+ if (removedTaskRef(join.before, plan, removedTaskIds)) return [];
2324
+ const after = join.after.filter((ref) => !removedTaskRef(ref, plan, removedTaskIds));
2325
+ if (after.length === 0 || (!localTaskRef(join.before, plan, taskId)
2326
+ && !after.some((ref) => localTaskRef(ref, plan, taskId)))) return [];
2327
+ return [{ ...join,
2328
+ after: after.map((ref) => mappedNodeRef(ref, plan, idMap))
2313
2329
  .sort((left, right) => canonicalizeTodoArtifact(left) < canonicalizeTodoArtifact(right) ? -1 : 1),
2314
- before: mappedNodeRef(join.before, plan, idMap),
2315
- })).sort((left, right) => left.id < right.id ? -1 : 1);
2330
+ before: mappedNodeRef(join.before, plan, idMap),
2331
+ }];
2332
+ }).sort((left, right) => left.id < right.id ? -1 : 1);
2316
2333
  const phaseAcceptDependencies = isDecoupledPhaseTodoPlanSchema(plan.schema)
2317
2334
  ? plan.phase_accept_dependencies
2318
2335
  .filter(({ to }) => localTaskRef(to, plan, taskId))
@@ -2323,7 +2340,7 @@ function taskSemantics(plan, taskId, idMap, {
2323
2340
  }
2324
2341
 
2325
2342
  function phaseV3CarrySemantics(plan, taskId, taskIdMap, phaseIdMap,
2326
- { reconciliationMetadata = false, includeDesignMemo = false } = {}) {
2343
+ { reconciliationMetadata = false, includeDesignMemo = false, removedTaskIds = new Set() } = {}) {
2327
2344
  const task = plan.tasks.find(({ task_id: id }) => id === taskId);
2328
2345
  if (task === undefined) return null;
2329
2346
  const mapTaskRef = (ref) => ref.project_id === plan.project_id && ref.plan_key === plan.plan_key
@@ -2351,13 +2368,17 @@ function phaseV3CarrySemantics(plan, taskId, taskIdMap, phaseIdMap,
2351
2368
  const incoming = [];
2352
2369
  const outgoing = [];
2353
2370
  for (const edge of plan.hard_dependencies) {
2371
+ if (removedTaskRef(edge.from, plan, removedTaskIds)
2372
+ || removedTaskRef(edge.to, plan, removedTaskIds)) continue;
2354
2373
  const mapped = { kind: 'hard', from: mapTaskRef(edge.from), to: mapTaskRef(edge.to) };
2355
2374
  if (localTaskRef(edge.to, plan, taskId)) incoming.push(mapped);
2356
2375
  if (localTaskRef(edge.from, plan, taskId)) outgoing.push(mapped);
2357
2376
  }
2358
2377
  for (const join of plan.joins) {
2378
+ if (removedTaskRef(join.before, plan, removedTaskIds)) continue;
2359
2379
  const to = mapTaskRef(join.before);
2360
2380
  for (const after of join.after) {
2381
+ if (removedTaskRef(after, plan, removedTaskIds)) continue;
2361
2382
  const tuple = { kind: 'join', join_id: join.id, from: mapTaskRef(after), to };
2362
2383
  if (localTaskRef(join.before, plan, taskId)) incoming.push(tuple);
2363
2384
  if (localTaskRef(after, plan, taskId)) outgoing.push(tuple);
@@ -2375,11 +2396,12 @@ function validatePhaseV3Carry(previous, revision, migration, idMap, state) {
2375
2396
  const reconciliationMetadata = migration.state_policy === 'carry_reconciled_metadata';
2376
2397
  const predecessorTask = previous.plan.tasks.find(({ task_id }) => task_id === migration.from_task_id);
2377
2398
  const includeDesignMemo = !reconciliationMetadata && typeof predecessorTask?.design_memo === 'string';
2399
+ const removedTaskIds = removedTaskIdsFor(revision);
2378
2400
  const phaseIdMap = new Map(revision.phase_migration
2379
2401
  .filter(({ from_phase_id, to_phase_id }) => from_phase_id !== null && to_phase_id !== 'removed')
2380
2402
  .map(({ from_phase_id, to_phase_id }) => [from_phase_id, to_phase_id]));
2381
2403
  const before = phaseV3CarrySemantics(previous.plan, migration.from_task_id, idMap, phaseIdMap,
2382
- { reconciliationMetadata, includeDesignMemo });
2404
+ { reconciliationMetadata, includeDesignMemo, removedTaskIds });
2383
2405
  const after = phaseV3CarrySemantics(revision.desired_plan, migration.to_task_id,
2384
2406
  new Map(), new Map(),
2385
2407
  { reconciliationMetadata, includeDesignMemo });
@@ -2412,8 +2434,9 @@ function validateAcquirePhaseCarry(previous, revision, migration, idMap) {
2412
2434
  .map(({ from_phase_id, to_phase_id }) => [from_phase_id, to_phase_id]));
2413
2435
  const predecessorTask = previous.plan.tasks.find(({ task_id }) => task_id === migration.from_task_id);
2414
2436
  const includeDesignMemo = typeof predecessorTask?.design_memo === 'string';
2437
+ const removedTaskIds = removedTaskIdsFor(revision);
2415
2438
  const before = phaseV3CarrySemantics(previous.plan, migration.from_task_id, idMap, phaseIdMap,
2416
- { includeDesignMemo });
2439
+ { includeDesignMemo, removedTaskIds });
2417
2440
  const after = phaseV3CarrySemantics(revision.desired_plan, migration.to_task_id, new Map(), new Map(),
2418
2441
  { includeDesignMemo });
2419
2442
  if (before.task.phase_id !== null) {
@@ -2448,6 +2471,7 @@ function stateMigrationFor(previous, revision) {
2448
2471
  const idMap = new Map(revision.task_migration
2449
2472
  .filter(({ to_task_id }) => to_task_id !== 'removed')
2450
2473
  .map(({ from_task_id, to_task_id }) => [from_task_id, to_task_id]));
2474
+ const removedTaskIds = removedTaskIdsFor(revision);
2451
2475
  const states = new Map(previous.tasks.map((state) => [state.task_id, state]));
2452
2476
  return revision.task_migration.map((migration) => {
2453
2477
  const carriesState = ['carry', 'carry_reconciled_metadata', 'acquire_phase'].includes(migration.state_policy);
@@ -2467,7 +2491,7 @@ function stateMigrationFor(previous, revision) {
2467
2491
  .find(({ task_id }) => task_id === migration.from_task_id);
2468
2492
  const includeDesignMemo = typeof predecessorTask?.design_memo === 'string';
2469
2493
  const before = taskSemantics(previous.plan, migration.from_task_id, idMap,
2470
- { includeDesignMemo });
2494
+ { includeDesignMemo, removedTaskIds });
2471
2495
  const after = taskSemantics(revision.desired_plan, migration.to_task_id, new Map(),
2472
2496
  { includeDesignMemo });
2473
2497
  if (canonicalizeTodoArtifact(before) !== canonicalizeTodoArtifact(after)) {
@@ -2482,7 +2506,7 @@ function stateMigrationFor(previous, revision) {
2482
2506
  const includeDesignMemo = !reconciliationMetadata
2483
2507
  && typeof predecessorTask?.design_memo === 'string';
2484
2508
  const before = taskSemantics(previous.plan, migration.from_task_id, idMap,
2485
- { reconciliationMetadata, includeDesignMemo });
2509
+ { reconciliationMetadata, includeDesignMemo, removedTaskIds });
2486
2510
  const after = taskSemantics(revision.desired_plan, migration.to_task_id, new Map(),
2487
2511
  { reconciliationMetadata, includeDesignMemo });
2488
2512
  if (canonicalizeTodoArtifact(before) !== canonicalizeTodoArtifact(after)) {