@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.
- package/LICENSE +147 -147
- package/README.ja.md +387 -387
- package/README.md +291 -291
- package/bin/lattice-dashboard.mjs +7 -3
- package/bin/lattice-mcp.mjs +0 -0
- package/bin/lattice-scripted-adapter.mjs +0 -0
- package/bin/lattice-scripted-worker.mjs +0 -0
- package/bin/lattice-work-order-adapter.mjs +0 -0
- package/bin/lattice.mjs +2 -4
- package/docs/bridge-setup.md +239 -239
- package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
- package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
- package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
- package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
- package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
- package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
- package/docs/schemas/lattice.plan_scope_review.v1.schema.json +55 -55
- package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
- package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
- package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
- package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
- package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +150 -150
- package/docs/schemas/lattice.todo_extraction.v4.schema.json +161 -161
- package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
- package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
- package/docs/schemas/lattice.todo_structure_binding.v1.schema.json +47 -47
- package/docs/schemas/lattice.todo_structure_realization.v1.schema.json +55 -55
- package/docs/schemas/lattice.todo_structure_set.v1.schema.json +264 -264
- package/package.json +109 -108
- package/sensor/LICENSE +21 -21
- package/sensor/NOTICE +19 -19
- package/sensor/dist/bin/lattice-sensor.js +9 -9
- package/sensor/dist/bin/node-version-check.js +1 -1
- package/sensor/dist/bin/node-version-check.js.map +1 -1
- package/sensor/dist/db/index.js +24 -24
- package/sensor/dist/db/migrations.js +41 -41
- package/sensor/dist/db/queries.js +164 -164
- package/sensor/dist/db/schema.sql +205 -205
- package/sensor/dist/directory.js +5 -5
- package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/sensor/dist/mcp/liveness-watchdog.js +53 -53
- package/sensor/dist/mcp/server-instructions.js +95 -95
- package/sensor/dist/sync/worktree.js +1 -1
- package/sensor/dist/sync/worktree.js.map +1 -1
- package/sensor/package.json +56 -56
- package/src/bridge-daemon.mjs +11 -3
- package/src/bridge-hub-landing.mjs +1 -1
- package/src/runtime-cli.mjs +7 -0
- package/src/runtime-managed-supervisor.mjs +9 -1
- package/src/sensor-node-runtime.mjs +3 -3
- package/src/todo-cli.mjs +5 -4
- package/src/todo-dashboard-registry.mjs +32 -12
- package/src/todo-gantt-live.mjs +18 -4
- package/src/todo-store-git-transaction.mjs +1 -1
- package/src/todo-store.mjs +128 -20
package/src/todo-gantt-live.mjs
CHANGED
|
@@ -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
|
|
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({
|
|
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({
|
|
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({
|
|
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.
|
|
120
|
+
const handle = await open(target, fsConstants.O_RDWR);
|
|
121
121
|
try { await handle.sync(); } finally { await handle.close(); }
|
|
122
122
|
}
|
|
123
123
|
|
package/src/todo-store.mjs
CHANGED
|
@@ -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);
|
|
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();
|
|
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
|
|
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
|
|
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 =
|
|
1563
|
-
|
|
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
|
});
|