@quolu/lattice 0.67.3 → 0.67.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/package.json +1 -1
- package/src/cli-help.mjs +1 -0
- package/src/todo-cli.mjs +54 -4
- package/src/todo-contracts.mjs +6 -0
- package/src/todo-store.mjs +28 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.67.
|
|
3
|
+
"version": "0.67.5",
|
|
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",
|
package/src/cli-help.mjs
CHANGED
|
@@ -110,6 +110,7 @@ Write commands:
|
|
|
110
110
|
reopen --plan <key> --task <id> --reason <text> [--override-reason <text>]
|
|
111
111
|
evidence promote --plan <key> --task <id> --evidence <file>
|
|
112
112
|
# done状態と完了時刻を維持し、最新doneへ追記eventで証拠を再束縛する
|
|
113
|
+
start --plan <key> --task <id> --rebind --reason <text> # 改訂でcarryされたin-progressへ現版のstart束縛を積み直す(ADR 0191)
|
|
113
114
|
dependency connect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>
|
|
114
115
|
dependency disconnect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>
|
|
115
116
|
# 開発中に発見した依存を明示接続する(同一plan内・plan跨ぎとも)。依存の自動推定は行わない
|
package/src/todo-cli.mjs
CHANGED
|
@@ -901,6 +901,47 @@ async function startTask({
|
|
|
901
901
|
structureContext });
|
|
902
902
|
}
|
|
903
903
|
|
|
904
|
+
// plan改訂でcarryされたin-progress ToDoの再束縛入口(ADR 0191)。task状態・開始時刻は
|
|
905
|
+
// 変えず、現plan_version・現actorのstart束縛だけをjournalへ積む。runtime intakeは
|
|
906
|
+
// literal startだけを束縛に使いcarryを推定しないため、改訂を跨いだ進行中工程の
|
|
907
|
+
// 再開はこの入口だけが正規経路になる。advisory・independence gateは初回startで
|
|
908
|
+
// 済んでいるため再適用しない。
|
|
909
|
+
async function rebindStart({ repoRoot, env, planKey, taskId, reason }) {
|
|
910
|
+
const store = await readTodoStore({ repoRoot });
|
|
911
|
+
const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
|
|
912
|
+
if (member === undefined) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active');
|
|
913
|
+
const task = member.tasks.find(({ task_id: candidate }) => candidate === taskId);
|
|
914
|
+
if (task === undefined) {
|
|
915
|
+
throw new TodoStoreError('TASK_NOT_FOUND', 'task_not_found', undefined, {
|
|
916
|
+
requested_task_id: taskId,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
if (task.status !== 'in-progress') {
|
|
920
|
+
throw new TodoStoreError('TASK_START_BINDING_UNSUPPORTED', 'rebind_requires_in_progress', undefined, {
|
|
921
|
+
task_id: taskId, status: task.status,
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
let noteContext;
|
|
925
|
+
try {
|
|
926
|
+
({ context: noteContext } = await readTodoNoteContext({ repoRoot, store, planKey, taskId }));
|
|
927
|
+
} catch (error) {
|
|
928
|
+
if (!['NOTE_LOG_CORRUPT', 'NOTE_PROJECTION_INVALID'].includes(error?.code)) throw error;
|
|
929
|
+
noteContext = {
|
|
930
|
+
schema: 'lattice.todo_note_context.unreadable.v1',
|
|
931
|
+
project_id: member.plan.project_id,
|
|
932
|
+
plan_key: planKey,
|
|
933
|
+
task_id: taskId,
|
|
934
|
+
code: error.code,
|
|
935
|
+
reason: error.detail?.reason ?? 'note_context_unreadable',
|
|
936
|
+
next_action: `lattice todo note list --plan ${planKey} --json`,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
return mutate({
|
|
940
|
+
repoRoot, env, planKey, taskId, kind: 'start',
|
|
941
|
+
payload: { start_mode: 'rebind', reason }, evidenceRef: null, noteContext,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
904
945
|
async function retractStart({ repoRoot, env, planKey, taskId, reason }) {
|
|
905
946
|
const actor = mutationActor(env);
|
|
906
947
|
const store = await readTodoStore({ repoRoot });
|
|
@@ -1052,7 +1093,8 @@ async function dependencyConnect({
|
|
|
1052
1093
|
async function dependencyDisconnect({
|
|
1053
1094
|
repoRoot, env, fromPlanKey, fromTaskId, toPlanKey, toTaskId, reason,
|
|
1054
1095
|
}) {
|
|
1055
|
-
|
|
1096
|
+
// 修理扉: 越境依存の固定digestが陳腐化した store でも disconnect だけは通す(todo-store.mjs 参照)
|
|
1097
|
+
const store = await readTodoStore({ repoRoot, tolerateStaleBindings: true });
|
|
1056
1098
|
const live = projectTodoCrossPlanDependencies(store.members).filter((dependency) => (
|
|
1057
1099
|
dependency.from.plan_key === fromPlanKey && dependency.from.task_id === fromTaskId
|
|
1058
1100
|
&& dependency.to.plan_key === toPlanKey && dependency.to.task_id === toTaskId
|
|
@@ -1068,6 +1110,7 @@ async function dependencyDisconnect({
|
|
|
1068
1110
|
.some((event) => event.event_digest === target.event_digest));
|
|
1069
1111
|
const { event } = await appendTodoEvent({
|
|
1070
1112
|
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), planKey: owner.plan.plan_key,
|
|
1113
|
+
tolerateStaleBindings: true,
|
|
1071
1114
|
event: {
|
|
1072
1115
|
kind: 'cross_plan_dependency_removed', actor: mutationActor(env),
|
|
1073
1116
|
payload: { target_event_digest: target.event_digest, reason },
|
|
@@ -3968,11 +4011,18 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3968
4011
|
action = (repoRoot) => phaseBaseline({ repoRoot, env, reason: argv[3], exceptPlanKeys });
|
|
3969
4012
|
} else if (argv[0] === 'start') {
|
|
3970
4013
|
const flags = matchFlagCommand(argv, ['start'], {
|
|
3971
|
-
known: ['plan', 'task', 'parallel-frontier', 'override-reason', 'serial-confirmed'],
|
|
4014
|
+
known: ['plan', 'task', 'parallel-frontier', 'override-reason', 'serial-confirmed', 'rebind', 'reason'],
|
|
3972
4015
|
required: ['plan', 'task'],
|
|
3973
|
-
booleans: ['parallel-frontier', 'serial-confirmed'],
|
|
4016
|
+
booleans: ['parallel-frontier', 'serial-confirmed', 'rebind'],
|
|
3974
4017
|
});
|
|
3975
|
-
if (flags !== null &&
|
|
4018
|
+
if (flags !== null && flags.rebind === true) {
|
|
4019
|
+
if (isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)
|
|
4020
|
+
&& typeof flags.reason === 'string' && flags.reason.length > 0) {
|
|
4021
|
+
action = (repoRoot) => rebindStart({
|
|
4022
|
+
repoRoot, env, planKey: flags.plan, taskId: flags.task, reason: flags.reason,
|
|
4023
|
+
});
|
|
4024
|
+
}
|
|
4025
|
+
} else if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)
|
|
3976
4026
|
&& (flags['override-reason'] === undefined || (typeof flags['override-reason'] === 'string'
|
|
3977
4027
|
&& flags['override-reason'].length > 0))) {
|
|
3978
4028
|
action = (repoRoot) => startTask({
|
package/src/todo-contracts.mjs
CHANGED
|
@@ -532,6 +532,12 @@ function validPayload(event) {
|
|
|
532
532
|
&& isTodoDigest(payload.target_event_digest)
|
|
533
533
|
&& nullableText(payload.reason) && payload.reason !== null;
|
|
534
534
|
}
|
|
535
|
+
// rebind: plan改訂でcarryされたin-progress ToDoへ、現plan_versionの明示的start束縛を積み直す
|
|
536
|
+
// (runtime intakeはliteral startだけを束縛に使い、carryを推定しない。ADR 0191)
|
|
537
|
+
if (event.kind === 'start' && payload?.start_mode === 'rebind') {
|
|
538
|
+
return exactRecord(payload, ['start_mode', 'reason'])
|
|
539
|
+
&& nullableText(payload.reason) && payload.reason !== null;
|
|
540
|
+
}
|
|
535
541
|
if (event.kind === 'start') return exactRecord(payload, ['override_reason']) && nullableText(payload.override_reason);
|
|
536
542
|
if (event.kind === 'start_retracted') return exactRecord(payload, ['reason', 'target_start_digest'])
|
|
537
543
|
&& nullableText(payload.reason) && payload.reason !== null
|
package/src/todo-store.mjs
CHANGED
|
@@ -678,6 +678,17 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
678
678
|
if (state === undefined) fail('STORE_INCONSISTENT', 'event_task_missing');
|
|
679
679
|
const dependenciesDone = localPredecessors(plan, event.task_id).every((id) => states.get(id)?.status === 'done');
|
|
680
680
|
if (event.kind === 'start') {
|
|
681
|
+
if (event.payload.start_mode === 'rebind') {
|
|
682
|
+
// 改訂でcarryされたin-progressの再束縛。状態・開始時刻は動かさず、start束縛だけを
|
|
683
|
+
// 現actor・現plan_versionのeventへ更新する(ADR 0191)
|
|
684
|
+
if (state.status !== 'in-progress') {
|
|
685
|
+
fail('STORE_INCONSISTENT', 'invalid_rebind_start_transition');
|
|
686
|
+
}
|
|
687
|
+
authoredStartBindings.set(event.task_id, {
|
|
688
|
+
actor: structuredClone(event.actor), event_digest: event.event_digest,
|
|
689
|
+
});
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
681
692
|
if (event.payload.start_mode === 'historical_import') {
|
|
682
693
|
if (!importedGenesis || state.status !== 'pending') {
|
|
683
694
|
fail('STORE_INCONSISTENT', 'invalid_historical_import_start_transition');
|
|
@@ -879,7 +890,13 @@ function snapshotFor(plan, events, tasks) {
|
|
|
879
890
|
return snapshot;
|
|
880
891
|
}
|
|
881
892
|
|
|
882
|
-
function validateMergedGraph(members, crossPlanDependencies = []) {
|
|
893
|
+
function validateMergedGraph(members, crossPlanDependencies = [], options = {}) {
|
|
894
|
+
// tolerateStaleBindings は dependency disconnect 専用の修理扉。plan revise が越境依存の
|
|
895
|
+
// 固定digestを陳腐化させると、通常loadは binding_stale で fail closed するが、その修理
|
|
896
|
+
// コマンド自身も load を要するため、扉が無いと修理経路が存在しない(実被弾 2026-08-29:
|
|
897
|
+
// evidence-2 改訂で store 全体が読めなくなり、disconnect も同エラーで詰んだ)。
|
|
898
|
+
// 許容するのは digest 不一致だけで、dangling / missing は修理扉でも fail する。
|
|
899
|
+
const tolerateStale = options.tolerateStaleBindings === true;
|
|
883
900
|
const tasks = new Map();
|
|
884
901
|
for (const member of members) for (const task of member.plan.tasks) {
|
|
885
902
|
tasks.set(`${member.plan.project_id}\0${member.plan.plan_key}\0${task.task_id}`, member.plan.topology_digest);
|
|
@@ -900,7 +917,7 @@ function validateMergedGraph(members, crossPlanDependencies = []) {
|
|
|
900
917
|
if (topology === undefined) fail('STORE_INCONSISTENT', 'dangling_dependency');
|
|
901
918
|
if ((ref.project_id !== ownerPlan.project_id || ref.plan_key !== ownerPlan.plan_key)
|
|
902
919
|
&& ref.expected_topology_digest === undefined) fail('STORE_INCONSISTENT', 'cross_plan_binding_missing');
|
|
903
|
-
if (ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
|
|
920
|
+
if (!tolerateStale && ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
|
|
904
921
|
fail('STORE_INCONSISTENT', 'binding_stale');
|
|
905
922
|
}
|
|
906
923
|
return key;
|
|
@@ -911,7 +928,7 @@ function validateMergedGraph(members, crossPlanDependencies = []) {
|
|
|
911
928
|
if (topology === undefined) fail('STORE_INCONSISTENT', 'dangling_phase_dependency');
|
|
912
929
|
if ((ref.project_id !== ownerPlan.project_id || ref.plan_key !== ownerPlan.plan_key)
|
|
913
930
|
&& ref.expected_topology_digest === undefined) fail('STORE_INCONSISTENT', 'cross_plan_binding_missing');
|
|
914
|
-
if (ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
|
|
931
|
+
if (!tolerateStale && ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
|
|
915
932
|
fail('STORE_INCONSISTENT', 'binding_stale');
|
|
916
933
|
}
|
|
917
934
|
return key;
|
|
@@ -1119,6 +1136,7 @@ function validateMergedTransition(store, member, event) {
|
|
|
1119
1136
|
const phaseAcceptReady = mergedPhaseAcceptReady(store, targetKey);
|
|
1120
1137
|
const dependenciesDone = taskDependenciesDone && phaseAcceptReady;
|
|
1121
1138
|
if (event.kind === 'start' && event.payload.start_mode !== 'historical_import'
|
|
1139
|
+
&& event.payload.start_mode !== 'rebind'
|
|
1122
1140
|
&& (!phaseAcceptReady || (!taskDependenciesDone && event.payload.override_reason === null))) {
|
|
1123
1141
|
fail('STORE_INCONSISTENT', 'invalid_start_transition');
|
|
1124
1142
|
}
|
|
@@ -1629,7 +1647,9 @@ export async function readTodoStore(options = {}) {
|
|
|
1629
1647
|
snapshot: snapshotStale ? expectedSnapshot : snapshot,
|
|
1630
1648
|
tasks, phases, coordination, snapshot_stale: snapshotStale });
|
|
1631
1649
|
}
|
|
1632
|
-
validateMergedGraph(loaded, projectTodoCrossPlanDependencies(loaded)
|
|
1650
|
+
validateMergedGraph(loaded, projectTodoCrossPlanDependencies(loaded), {
|
|
1651
|
+
tolerateStaleBindings: options.tolerateStaleBindings === true,
|
|
1652
|
+
});
|
|
1633
1653
|
return {
|
|
1634
1654
|
schema: 'lattice.todo_store_read.v1', project_id: manifest.project_id, manifest,
|
|
1635
1655
|
members: loaded, snapshot_stale: loaded.some((member) => member.snapshot_stale),
|
|
@@ -2036,6 +2056,10 @@ export async function appendTodoEvent(options = {}) {
|
|
|
2036
2056
|
const store = await readTodoStore({
|
|
2037
2057
|
repoRoot, forWrite: true, now: options.now, evidenceRepair,
|
|
2038
2058
|
evidenceRepairCapability: EVIDENCE_REPAIR_CAPABILITY,
|
|
2059
|
+
// 修理扉は除去event(tombstone)の書込みだけに開く。それ以外の書込みは従来どおり
|
|
2060
|
+
// binding_stale で fail closed する(validateMergedGraph の注記を参照)
|
|
2061
|
+
tolerateStaleBindings: options.tolerateStaleBindings === true
|
|
2062
|
+
&& options.event.kind === 'cross_plan_dependency_removed',
|
|
2039
2063
|
});
|
|
2040
2064
|
const member = store.members.find(({ descriptor }) => descriptor.plan_key === options.planKey);
|
|
2041
2065
|
if (!member) fail('STORE_INCONSISTENT', 'plan_not_active');
|