@quolu/lattice 0.60.3 → 0.60.5

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.
Files changed (71) hide show
  1. package/LICENSE +147 -147
  2. package/README.ja.md +387 -387
  3. package/README.md +291 -291
  4. package/bin/lattice-dashboard.mjs +7 -3
  5. package/bin/lattice-mcp.mjs +0 -0
  6. package/bin/lattice-scripted-adapter.mjs +0 -0
  7. package/bin/lattice-scripted-worker.mjs +0 -0
  8. package/bin/lattice-work-order-adapter.mjs +0 -0
  9. package/bin/lattice.mjs +2 -4
  10. package/docs/bridge-setup.md +239 -239
  11. package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
  12. package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
  13. package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
  14. package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
  15. package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
  16. package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
  17. package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
  18. package/docs/schemas/lattice.plan_scope_review.v1.schema.json +55 -55
  19. package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
  20. package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
  21. package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
  22. package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
  23. package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
  24. package/docs/schemas/lattice.todo_extraction.v3.schema.json +150 -150
  25. package/docs/schemas/lattice.todo_extraction.v4.schema.json +161 -161
  26. package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
  27. package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
  28. package/docs/schemas/lattice.todo_structure_binding.v1.schema.json +47 -47
  29. package/docs/schemas/lattice.todo_structure_realization.v1.schema.json +55 -55
  30. package/docs/schemas/lattice.todo_structure_set.v1.schema.json +264 -264
  31. package/package.json +109 -108
  32. package/sensor/LICENSE +21 -21
  33. package/sensor/NOTICE +19 -19
  34. package/sensor/dist/bin/lattice-sensor.js +9 -9
  35. package/sensor/dist/bin/node-version-check.js +1 -1
  36. package/sensor/dist/bin/node-version-check.js.map +1 -1
  37. package/sensor/dist/db/index.js +24 -24
  38. package/sensor/dist/db/migrations.js +41 -41
  39. package/sensor/dist/db/queries.js +164 -164
  40. package/sensor/dist/db/schema.sql +205 -205
  41. package/sensor/dist/directory.js +5 -5
  42. package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  43. package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  44. package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  45. package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  46. package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  47. package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  48. package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
  49. package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
  50. package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
  51. package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  52. package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  53. package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
  54. package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
  55. package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
  56. package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  57. package/sensor/dist/mcp/liveness-watchdog.js +53 -53
  58. package/sensor/dist/mcp/server-instructions.js +95 -95
  59. package/sensor/dist/sync/worktree.js +1 -1
  60. package/sensor/dist/sync/worktree.js.map +1 -1
  61. package/sensor/package.json +56 -56
  62. package/src/bridge-daemon.mjs +11 -3
  63. package/src/bridge-hub-landing.mjs +1 -1
  64. package/src/runtime-cli.mjs +7 -0
  65. package/src/runtime-managed-supervisor.mjs +9 -1
  66. package/src/sensor-node-runtime.mjs +3 -3
  67. package/src/todo-cli.mjs +5 -4
  68. package/src/todo-dashboard-registry.mjs +32 -12
  69. package/src/todo-gantt-live.mjs +18 -4
  70. package/src/todo-store-git-transaction.mjs +1 -1
  71. package/src/todo-store.mjs +128 -20
@@ -87,7 +87,7 @@ function dashboardHtml(projects) {
87
87
  }).join('');
88
88
  const content = rows.length === 0 ? '<p>アクティブなプロジェクトはありません。</p>'
89
89
  : `<ul>${rows}</ul>`;
90
- return `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Latticeが管理している公開中の工程と現在地を確認できます。"><meta property="og:title" content="公開中の工程表 — Lattice"><meta property="og:description" content="Latticeが管理している公開中の工程と現在地を確認できます。"><meta name="theme-color" content="#f7f3ea"><title>公開中の工程表 — Lattice</title><style>:root{color-scheme:light;--paper:#f7f3ea;--panel:#fffdf8;--ink:#201d19;--soft:#6c655d;--line:#d8d0c5;--cobalt:#315cbe;--orange:#e85f2a}*{box-sizing:border-box}body{min-height:100vh;margin:0;color:var(--ink);background:var(--paper);font:16px/1.7 system-ui,-apple-system,sans-serif}.shell{max-width:880px;margin:0 auto;padding:28px 22px 40px}.brand{display:flex;align-items:center;gap:9px;padding-bottom:24px;border-bottom:1px solid var(--line);font-size:.88rem}.brand a,.footer a{color:var(--ink);font-weight:800;text-decoration:none}.brand a:hover,.footer a:hover{color:var(--cobalt)}.brand span{color:var(--soft)}main{padding:64px 0 72px}.eyebrow{margin:0 0 8px;color:var(--orange);font-size:.76rem;font-weight:800;letter-spacing:.14em}.lead{max-width:620px;margin:0 0 34px;color:var(--soft)}h1{margin:0 0 14px;font-size:clamp(2rem,6vw,3.4rem);line-height:1.12;letter-spacing:-.04em}ul{display:grid;gap:12px;margin:0;padding:0;list-style:none}li a{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:20px;padding:18px 20px;border:1px solid var(--line);border-radius:12px;color:inherit;background:var(--panel);text-decoration:none;box-shadow:0 8px 28px rgba(48,39,27,.04)}li a:hover{border-color:var(--cobalt);transform:translateY(-1px)}li strong{font-size:1.04rem}li code{color:var(--soft);font-size:.78rem}li span{color:var(--cobalt);font-weight:800}.note{margin:28px 0 0;padding:16px 18px;border-left:3px solid var(--orange);color:var(--soft);background:rgba(255,253,248,.72);font-size:.88rem}.footer{display:flex;flex-wrap:wrap;justify-content:space-between;gap:16px;padding-top:20px;border-top:1px solid var(--line);color:var(--soft);font-size:.82rem}.footer nav{display:flex;gap:18px}@media(max-width:560px){.shell{padding:20px 16px 32px}main{padding:44px 0 56px}li a{grid-template-columns:minmax(0,1fr) auto;padding:16px}li code{grid-column:1/-1;grid-row:2}.footer{display:block}.footer nav{margin-top:10px}}</style></head><body><div class="shell"><header class="brand"><a href="https://kitepon.dev/">kitepon.dev</a><span aria-hidden="true">/</span><strong>Lattice</strong></header><main><p class="eyebrow">LIVE DEVELOPMENT</p><h1>公開中の工程表</h1><p class="lead">Latticeが管理しているプロジェクトの工程と、いまどこまで進んでいるかを公開データから確認できます。</p>${content}<p class="note">表示内容はLatticeの記録から自動生成されます。製品の紹介や使い方はGitHubをご覧ください。</p></main><footer class="footer"><span>kitepon.dev の開発工程を、Latticeで可視化しています。</span><nav aria-label="関連リンク"><a href="https://kitepon.dev/">kitepon.dev</a><a href="https://github.com/kitepon-rgb/Lattice">GitHub</a></nav></footer></div></body></html>`;
90
+ return `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Latticeが管理している公開中の工程と現在地を確認できます。"><meta property="og:title" content="公開中の工程表 — Lattice"><meta property="og:description" content="Latticeが管理している公開中の工程と現在地を確認できます。"><meta name="theme-color" content="#f7f3ea"><title>公開中の工程表 — Lattice</title><style>:root{color-scheme:light;--paper:#f7f3ea;--panel:#fffdf8;--ink:#201d19;--soft:#6c655d;--line:#d8d0c5;--cobalt:#315cbe;--orange:#e85f2a}*{box-sizing:border-box}body{min-height:100vh;margin:0;color:var(--ink);background:var(--paper);font:16px/1.7 system-ui,-apple-system,sans-serif}.shell{max-width:880px;margin:0 auto;padding:28px 22px 40px}.brand{display:flex;align-items:center;gap:9px;padding-bottom:24px;border-bottom:1px solid var(--line);font-size:.88rem}.brand a,.footer a{color:var(--ink);font-weight:800;text-decoration:none}.brand a:hover,.footer a:hover{color:var(--cobalt)}.brand span{color:var(--soft)}main{padding:64px 0 72px}.eyebrow{margin:0 0 8px;color:var(--orange);font-size:.76rem;font-weight:800;letter-spacing:.14em}.lead{max-width:620px;margin:0 0 34px;color:var(--soft)}h1{margin:0 0 14px;font-size:clamp(2rem,6vw,3.4rem);line-height:1.12;letter-spacing:-.04em}ul{display:grid;gap:12px;margin:0;padding:0;list-style:none}li a{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:20px;padding:18px 20px;border:1px solid var(--line);border-radius:12px;color:inherit;background:var(--panel);text-decoration:none;box-shadow:0 8px 28px rgba(48,39,27,.04)}li a:hover{border-color:var(--cobalt);transform:translateY(-1px)}li strong{font-size:1.04rem}li code{color:var(--soft);font-size:.78rem}li span{color:var(--cobalt);font-weight:800}.note{margin:28px 0 0;padding:16px 18px;border-left:3px solid var(--orange);color:var(--soft);background:rgba(255,253,248,.72);font-size:.88rem}.footer{display:flex;flex-wrap:wrap;justify-content:space-between;gap:16px;padding-top:20px;border-top:1px solid var(--line);color:var(--soft);font-size:.82rem}.footer nav{display:flex;gap:18px}@media(max-width:560px){.shell{padding:20px 16px 32px}main{padding:44px 0 56px}li a{grid-template-columns:minmax(0,1fr) auto;padding:16px}li code{grid-column:1/-1;grid-row:2}.footer{display:block}.footer nav{margin-top:10px}}</style></head><body><div class="shell"><header class="brand"><a href="https://kitepon.dev/">kitepon.dev</a><span aria-hidden="true">/</span><strong>Lattice</strong></header><main><p class="eyebrow">LIVE DEVELOPMENT</p><h1>公開中の工程表</h1><p class="lead">Latticeが管理しているプロジェクトの工程と、いまどこまで進んでいるかを公開データから確認できます。</p>${content}<p class="note">表示内容はLatticeの記録から自動生成されます。製品の紹介や使い方はGitHubをご覧ください。</p></main><footer class="footer"><span>kitepon.dev の開発工程を、Latticeで可視化しています。</span><nav aria-label="関連リンク"><a href="https://kitepon.dev/">kitepon.dev</a><a href="https://github.com/kitepon/Lattice">GitHub</a></nav></footer></div></body></html>`;
91
91
  }
92
92
 
93
93
  function notFoundHtml(code, path) {
@@ -181,10 +181,13 @@ function finishRequestFailure(response, code, path) {
181
181
  } catch { response.destroy(); }
182
182
  }
183
183
 
184
- export async function startTodoGanttDashboardServer({ registry, port = 0, redirectSingleProject = false }) {
184
+ export async function startTodoGanttDashboardServer({
185
+ registry, port = 0, redirectSingleProject = false, onShutdown = null,
186
+ }) {
185
187
  if (!Number.isSafeInteger(port) || port < 0 || port > 65_535) throw new TypeError('port must be 0..65535');
186
188
  if (registry === null || typeof registry !== 'object' || typeof registry.get !== 'function'
187
189
  || typeof registry.list !== 'function') throw new TypeError('registry required');
190
+ if (onShutdown !== null && typeof onShutdown !== 'function') throw new TypeError('onShutdown must be a function');
188
191
  const clientsByProject = new Map();
189
192
  const lastHeads = new Map();
190
193
  const lastHeartbeats = new Map();
@@ -208,6 +211,13 @@ export async function startTodoGanttDashboardServer({ registry, port = 0, redire
208
211
  try {
209
212
  const url = new URL(requestPath, `http://${LOOPBACK}`);
210
213
  requestPath = url.pathname;
214
+ if (url.pathname === '/__lattice/shutdown' && request.method === 'POST' && onShutdown !== null) {
215
+ response.writeHead(202, { 'content-type': 'application/json; charset=utf-8',
216
+ 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' });
217
+ response.end(`${JSON.stringify({ schema: 'lattice.todo_dashboard_shutdown.v1', accepted: true })}\n`);
218
+ setImmediate(() => Promise.resolve().then(onShutdown).catch(() => {}));
219
+ return;
220
+ }
211
221
  if (request.method !== 'GET') {
212
222
  response.setHeader('allow', 'GET');
213
223
  sendHttpError(response, 405, 'METHOD_NOT_ALLOWED', requestPath);
@@ -341,9 +351,13 @@ export async function startTodoGanttDashboardServer({ registry, port = 0, redire
341
351
  });
342
352
  }
343
353
 
344
- export async function startTodoGanttLiveServer({ projectId, displayName = projectId, port = 0, render, readHead }) {
354
+ export async function startTodoGanttLiveServer({
355
+ projectId, displayName = projectId, port = 0, render, readHead, onShutdown = null,
356
+ }) {
345
357
  const registry = createTodoGanttProjectRegistry([{ projectId, displayName, render, readHead }]);
346
- const dashboard = await startTodoGanttDashboardServer({ registry, port, redirectSingleProject: true });
358
+ const dashboard = await startTodoGanttDashboardServer({
359
+ registry, port, redirectSingleProject: true, onShutdown,
360
+ });
347
361
  const path = projectPath(projectId);
348
362
  return Object.freeze({
349
363
  projectId,
@@ -117,7 +117,7 @@ async function actualIndexPath(repoRoot, env) {
117
117
  }
118
118
 
119
119
  async function syncPath(target) {
120
- const handle = await open(target, fsConstants.O_RDONLY);
120
+ const handle = await open(target, fsConstants.O_RDWR);
121
121
  try { await handle.sync(); } finally { await handle.close(); }
122
122
  }
123
123
 
@@ -1177,6 +1177,102 @@ function pinnedSourceLine(repoRoot, source, cache = null) {
1177
1177
  throw new Error('line outside blob');
1178
1178
  }
1179
1179
 
1180
+ // 検証対象objectの一括prefetch。event payloadに現れるevidence記述子とimport sourceを
1181
+ // 収集し、repoごとに1回の`cat-file --batch`でcacheへseedする。object 1個=spawn 1回の
1182
+ // 雪崩はWindowsで起動単価66〜100msになり、evidenceが育ったstoreのstatusを桁で遅くする。
1183
+ // 成功した読みだけをcacheへ入れ、missing・型不一致・batchごと失敗(repo不在等の境界)は
1184
+ // 何もcacheしない——正誤の裁定は従来どおりper-objectの検証経路が同じ結果を出す。
1185
+ function prefetchVerificationObjects(manifest, repoRoot, events, pinnedSourceCache) {
1186
+ const candidates = [];
1187
+ for (const event of events) {
1188
+ // 改版genesisはevent直下のstate_migrationでtask状態(evidence込み)を搬送する。
1189
+ // replayがcarry系だけを復元・検証するので、収集も同じpolicyへ揃える。
1190
+ if (Array.isArray(event.state_migration)) {
1191
+ for (const migration of event.state_migration) {
1192
+ if (['carry', 'carry_reconciled_metadata', 'acquire_phase'].includes(migration.state_policy)
1193
+ && migration.state !== null && typeof migration.state === 'object'
1194
+ && migration.state.evidence !== null && typeof migration.state.evidence === 'object') {
1195
+ candidates.push(migration.state.evidence);
1196
+ }
1197
+ }
1198
+ }
1199
+ const payload = event.payload;
1200
+ if (payload === null || typeof payload !== 'object') continue;
1201
+ if (payload.evidence !== null && typeof payload.evidence === 'object') candidates.push(payload.evidence);
1202
+ if (payload.decision_evidence !== null && typeof payload.decision_evidence === 'object') {
1203
+ candidates.push(payload.decision_evidence);
1204
+ }
1205
+ if (Array.isArray(payload.evidence_slots)) {
1206
+ for (const slot of payload.evidence_slots) {
1207
+ if (slot !== null && typeof slot === 'object'
1208
+ && slot.evidence !== null && typeof slot.evidence === 'object') candidates.push(slot.evidence);
1209
+ }
1210
+ }
1211
+ }
1212
+ const repositories = new Map(manifest.repositories.map((repo) => [repo.repo_id, repo.path]));
1213
+ const oidsByRepo = new Map();
1214
+ const pinnedCommits = new Set();
1215
+ const pinnedSpecs = new Set();
1216
+ for (const candidate of candidates) {
1217
+ if (validateEvidenceDescriptor(candidate)) {
1218
+ const repoRef = repositories.get(candidate.repo_id);
1219
+ if (repoRef === undefined) continue;
1220
+ const absoluteRepo = path.resolve(repoRoot, repoRef);
1221
+ if (evidenceBlobCache.has(`${absoluteRepo}\0${candidate.git_blob_oid}`)) continue;
1222
+ let oids = oidsByRepo.get(absoluteRepo);
1223
+ if (oids === undefined) oidsByRepo.set(absoluteRepo, oids = new Set());
1224
+ oids.add(candidate.git_blob_oid);
1225
+ } else if (validateTodoImportSource(candidate) && pinnedSourceCache !== null) {
1226
+ if (!pinnedSourceCache.commits.has(candidate.source_commit)) pinnedCommits.add(candidate.source_commit);
1227
+ const spec = `${candidate.source_commit}:${candidate.origin_plan_ref}`;
1228
+ if (!pinnedSourceCache.blobs.has(spec)) pinnedSpecs.add(spec);
1229
+ }
1230
+ }
1231
+ for (const [absoluteRepo, oids] of oidsByRepo) {
1232
+ let entries;
1233
+ try {
1234
+ entries = gitCatFileBatch([...oids], {
1235
+ cwd: absoluteRepo, maxBodyBytes: TODO_LIMITS.narrativeSectionBytes + 1,
1236
+ });
1237
+ } catch { continue; }
1238
+ let index = 0;
1239
+ for (const oid of oids) {
1240
+ const entry = entries[index];
1241
+ index += 1;
1242
+ // seedしないだけで正誤は変えない: 上限超過bodyは単体読みのmaxBufferでは読めない
1243
+ // (=従来は未検証)ので、batch同乗の予算合算で検証済みへ昇格させない。cacheが
1244
+ // 満杯なら追い出さずseedを止める——seeding自身の追い出しは自分のseedを連鎖破壊し、
1245
+ // 513件以上で全件個別spawnへ戻る。溢れた分はper-item経路が従来どおり扱う。
1246
+ if (entry.missing || entry.type !== 'blob'
1247
+ || entry.bytes.length > TODO_LIMITS.narrativeSectionBytes
1248
+ || evidenceBlobCache.size >= EVIDENCE_BLOB_CACHE_LIMIT) continue;
1249
+ evidenceBlobCache.set(`${absoluteRepo}\0${oid}`, entry.bytes);
1250
+ }
1251
+ }
1252
+ const pinnedNames = [...pinnedCommits, ...pinnedSpecs];
1253
+ if (pinnedNames.length === 0) return;
1254
+ let entries;
1255
+ try {
1256
+ entries = gitCatFileBatch(pinnedNames, {
1257
+ cwd: repoRoot, maxBodyBytes: TODO_LIMITS.narrativeSectionBytes + 1,
1258
+ });
1259
+ } catch { return; }
1260
+ let index = 0;
1261
+ for (const commit of pinnedCommits) {
1262
+ const entry = entries[index];
1263
+ index += 1;
1264
+ if (!entry.missing && entry.type === 'commit') pinnedSourceCache.commits.add(commit);
1265
+ }
1266
+ for (const spec of pinnedSpecs) {
1267
+ const entry = entries[index];
1268
+ index += 1;
1269
+ if (!entry.missing && entry.type === 'blob'
1270
+ && entry.bytes.length <= TODO_LIMITS.narrativeSectionBytes) {
1271
+ pinnedSourceCache.blobs.set(spec, entry.bytes);
1272
+ }
1273
+ }
1274
+ }
1275
+
1180
1276
  function markdownCheckboxState(lineBytes) {
1181
1277
  if (lineBytes.length >= 3 && lineBytes[0] === 0xef && lineBytes[1] === 0xbb && lineBytes[2] === 0xbf) return null;
1182
1278
  let line;
@@ -1221,7 +1317,7 @@ function narrativeAnchorSource(value) {
1221
1317
  });
1222
1318
  }
1223
1319
 
1224
- function materializeImportedNarrativeAnchors(repoRoot, planInput, sources) {
1320
+ function materializeImportedNarrativeAnchors(repoRoot, planInput, sources, cache = null) {
1225
1321
  if (sources === undefined) return planInput;
1226
1322
  if (!['lattice.todo_plan.v2', 'lattice.todo_plan.v6'].includes(planInput?.schema)
1227
1323
  || !Array.isArray(sources)
@@ -1241,7 +1337,7 @@ function materializeImportedNarrativeAnchors(repoRoot, planInput, sources) {
1241
1337
  if (source !== undefined && task.narrative_ref === source.origin_plan_ref
1242
1338
  && ['checked', 'unchecked'].includes(source.checkbox_state)) {
1243
1339
  try {
1244
- const line = pinnedSourceLine(repoRoot, source);
1340
+ const line = pinnedSourceLine(repoRoot, source, cache);
1245
1341
  if (markdownCheckboxState(line) === source.checkbox_state) {
1246
1342
  narrativeAnchor = {
1247
1343
  origin_plan_ref: source.origin_plan_ref,
@@ -1259,7 +1355,7 @@ function materializeImportedNarrativeAnchors(repoRoot, planInput, sources) {
1259
1355
  };
1260
1356
  }
1261
1357
 
1262
- function verifyPlanNarrativeAnchors(repoRoot, plan, trustedPlan = null) {
1358
+ function verifyPlanNarrativeAnchors(repoRoot, plan, trustedPlan = null, cache = null) {
1263
1359
  if (!['lattice.todo_plan.v2', 'lattice.todo_plan.v3', 'lattice.todo_plan.v4',
1264
1360
  'lattice.todo_plan.v5', 'lattice.todo_plan.v6', 'lattice.todo_plan.v7'].includes(plan.schema)) return;
1265
1361
  const trusted = new Map((['lattice.todo_plan.v2', 'lattice.todo_plan.v3', 'lattice.todo_plan.v4',
@@ -1273,7 +1369,7 @@ function verifyPlanNarrativeAnchors(repoRoot, plan, trustedPlan = null) {
1273
1369
  if (previous !== undefined
1274
1370
  && canonicalizeTodoArtifact(previous) === canonicalizeTodoArtifact(anchor)) continue;
1275
1371
  try {
1276
- const line = pinnedSourceLine(repoRoot, anchor);
1372
+ const line = pinnedSourceLine(repoRoot, anchor, cache);
1277
1373
  if (sha256Bytes(line) !== anchor.source_line_digest || markdownCheckboxState(line) === null) {
1278
1374
  throw new Error('anchor mismatch');
1279
1375
  }
@@ -1374,6 +1470,7 @@ export async function readTodoStore(options = {}) {
1374
1470
  && descriptor.active_revision_digest !== plan.plan_digest) {
1375
1471
  fail('STORE_INCONSISTENT', 'manifest_revision_binding_mismatch');
1376
1472
  }
1473
+ prefetchVerificationObjects(manifest, repoRoot, journal.events, pinnedSourceCache);
1377
1474
  const verifyEvidence = evidenceVerifier(manifest, repoRoot, options.forWrite === true);
1378
1475
  const verifyImportSource = importSourceVerifier(repoRoot, options.forWrite === true, pinnedSourceCache);
1379
1476
  const tasks = replay(plan, journal.events, {
@@ -1471,7 +1568,13 @@ async function atomicWrite(absolute, bytes) {
1471
1568
  let handle;
1472
1569
  try {
1473
1570
  handle = await open(temporary, 'wx', 0o600);
1474
- await handle.writeFile(bytes); await handle.sync(); await handle.close(); handle = null;
1571
+ await handle.writeFile(bytes);
1572
+ // WSL2でのNTFSパス(/mnt/以下)ではfsyncがEIOを返すことがある。データ自体は書き込まれている。
1573
+ try { await handle.sync(); } catch (syncError) {
1574
+ if (!(process.platform === 'linux' && /^\/mnt\//.test(absolute) && syncError?.code === 'EIO')) throw syncError;
1575
+ process.stderr.write(`lattice: fsync skipped (WSL2/NTFS EIO) ${absolute}\n`);
1576
+ }
1577
+ await handle.close(); handle = null;
1475
1578
  await rename(temporary, absolute);
1476
1579
  const directory = await open(path.dirname(absolute), 'r');
1477
1580
  // Windowsはdirectory handleのfsyncを許さず常にEPERM/EINVALを返す(Node仕様)。
@@ -1493,7 +1596,11 @@ async function atomicWriteMode(absolute, bytes, mode) {
1493
1596
  try {
1494
1597
  handle = await open(temporary, 'wx', mode);
1495
1598
  await handle.writeFile(bytes); await handle.chmod(mode);
1496
- await handle.sync(); await handle.close(); handle = null;
1599
+ try { await handle.sync(); } catch (syncError) {
1600
+ if (!(process.platform === 'linux' && /^\/mnt\//.test(absolute) && syncError?.code === 'EIO')) throw syncError;
1601
+ process.stderr.write(`lattice: fsync skipped (WSL2/NTFS EIO) ${absolute}\n`);
1602
+ }
1603
+ await handle.close(); handle = null;
1497
1604
  await rename(temporary, absolute);
1498
1605
  await fsyncDirectory(path.dirname(absolute));
1499
1606
  } finally {
@@ -1549,23 +1656,20 @@ function processIsDead(pid) {
1549
1656
 
1550
1657
  async function acquireCrossPlanRecoveryClaim(recoveryClaimsRef) {
1551
1658
  await mkdir(recoveryClaimsRef, { recursive: true, mode: 0o700 });
1552
- const claimName = `${process.pid}-${randomBytes(8).toString('hex')}`;
1659
+ const claimOrder = process.hrtime.bigint().toString().padStart(20, '0');
1660
+ const claimName = `${claimOrder}-${process.pid}-${randomBytes(8).toString('hex')}`;
1553
1661
  const claimRef = path.join(recoveryClaimsRef, claimName);
1554
1662
  await mkdir(claimRef, { mode: 0o700 });
1555
1663
  for (const entry of await readdir(recoveryClaimsRef, { withFileTypes: true })) {
1556
1664
  if (!entry.isDirectory() || entry.name === claimName) continue;
1557
- const pid = Number(entry.name.split('-', 1)[0]);
1665
+ const parts = entry.name.split('-', 3);
1666
+ const pid = Number(parts.length === 2 ? parts[0] : parts[1]);
1558
1667
  if (Number.isSafeInteger(pid) && pid > 0 && processIsDead(pid)) {
1559
1668
  await rm(path.join(recoveryClaimsRef, entry.name), { recursive: true, force: true });
1560
1669
  }
1561
1670
  }
1562
- const candidates = await Promise.all((await readdir(recoveryClaimsRef)).map(async (name) => ({
1563
- name,
1564
- mtimeNs: (await lstat(path.join(recoveryClaimsRef, name), { bigint: true })).mtimeNs,
1565
- })));
1566
- candidates.sort((left, right) => left.mtimeNs < right.mtimeNs ? -1
1567
- : left.mtimeNs > right.mtimeNs ? 1 : left.name < right.name ? -1 : 1);
1568
- if (candidates[0]?.name === claimName) return claimRef;
1671
+ const candidates = (await readdir(recoveryClaimsRef)).sort();
1672
+ if (candidates[0] === claimName) return claimRef;
1569
1673
  await rmdir(claimRef);
1570
1674
  return null;
1571
1675
  }
@@ -1857,10 +1961,13 @@ export async function appendTodoEvent(options = {}) {
1857
1961
  }
1858
1962
  // Validate the prospective history, including hard evidence and transitions, before any write.
1859
1963
  validateMergedTransition(store, member, event);
1964
+ const appendSourceCache = { commits: new Set(), blobs: new Map() };
1965
+ prefetchVerificationObjects(store.manifest, repoRoot,
1966
+ [...member.journal.events, event], appendSourceCache);
1860
1967
  replay(member.plan, [...member.journal.events, event], {
1861
1968
  now: options.now ? new Date(options.now) : new Date(),
1862
1969
  verifyEvidence: evidenceVerifier(store.manifest, repoRoot, true),
1863
- verifyImportSource: importSourceVerifier(repoRoot, true),
1970
+ verifyImportSource: importSourceVerifier(repoRoot, true, appendSourceCache),
1864
1971
  });
1865
1972
  const eventBytes = canonicalLine(event);
1866
1973
  const activeRef = member.descriptor.journal_ref;
@@ -1879,7 +1986,7 @@ export async function appendTodoEvent(options = {}) {
1879
1986
  const tasks = replay(member.plan, [...member.journal.events, event], {
1880
1987
  now: options.now ? new Date(options.now) : new Date(),
1881
1988
  verifyEvidence: evidenceVerifier(store.manifest, repoRoot, false),
1882
- verifyImportSource: importSourceVerifier(repoRoot, false),
1989
+ verifyImportSource: importSourceVerifier(repoRoot, false, appendSourceCache),
1883
1990
  });
1884
1991
  const snapshot = snapshotFor(member.plan, [...member.journal.events, event], tasks);
1885
1992
  await atomicWrite(path.resolve(repoRoot, member.descriptor.snapshot_ref), canonicalLine(snapshot));
@@ -2059,14 +2166,15 @@ function historicalImportInputs(value, timestampKey) {
2059
2166
  }
2060
2167
 
2061
2168
  function prepareImportedArtifacts(repoRoot, options, projectId, memberPlans) {
2169
+ const importSourceCache = { commits: new Set(), blobs: new Map() };
2062
2170
  const planInput = materializeImportedNarrativeAnchors(
2063
- repoRoot, options.plan, options.narrativeAnchorSources,
2171
+ repoRoot, options.plan, options.narrativeAnchorSources, importSourceCache,
2064
2172
  );
2065
2173
  const plan = buildTodoPlan(planInput);
2066
2174
  if (plan.project_id !== projectId || plan.predecessor_plan_digest !== null) {
2067
2175
  throw new TypeError('imported plan must be a project-local genesis plan');
2068
2176
  }
2069
- verifyPlanNarrativeAnchors(repoRoot, plan);
2177
+ verifyPlanNarrativeAnchors(repoRoot, plan, null, importSourceCache);
2070
2178
  const genesis = buildPlanGenesis(plan, { ...options.genesis, historical_import: true });
2071
2179
  const events = [genesis];
2072
2180
  const inProgressTasks = options.inProgressTasks ?? [];
@@ -2084,7 +2192,7 @@ function prepareImportedArtifacts(repoRoot, options, projectId, memberPlans) {
2084
2192
  }
2085
2193
  for (const input of inProgressTasks) events.push(buildHistoricalStart(plan, events.at(-1), input, genesis));
2086
2194
  for (const input of completedTasks) events.push(buildHistoricalDone(plan, events.at(-1), input, genesis));
2087
- const verifyImportSource = importSourceVerifier(repoRoot, true);
2195
+ const verifyImportSource = importSourceVerifier(repoRoot, true, importSourceCache);
2088
2196
  const tasks = replay(plan, events, {
2089
2197
  now: options.now ? new Date(options.now) : new Date(), verifyImportSource,
2090
2198
  });