@quolu/lattice 0.67.2 → 0.67.4
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/package.json +1 -1
- package/src/runtime-diff-observer.mjs +5 -55
- package/src/todo-cli.mjs +3 -1
- package/src/todo-store.mjs +16 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.67.
|
|
3
|
+
"version": "0.67.4",
|
|
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",
|
|
@@ -23,33 +23,6 @@ const MAX_TRACKED_FILE_BYTES = 4_194_304;
|
|
|
23
23
|
const GIT_SHA1 = /^[0-9a-f]{40}$/;
|
|
24
24
|
const LINE_ID = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
* gitignore 済みのコンパイラ/ツール出力 directory 名。
|
|
28
|
-
* 観測から外すのは ignored かつこの segment を持つ path だけ。
|
|
29
|
-
* tracked な `bin/`(CLI の正本)は status code が `!!` ではないので残る。
|
|
30
|
-
* gitignore 迂回の検知(ignored なソース相当 file)は残す。
|
|
31
|
-
*/
|
|
32
|
-
export const GENERATED_OUTPUT_DIR_NAMES = Object.freeze([
|
|
33
|
-
'obj', 'bin', 'node_modules', '.vs', 'TestResults',
|
|
34
|
-
'__pycache__', '.pytest_cache', 'dist', 'coverage',
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
export function isGeneratedOutputPath(relativePath) {
|
|
38
|
-
if (typeof relativePath !== 'string' || relativePath.length === 0) return false;
|
|
39
|
-
return relativePath.replace(/\/$/u, '').split('/').some(
|
|
40
|
-
(segment) => GENERATED_OUTPUT_DIR_NAMES.includes(segment),
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isIgnoredStatus(code) {
|
|
45
|
-
return typeof code === 'string' && code.includes('!');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function keepObservedEntry(entry) {
|
|
49
|
-
if (!isIgnoredStatus(entry.code)) return true;
|
|
50
|
-
return !isGeneratedOutputPath(entry.path);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
26
|
function fail(reason) {
|
|
54
27
|
throw new TypeError(`diff observer契約違反: ${reason}`);
|
|
55
28
|
}
|
|
@@ -199,14 +172,12 @@ export async function captureWorktreeDiff(options = {}) {
|
|
|
199
172
|
fail(`worktree HEADがbaseの子孫でない(reset・branch切替・rebase): ${head}`);
|
|
200
173
|
}
|
|
201
174
|
}
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
// ただし obj/bin/node_modules 等のコンパイラ出力は成果ではない。
|
|
205
|
-
// 展開すると MAX_DIFF_ENTRIES を踏み、accept が undeclared_write で hold する。
|
|
175
|
+
// gitignore済みfileは観測しない(オーナー裁定 2026-08-29: gitignore迂回という脅威は
|
|
176
|
+
// 実在せず、ignored観測はツールキャッシュで正当なacceptをholdする実害だけを生んだ)。
|
|
206
177
|
const statusBytes = await run('git', [
|
|
207
|
-
'status', '--porcelain=v1', '-z', '--untracked-files=all',
|
|
178
|
+
'status', '--porcelain=v1', '-z', '--untracked-files=all',
|
|
208
179
|
], worktreePath);
|
|
209
|
-
const entries = statusEntries(statusBytes)
|
|
180
|
+
const entries = statusEntries(statusBytes);
|
|
210
181
|
// commit済みの変更はstatusへ出ない。base..HEADの範囲も観測へ入れないと、
|
|
211
182
|
// commitした瞬間に変更が観測から消える。
|
|
212
183
|
if (head !== baseSha) {
|
|
@@ -221,28 +192,7 @@ export async function captureWorktreeDiff(options = {}) {
|
|
|
221
192
|
if (entries.length > MAX_DIFF_ENTRIES) {
|
|
222
193
|
fail(`diff entry数が上限を超える: ${entries.length}`);
|
|
223
194
|
}
|
|
224
|
-
|
|
225
|
-
// (集約のまま扱うとdirectoryをspecial file扱いで落とし、write pathを特定できない)。
|
|
226
|
-
const expanded = [];
|
|
227
|
-
for (const entry of entries) {
|
|
228
|
-
if (isIgnoredStatus(entry.code) && isGeneratedOutputPath(entry.path)) continue;
|
|
229
|
-
if (!entry.path.endsWith('/')) {
|
|
230
|
-
expanded.push(entry);
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const inner = await run('git', [
|
|
234
|
-
'ls-files', '--others', '--ignored', '--exclude-standard', '-z', '--', entry.path,
|
|
235
|
-
], worktreePath);
|
|
236
|
-
for (const innerPath of inner.toString('utf8').split('\0')) {
|
|
237
|
-
if (innerPath.length === 0) continue;
|
|
238
|
-
const innerEntry = { path: innerPath, code: entry.code };
|
|
239
|
-
if (!keepObservedEntry(innerEntry)) continue;
|
|
240
|
-
expanded.push(innerEntry);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (expanded.length > MAX_DIFF_ENTRIES) {
|
|
244
|
-
fail(`diff entry数が上限を超える: ${expanded.length}`);
|
|
245
|
-
}
|
|
195
|
+
const expanded = entries;
|
|
246
196
|
const seen = new Set();
|
|
247
197
|
const records = [];
|
|
248
198
|
for (const entry of expanded) {
|
package/src/todo-cli.mjs
CHANGED
|
@@ -1052,7 +1052,8 @@ async function dependencyConnect({
|
|
|
1052
1052
|
async function dependencyDisconnect({
|
|
1053
1053
|
repoRoot, env, fromPlanKey, fromTaskId, toPlanKey, toTaskId, reason,
|
|
1054
1054
|
}) {
|
|
1055
|
-
|
|
1055
|
+
// 修理扉: 越境依存の固定digestが陳腐化した store でも disconnect だけは通す(todo-store.mjs 参照)
|
|
1056
|
+
const store = await readTodoStore({ repoRoot, tolerateStaleBindings: true });
|
|
1056
1057
|
const live = projectTodoCrossPlanDependencies(store.members).filter((dependency) => (
|
|
1057
1058
|
dependency.from.plan_key === fromPlanKey && dependency.from.task_id === fromTaskId
|
|
1058
1059
|
&& dependency.to.plan_key === toPlanKey && dependency.to.task_id === toTaskId
|
|
@@ -1068,6 +1069,7 @@ async function dependencyDisconnect({
|
|
|
1068
1069
|
.some((event) => event.event_digest === target.event_digest));
|
|
1069
1070
|
const { event } = await appendTodoEvent({
|
|
1070
1071
|
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), planKey: owner.plan.plan_key,
|
|
1072
|
+
tolerateStaleBindings: true,
|
|
1071
1073
|
event: {
|
|
1072
1074
|
kind: 'cross_plan_dependency_removed', actor: mutationActor(env),
|
|
1073
1075
|
payload: { target_event_digest: target.event_digest, reason },
|
package/src/todo-store.mjs
CHANGED
|
@@ -879,7 +879,13 @@ function snapshotFor(plan, events, tasks) {
|
|
|
879
879
|
return snapshot;
|
|
880
880
|
}
|
|
881
881
|
|
|
882
|
-
function validateMergedGraph(members, crossPlanDependencies = []) {
|
|
882
|
+
function validateMergedGraph(members, crossPlanDependencies = [], options = {}) {
|
|
883
|
+
// tolerateStaleBindings は dependency disconnect 専用の修理扉。plan revise が越境依存の
|
|
884
|
+
// 固定digestを陳腐化させると、通常loadは binding_stale で fail closed するが、その修理
|
|
885
|
+
// コマンド自身も load を要するため、扉が無いと修理経路が存在しない(実被弾 2026-08-29:
|
|
886
|
+
// evidence-2 改訂で store 全体が読めなくなり、disconnect も同エラーで詰んだ)。
|
|
887
|
+
// 許容するのは digest 不一致だけで、dangling / missing は修理扉でも fail する。
|
|
888
|
+
const tolerateStale = options.tolerateStaleBindings === true;
|
|
883
889
|
const tasks = new Map();
|
|
884
890
|
for (const member of members) for (const task of member.plan.tasks) {
|
|
885
891
|
tasks.set(`${member.plan.project_id}\0${member.plan.plan_key}\0${task.task_id}`, member.plan.topology_digest);
|
|
@@ -900,7 +906,7 @@ function validateMergedGraph(members, crossPlanDependencies = []) {
|
|
|
900
906
|
if (topology === undefined) fail('STORE_INCONSISTENT', 'dangling_dependency');
|
|
901
907
|
if ((ref.project_id !== ownerPlan.project_id || ref.plan_key !== ownerPlan.plan_key)
|
|
902
908
|
&& ref.expected_topology_digest === undefined) fail('STORE_INCONSISTENT', 'cross_plan_binding_missing');
|
|
903
|
-
if (ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
|
|
909
|
+
if (!tolerateStale && ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
|
|
904
910
|
fail('STORE_INCONSISTENT', 'binding_stale');
|
|
905
911
|
}
|
|
906
912
|
return key;
|
|
@@ -911,7 +917,7 @@ function validateMergedGraph(members, crossPlanDependencies = []) {
|
|
|
911
917
|
if (topology === undefined) fail('STORE_INCONSISTENT', 'dangling_phase_dependency');
|
|
912
918
|
if ((ref.project_id !== ownerPlan.project_id || ref.plan_key !== ownerPlan.plan_key)
|
|
913
919
|
&& ref.expected_topology_digest === undefined) fail('STORE_INCONSISTENT', 'cross_plan_binding_missing');
|
|
914
|
-
if (ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
|
|
920
|
+
if (!tolerateStale && ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
|
|
915
921
|
fail('STORE_INCONSISTENT', 'binding_stale');
|
|
916
922
|
}
|
|
917
923
|
return key;
|
|
@@ -1629,7 +1635,9 @@ export async function readTodoStore(options = {}) {
|
|
|
1629
1635
|
snapshot: snapshotStale ? expectedSnapshot : snapshot,
|
|
1630
1636
|
tasks, phases, coordination, snapshot_stale: snapshotStale });
|
|
1631
1637
|
}
|
|
1632
|
-
validateMergedGraph(loaded, projectTodoCrossPlanDependencies(loaded)
|
|
1638
|
+
validateMergedGraph(loaded, projectTodoCrossPlanDependencies(loaded), {
|
|
1639
|
+
tolerateStaleBindings: options.tolerateStaleBindings === true,
|
|
1640
|
+
});
|
|
1633
1641
|
return {
|
|
1634
1642
|
schema: 'lattice.todo_store_read.v1', project_id: manifest.project_id, manifest,
|
|
1635
1643
|
members: loaded, snapshot_stale: loaded.some((member) => member.snapshot_stale),
|
|
@@ -2036,6 +2044,10 @@ export async function appendTodoEvent(options = {}) {
|
|
|
2036
2044
|
const store = await readTodoStore({
|
|
2037
2045
|
repoRoot, forWrite: true, now: options.now, evidenceRepair,
|
|
2038
2046
|
evidenceRepairCapability: EVIDENCE_REPAIR_CAPABILITY,
|
|
2047
|
+
// 修理扉は除去event(tombstone)の書込みだけに開く。それ以外の書込みは従来どおり
|
|
2048
|
+
// binding_stale で fail closed する(validateMergedGraph の注記を参照)
|
|
2049
|
+
tolerateStaleBindings: options.tolerateStaleBindings === true
|
|
2050
|
+
&& options.event.kind === 'cross_plan_dependency_removed',
|
|
2039
2051
|
});
|
|
2040
2052
|
const member = store.members.find(({ descriptor }) => descriptor.plan_key === options.planKey);
|
|
2041
2053
|
if (!member) fail('STORE_INCONSISTENT', 'plan_not_active');
|