@quolu/lattice 0.60.8 → 0.61.2
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/README.ja.md +11 -25
- package/README.md +9 -0
- package/bin/lattice.mjs +0 -1
- package/package.json +1 -1
- package/src/cli-help.mjs +5 -4
- package/src/project-cli.mjs +2 -5
- package/src/runtime-pull-intake.mjs +32 -1
- package/src/runtime-windows-process.mjs +54 -0
- package/src/todo-cli.mjs +12 -105
- package/src/todo-dispatch-shape.mjs +9 -39
package/README.ja.md
CHANGED
|
@@ -218,39 +218,25 @@ cross-plan topologyを同時に切り替える場合は
|
|
|
218
218
|
Phase付きv5 planでは、通常ToDoの開始順はToDo DAGだけで決まり、Phase前後関係は重監査の順序だけを
|
|
219
219
|
制御します。特定ToDoがPhase受理を本当に必要とする場合だけ`phase_accept_dependencies`で明示します。
|
|
220
220
|
`lattice todo status --json`の`dispatch_frontier`はready全件を同時dispatchする既定を示します。
|
|
221
|
-
ready
|
|
222
|
-
`--
|
|
221
|
+
readyが複数でも、最初の`todo start`はflagなしで通ります。並列で始めるなら
|
|
222
|
+
`--parallel-frontier`を付けられます。subsetだけを直列着手する場合は
|
|
223
|
+
`--override-reason <reason>`で理由を残せます。どちらも門ではなく記録です。
|
|
223
224
|
|
|
224
225
|
```bash
|
|
226
|
+
lattice todo start --plan <key> --task <id>
|
|
225
227
|
lattice todo start --plan <key> --task <id> --parallel-frontier
|
|
226
|
-
lattice todo start --plan <key> --task <id> --override-reason <reason>
|
|
228
|
+
lattice todo start --plan <key> --task <id> --override-reason <reason>
|
|
227
229
|
```
|
|
228
230
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
読み飛ばされるため、再考をコマンドの往復で強制する設計です。**足止めは一度だけで、
|
|
232
|
-
再実行すれば直列で進みます。**
|
|
231
|
+
`--serial-confirmed`と`--serialization-reviewed`は古い手順の互換として受理しますが、
|
|
232
|
+
再実行の条件ではありません。直列理由の文言を審査して拒否することもありません。
|
|
233
233
|
|
|
234
|
-
|
|
235
|
-
のようにworker数・セッション構成・作業者の都合を述べただけの場合——は
|
|
236
|
-
`PARALLEL_DISPATCH_INVALID`で拒否し、`--serial-confirmed`を付けても通しません。
|
|
237
|
-
実行主体が1つしか無いことは並列にできない理由ではない(必要ならworkerを増やす)ためです。
|
|
238
|
-
「両taskが同一fileへ書き込む」のような干渉を書けば、再確認を経て通ります。
|
|
239
|
-
|
|
240
|
-
**同じ検査を計画時点にも掛けます。** 着手時だけを締めても、planそのものが直列に組まれていれば
|
|
241
|
-
並列は生まれません。`lattice plan create`と`lattice todo migrate`は依存グラフから
|
|
234
|
+
`lattice plan create`と`lattice todo migrate`は依存グラフから
|
|
242
235
|
`dispatch_shape`(`task_count`/`critical_path_length`/`max_frontier_width`/
|
|
243
|
-
`serialization_ratio
|
|
244
|
-
再考した上でなお直列でよいなら`--serialization-reviewed`を付けて再実行します
|
|
245
|
-
(6 task未満のplanは対象外)。判定はstore書込みの前に行うので、拒否された時にstoreへは
|
|
246
|
-
何も書かれません。
|
|
247
|
-
|
|
248
|
-
```bash
|
|
249
|
-
lattice todo migrate --input <extraction.json> --serialization-reviewed
|
|
250
|
-
```
|
|
236
|
+
`serialization_ratio`)を計算して結果へ載せます。直列度が閾値を超えても作成は通ります。
|
|
251
237
|
|
|
252
|
-
`--parallel-frontier`はhostへ並列dispatch
|
|
253
|
-
起動するものではなく、実際のdispatchはhost
|
|
238
|
+
`--parallel-frontier`はhostへ並列dispatch方針を伝える記録です。Lattice自身がAI hostのagentを
|
|
239
|
+
起動するものではなく、実際のdispatchはhostが行います。ready全件が着手されたかは
|
|
254
240
|
`active_set`と`next_ready`で観測できます。
|
|
255
241
|
ToDo完了は軽量確認までで、所属ToDoが全てdoneになったPhaseは`gate_ready`となり、`todo phase review`後に
|
|
256
242
|
required evidenceを束縛した`todo phase accept`で重監査の判断を記録します。監査回数やPhase数を自動追加する
|
package/README.md
CHANGED
|
@@ -189,6 +189,15 @@ lattice todo seam-proposal apply --plan <key> # isolated worktre
|
|
|
189
189
|
lattice todo seam-proposal land --plan <key> --names names.json
|
|
190
190
|
```
|
|
191
191
|
|
|
192
|
+
`lattice todo status --json` exposes `dispatch_frontier`: every ready task is the default
|
|
193
|
+
parallel set. Starting one of them does not require `--parallel-frontier` or
|
|
194
|
+
`--override-reason`. Those flags record intent; they are not gates.
|
|
195
|
+
`--serial-confirmed` and `--serialization-reviewed` are accepted only for compatibility.
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
lattice todo start --plan <key> --task <id>
|
|
199
|
+
```
|
|
200
|
+
|
|
192
201
|
Full CLI surface: `lattice --help`, then
|
|
193
202
|
`lattice <plan|run|event|todo|sensor|factory-diagnostics|runtime-errors|bridge|hooks> --help`.
|
|
194
203
|
|
package/bin/lattice.mjs
CHANGED
|
@@ -37,7 +37,6 @@ if (help !== null) {
|
|
|
37
37
|
try {
|
|
38
38
|
process.exitCode = await runPlanCreate({
|
|
39
39
|
cwd: process.cwd(), inputRef: args[3], stdout: process.stdout,
|
|
40
|
-
serializationReviewed: args.length === 5,
|
|
41
40
|
});
|
|
42
41
|
} catch (error) {
|
|
43
42
|
process.exitCode = projectCliFailure(process.stderr, error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.2",
|
|
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
|
@@ -25,8 +25,7 @@ const NAMESPACE_HELP = Object.freeze({
|
|
|
25
25
|
|
|
26
26
|
Commands:
|
|
27
27
|
create --input <file> [--serialization-reviewed]
|
|
28
|
-
#
|
|
29
|
-
# 再考した上でなお直列でよいなら --serialization-reviewed を付けて再実行する
|
|
28
|
+
# 結果へdispatch_shapeを載せる。--serialization-reviewedは互換のため受理するだけで門ではない
|
|
30
29
|
create --schema --json # 既定は最新版(v4)のJSON Schemaを返す
|
|
31
30
|
create --schema-version <1|2|3|4> --json
|
|
32
31
|
show <plan_key> --json # task・依存・phase・状態をplan本体から1コマンドで投影する
|
|
@@ -94,9 +93,10 @@ Write commands:
|
|
|
94
93
|
migrate --input <extraction.json> [--serialization-reviewed] [--json]
|
|
95
94
|
migrate --input <extraction.json> --dry-run --json [--serialization-reviewed]
|
|
96
95
|
# 既存storeへplanを追加する(plan createは空store初期化専用)。
|
|
97
|
-
#
|
|
96
|
+
# 結果へdispatch_shapeを載せる。--serialization-reviewedは互換のため受理するだけで門ではない
|
|
98
97
|
start --plan <key> --task <id> [--parallel-frontier|--override-reason <text> [--serial-confirmed]]
|
|
99
|
-
#
|
|
98
|
+
# readyならflagなしで着手する。--parallel-frontierと--override-reasonは方針・理由の記録。
|
|
99
|
+
# --serial-confirmedは互換のため受理する。いずれも門ではない
|
|
100
100
|
block --plan <key> --task <id> --reason <text>
|
|
101
101
|
unblock --plan <key> --task <id>
|
|
102
102
|
done --plan <key> --task <id> --evidence <file> # 構造対象taskはfresh realizationを要求する
|
|
@@ -152,6 +152,7 @@ ${TODO_INDEPENDENCE_WORKFLOW.join('\n')}
|
|
|
152
152
|
|
|
153
153
|
判定していない工程は「競合が無い」ではなく「未検査」として扱われ、
|
|
154
154
|
todo startのadvisoryとtodo independenceの投影が、その状況と次の一歩を返す。
|
|
155
|
+
並列既定はstatusのdispatch_frontierとnext_actionが案内する。着手・作成を拒まない。
|
|
155
156
|
`,
|
|
156
157
|
sensor: `Usage: lattice sensor <init|sync> [path] --json
|
|
157
158
|
lattice sensor diff <rootA> <rootB> [options] --json
|
package/src/project-cli.mjs
CHANGED
|
@@ -37,7 +37,6 @@ import {
|
|
|
37
37
|
TodoStoreError,
|
|
38
38
|
} from './todo-store.mjs';
|
|
39
39
|
import {
|
|
40
|
-
assertTodoDispatchShapeReviewed,
|
|
41
40
|
computeTodoDispatchShapeForPlan,
|
|
42
41
|
} from './todo-dispatch-shape.mjs';
|
|
43
42
|
|
|
@@ -485,7 +484,7 @@ function validateCreateInput(value) {
|
|
|
485
484
|
} catch { return false; }
|
|
486
485
|
}
|
|
487
486
|
|
|
488
|
-
export async function runPlanCreate({ cwd, inputRef, stdout
|
|
487
|
+
export async function runPlanCreate({ cwd, inputRef, stdout }) {
|
|
489
488
|
const repoRoot = resolveRepoRoot(cwd);
|
|
490
489
|
if (repoRoot === null) throw new TodoStoreError('REPO_UNRESOLVED', 'git_toplevel_unresolved');
|
|
491
490
|
const input = await readCanonicalInput(repoRoot, inputRef);
|
|
@@ -526,8 +525,7 @@ export async function runPlanCreate({ cwd, inputRef, stdout, serializationReview
|
|
|
526
525
|
next_action: 'correct_the_reported_pointer_then_rerun_plan_create',
|
|
527
526
|
});
|
|
528
527
|
}
|
|
529
|
-
// dispatch_shape
|
|
530
|
-
// 再考後の再実行がplan_key_already_existsで詰まらない)。
|
|
528
|
+
// dispatch_shapeは結果へ載せる観測であり、直列度では初期化を拒まない(ADR 0180)。
|
|
531
529
|
const dispatchShape = computeTodoDispatchShapeForPlan({
|
|
532
530
|
projectId: input.project_id,
|
|
533
531
|
planKey: input.plan_key,
|
|
@@ -535,7 +533,6 @@ export async function runPlanCreate({ cwd, inputRef, stdout, serializationReview
|
|
|
535
533
|
hardDependencies: input.hard_dependencies,
|
|
536
534
|
joins: input.joins,
|
|
537
535
|
});
|
|
538
|
-
assertTodoDispatchShapeReviewed({ shape: dispatchShape, reviewed: serializationReviewed });
|
|
539
536
|
const store = await initializeAuthoredTodoStore({
|
|
540
537
|
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }),
|
|
541
538
|
projectId: input.project_id, repositories: [{ repo_id: 'self', path: '.' }],
|
|
@@ -13,6 +13,7 @@ import { classifyObservedDiff } from './runtime-decision-verifier.mjs';
|
|
|
13
13
|
import { captureWorktreeDiff, detectCheckpointFindings } from './runtime-diff-observer.mjs';
|
|
14
14
|
import { acquireRuntimeLifecycleLock } from './runtime-lifecycle-lock.mjs';
|
|
15
15
|
import { observeManagedProcessStartIdentity } from './runtime-managed-supervisor.mjs';
|
|
16
|
+
import { deliverWorkerSignal, observeWindowsWorkerProcess } from './runtime-windows-process.mjs';
|
|
16
17
|
import { ensureScriptedWorktree } from './runtime-scripted-worktree.mjs';
|
|
17
18
|
import {
|
|
18
19
|
BOUNDARY_MANIFEST_SCHEMA, selfDigest, validateRuntimeBoundaryManifest, validateRuntimePlan,
|
|
@@ -890,6 +891,36 @@ export async function releasePullTask({ runDir, taskId, environment = process.en
|
|
|
890
891
|
|
|
891
892
|
async function observeProcessBinding(pid) {
|
|
892
893
|
if (!Number.isSafeInteger(pid) || pid < 1) fail('INVALID_WORKER_PID', 'pidが正整数でない');
|
|
894
|
+
if (process.platform === 'win32') {
|
|
895
|
+
let observed;
|
|
896
|
+
try {
|
|
897
|
+
observed = await observeWindowsWorkerProcess(pid);
|
|
898
|
+
} catch (error) {
|
|
899
|
+
fail('WORKER_IDENTITY_MISMATCH', `Windows worker process identityを完全観測できない: ${error.message}`);
|
|
900
|
+
}
|
|
901
|
+
const processStartIdentity = {
|
|
902
|
+
schema: 'lattice.process_start_identity.v1',
|
|
903
|
+
platform: 'win32',
|
|
904
|
+
pid: observed.pid,
|
|
905
|
+
started_identity: observed.started_identity,
|
|
906
|
+
identity_digest: '',
|
|
907
|
+
};
|
|
908
|
+
processStartIdentity.identity_digest = selfDigest(processStartIdentity, 'identity_digest');
|
|
909
|
+
const expected = {
|
|
910
|
+
pid: observed.pid,
|
|
911
|
+
process_group_id: observed.process_group_id,
|
|
912
|
+
process_start_identity: processStartIdentity,
|
|
913
|
+
};
|
|
914
|
+
if (!validateExpectedWorkerProcess(expected) || observed.argv.length === 0) {
|
|
915
|
+
fail('WORKER_IDENTITY_MISMATCH', 'worker process identityを完全観測できない');
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
pid: observed.pid,
|
|
919
|
+
process_group_id: observed.process_group_id,
|
|
920
|
+
process_start_identity: processStartIdentity,
|
|
921
|
+
argv_digest: createHash('sha256').update(observed.argv, 'utf8').digest('hex'),
|
|
922
|
+
};
|
|
923
|
+
}
|
|
893
924
|
const processStartIdentity = await observeManagedProcessStartIdentity(pid);
|
|
894
925
|
const { stdout: pgidOut } = await execFileAsync('/bin/ps', ['-o', 'pgid=', '-p', String(pid)], { encoding: 'utf8' });
|
|
895
926
|
const { stdout: argvOut } = await execFileAsync('/bin/ps', ['-o', 'command=', '-p', String(pid)], { encoding: 'utf8' });
|
|
@@ -917,7 +948,7 @@ async function signalAttachedWorker(intake, signal) {
|
|
|
917
948
|
|| observed.process_group_id !== intake.worker.process_group_id) {
|
|
918
949
|
fail('WORKER_IDENTITY_MISMATCH', 'signal前のlstart/argv/pgidがattach bindingと一致しない');
|
|
919
950
|
}
|
|
920
|
-
try {
|
|
951
|
+
try { deliverWorkerSignal(intake.worker.pid, signal); }
|
|
921
952
|
catch { fail('WORKER_SIGNAL_FAILED', `workerへ${signal}を送れない`); }
|
|
922
953
|
return true;
|
|
923
954
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
export function parseWindowsCreationDate(value) {
|
|
7
|
+
if (value instanceof Date) {
|
|
8
|
+
if (Number.isNaN(value.getTime())) throw new Error('Windows CreationDateを解釈できない');
|
|
9
|
+
return value.toISOString();
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
12
|
+
const parsed = new Date(value);
|
|
13
|
+
if (Number.isNaN(parsed.getTime())) throw new Error('Windows CreationDateを解釈できない');
|
|
14
|
+
return parsed.toISOString();
|
|
15
|
+
}
|
|
16
|
+
const text = String(value ?? '').trim();
|
|
17
|
+
const microsoft = /^\/Date\((-?\d+)(?:[+-]\d+)?\)\/$/u.exec(text);
|
|
18
|
+
if (microsoft) return new Date(Number(microsoft[1])).toISOString();
|
|
19
|
+
const parsed = new Date(text);
|
|
20
|
+
if (Number.isNaN(parsed.getTime())) throw new Error(`Windows CreationDateを解釈できない: ${text}`);
|
|
21
|
+
return parsed.toISOString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function deliverWorkerSignal(pid, signal) {
|
|
25
|
+
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('pidが正整数でない');
|
|
26
|
+
const jobControl = signal === 'SIGSTOP' || signal === 'SIGCONT';
|
|
27
|
+
if (process.platform === 'win32' && jobControl) {
|
|
28
|
+
return { delivered: false, recorded: true };
|
|
29
|
+
}
|
|
30
|
+
process.kill(pid, signal);
|
|
31
|
+
return { delivered: true, recorded: true };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function observeWindowsWorkerProcess(pid) {
|
|
35
|
+
if (!Number.isSafeInteger(pid) || pid < 1) {
|
|
36
|
+
throw new Error('pidが正整数でない');
|
|
37
|
+
}
|
|
38
|
+
const filter = JSON.stringify(`ProcessId=${pid}`);
|
|
39
|
+
const script = `Get-CimInstance Win32_Process -Filter ${filter} | Select-Object ProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress`;
|
|
40
|
+
const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', script], {
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
});
|
|
43
|
+
const row = JSON.parse(stdout.trim() || 'null');
|
|
44
|
+
if (!row || Number(row.ProcessId) !== pid) throw new Error(`Win32_Processを観測できない: ${pid}`);
|
|
45
|
+
const argv = String(row.CommandLine || '').trim();
|
|
46
|
+
const startedIdentity = parseWindowsCreationDate(row.CreationDate);
|
|
47
|
+
if (!argv || !startedIdentity) throw new Error('Windows worker process identityを完全観測できない');
|
|
48
|
+
return {
|
|
49
|
+
pid,
|
|
50
|
+
process_group_id: pid,
|
|
51
|
+
started_identity: startedIdentity,
|
|
52
|
+
argv,
|
|
53
|
+
};
|
|
54
|
+
}
|
package/src/todo-cli.mjs
CHANGED
|
@@ -104,7 +104,6 @@ import {
|
|
|
104
104
|
validateTodoExtraction,
|
|
105
105
|
} from './todo-migration.mjs';
|
|
106
106
|
import {
|
|
107
|
-
assertTodoDispatchShapeReviewed,
|
|
108
107
|
computeTodoDispatchShapeForPlan,
|
|
109
108
|
} from './todo-dispatch-shape.mjs';
|
|
110
109
|
import {
|
|
@@ -842,31 +841,8 @@ async function startAdvisory({ repoRoot, store, projection, planKey, taskId }) {
|
|
|
842
841
|
};
|
|
843
842
|
}
|
|
844
843
|
|
|
845
|
-
// 直列化の理由として認めない定型句。
|
|
846
|
-
// worker数・セッション構成・作業者の都合は「並列にできない根拠」ではない。
|
|
847
|
-
// 根拠になるのは実際の干渉だけ(同一fileへの書込衝突・外部資源の排他・順序依存)。
|
|
848
|
-
// 単一プロセスのagentが「自分は1人だから」と直列へ逃げる事例が実運用で出たため、
|
|
849
|
-
// frontierの既定(all_ready_parallel_by_default)を宣言だけでなく機構で守る。
|
|
850
|
-
const SERIAL_NON_REASONS = [
|
|
851
|
-
/単一(?:の)?(?:セッション|エージェント|worker|ワーカー|プロセス|スレッド)/u,
|
|
852
|
-
/(?:逐次|順次|直列|シリアル)(?:実行|処理|化|に|で)/u,
|
|
853
|
-
/(?:一人|1人|ひとり|1名|単独)(?:で|の|しか)/u,
|
|
854
|
-
/(?:サブ)?エージェント(?:が|は)?(?:居ない|いない|使わない|使えない)/u,
|
|
855
|
-
/single[-\s]?(?:session|agent|worker|process|thread)/iu,
|
|
856
|
-
/\b(?:sequential|serial)(?:ly)?\s*(?:execution|processing|run|dispatch)?\b/iu,
|
|
857
|
-
/one[-\s]at[-\s]a[-\s]time|\bsolo\b|\bby myself\b/iu,
|
|
858
|
-
];
|
|
859
|
-
|
|
860
|
-
/**
|
|
861
|
-
* 直列化理由が「実際の干渉」を述べているかを検査する。
|
|
862
|
-
* worker数・セッション構成を述べただけの理由は根拠にならないので拒否する。
|
|
863
|
-
*/
|
|
864
|
-
function serialReasonNonInterference(reason) {
|
|
865
|
-
return SERIAL_NON_REASONS.find((pattern) => pattern.test(reason)) ?? null;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
844
|
async function startTask({
|
|
869
|
-
repoRoot, env, planKey, taskId, overrideReason, parallelFrontier,
|
|
845
|
+
repoRoot, env, planKey, taskId, overrideReason, parallelFrontier,
|
|
870
846
|
}) {
|
|
871
847
|
const store = await readTodoStore({ repoRoot });
|
|
872
848
|
// startはready判定にしかprojectionを使わず、resultを出力しない。
|
|
@@ -875,63 +851,11 @@ async function startTask({
|
|
|
875
851
|
task.plan_key === planKey && task.task_id.toLowerCase() === taskId.toLowerCase()
|
|
876
852
|
));
|
|
877
853
|
const targetReady = readyTask !== undefined;
|
|
854
|
+
// `--parallel-frontier`は方針の記録口で、対象がreadyでないときにだけ用法誤り。
|
|
855
|
+
// 複数readyのplain startや直列理由の文言審査で着手を止めない(ADR 0180)。
|
|
878
856
|
if (parallelFrontier && !targetReady) {
|
|
879
857
|
throw new TodoStoreError('PARALLEL_DISPATCH_INVALID', 'parallel_frontier_not_applicable');
|
|
880
858
|
}
|
|
881
|
-
const frontierContested = targetReady && projection.active_set.length === 0
|
|
882
|
-
&& projection.next_ready.length > 1;
|
|
883
|
-
if (frontierContested && overrideReason === null && !parallelFrontier) {
|
|
884
|
-
throw new TodoStoreError('PARALLEL_DISPATCH_REQUIRED', 'parallel_frontier_requires_declaration',
|
|
885
|
-
undefined, {
|
|
886
|
-
ready_count: projection.next_ready.length,
|
|
887
|
-
ready_task_ids: projection.next_ready.map((task) => task.task_id),
|
|
888
|
-
frontier_digest: projection.dispatch_frontier.frontier_digest,
|
|
889
|
-
parallel_start_flag: projection.dispatch_frontier.parallel_start_flag,
|
|
890
|
-
serial_reason_flag: '--override-reason',
|
|
891
|
-
default_policy: projection.dispatch_frontier.policy,
|
|
892
|
-
guidance: '既定は全ready分の同時dispatch。並列で始めるなら --parallel-frontier を使う。'
|
|
893
|
-
+ '--override-reason は「なぜ並列にできないか」を書く欄であり、'
|
|
894
|
-
+ 'worker数・セッション構成・作業者の都合は根拠にならない'
|
|
895
|
-
+ '(同一fileへの書込衝突・外部資源の排他・順序依存だけが根拠になる)。',
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
if (frontierContested && overrideReason !== null) {
|
|
899
|
-
if (serialReasonNonInterference(overrideReason) !== null) {
|
|
900
|
-
throw new TodoStoreError('PARALLEL_DISPATCH_INVALID', 'serial_reason_is_not_an_interference',
|
|
901
|
-
undefined, {
|
|
902
|
-
ready_count: projection.next_ready.length,
|
|
903
|
-
ready_task_ids: projection.next_ready.map((task) => task.task_id),
|
|
904
|
-
rejected_reason: overrideReason,
|
|
905
|
-
default_policy: projection.dispatch_frontier.policy,
|
|
906
|
-
parallel_start_flag: projection.dispatch_frontier.parallel_start_flag,
|
|
907
|
-
guidance: 'worker数・セッション構成・作業者の都合は直列化の根拠にならない。'
|
|
908
|
-
+ 'readyが複数あるなら既定は同時dispatchであり、実行主体が1つしか無いことは'
|
|
909
|
-
+ '並列にできない理由ではない(必要ならworkerを増やす)。'
|
|
910
|
-
+ '直列にするなら、並列で走らせたときに実際に起きる干渉'
|
|
911
|
-
+ '(同一fileへの書込衝突・外部資源の排他・順序依存)を書く。',
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
// 直列の申告は一度突き返して並列を再検討させる。
|
|
915
|
-
// 規則を書くだけでは読み飛ばされるため、再考をコマンドの往復で強制する。
|
|
916
|
-
if (!serialConfirmed) {
|
|
917
|
-
throw new TodoStoreError('PARALLEL_DISPATCH_RECONSIDER', 'consider_parallel_before_serial',
|
|
918
|
-
undefined, {
|
|
919
|
-
ready_count: projection.next_ready.length,
|
|
920
|
-
ready_task_ids: projection.next_ready.map((task) => task.task_id),
|
|
921
|
-
declared_reason: overrideReason,
|
|
922
|
-
default_policy: projection.dispatch_frontier.policy,
|
|
923
|
-
parallel_start_flag: projection.dispatch_frontier.parallel_start_flag,
|
|
924
|
-
serial_confirm_flag: '--serial-confirmed',
|
|
925
|
-
guidance: `並列を検討しなさい。ready ${projection.next_ready.length} 件は同時に着手できる`
|
|
926
|
-
+ '前提で並んでおり、既定は全件同時dispatchである。'
|
|
927
|
-
+ `まず ${projection.dispatch_frontier.parallel_start_flag} で全readyを起こし、`
|
|
928
|
-
+ 'それぞれ別のworkerへ渡すことを検討する'
|
|
929
|
-
+ '(実行主体が足りないなら増やす。増やせないことは並列にできない理由ではない)。'
|
|
930
|
-
+ '検討した上でなお直列にするなら、同じ --override-reason に'
|
|
931
|
-
+ ' --serial-confirmed を付けて再実行する。',
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
859
|
const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
|
|
936
860
|
if (member === undefined) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active');
|
|
937
861
|
const taskMatches = member.plan.tasks.filter(({ task_id: candidate }) => (
|
|
@@ -1244,13 +1168,11 @@ async function phaseBaseline({ repoRoot, env, reason, exceptPlanKeys }) {
|
|
|
1244
1168
|
return result;
|
|
1245
1169
|
}
|
|
1246
1170
|
|
|
1247
|
-
async function migrate({ repoRoot, inputRef
|
|
1171
|
+
async function migrate({ repoRoot, inputRef }) {
|
|
1248
1172
|
const extraction = await readMigrationInput(repoRoot, inputRef);
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
// 同名gateをここでも先に通しておくことで、既存のエラー優先順位
|
|
1253
|
-
// (unresolved・空集合を直列度より先に報告する)を変えない。
|
|
1173
|
+
// unresolved/空集合の早期gateは、compileTodoExtraction内部の同名gateをここでも
|
|
1174
|
+
// 先に通しておくことで、既存のエラー優先順位を変えない。
|
|
1175
|
+
// dispatch_shapeは結果へ載せる観測であり、直列度では書込みを拒まない(ADR 0180)。
|
|
1254
1176
|
const unresolvedTaskIds = extraction.tasks
|
|
1255
1177
|
.filter(({ disposition }) => disposition === 'unknown_requires_evidence')
|
|
1256
1178
|
.map(({ task_id: taskId }) => taskId);
|
|
@@ -1272,9 +1194,6 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
|
|
|
1272
1194
|
hardDependencies: extraction.hard_dependencies,
|
|
1273
1195
|
joins: extraction.joins,
|
|
1274
1196
|
});
|
|
1275
|
-
if (dispatchShape !== null) {
|
|
1276
|
-
assertTodoDispatchShapeReviewed({ shape: dispatchShape, reviewed: serializationReviewed });
|
|
1277
|
-
}
|
|
1278
1197
|
|
|
1279
1198
|
const imported = await appendTodoExtraction({ repoRoot, extraction });
|
|
1280
1199
|
let companion = null;
|
|
@@ -1448,7 +1367,7 @@ function extractionAuthoringViolations(extraction, now = new Date()) {
|
|
|
1448
1367
|
return violations;
|
|
1449
1368
|
}
|
|
1450
1369
|
|
|
1451
|
-
async function migrateDryRun({ repoRoot, inputRef
|
|
1370
|
+
async function migrateDryRun({ repoRoot, inputRef }) {
|
|
1452
1371
|
const extraction = await readMigrationInput(repoRoot, inputRef, { requireValid: false });
|
|
1453
1372
|
const violations = [];
|
|
1454
1373
|
const tasks = Array.isArray(extraction?.tasks) ? extraction.tasks : [];
|
|
@@ -1521,13 +1440,6 @@ async function migrateDryRun({ repoRoot, inputRef, serializationReviewed = false
|
|
|
1521
1440
|
taskIds: registered.map(({ task_id: taskId }) => taskId),
|
|
1522
1441
|
hardDependencies: extraction.hard_dependencies, joins: extraction.joins,
|
|
1523
1442
|
});
|
|
1524
|
-
try {
|
|
1525
|
-
assertTodoDispatchShapeReviewed({ shape: dispatchShape, reviewed: serializationReviewed });
|
|
1526
|
-
} catch (error) {
|
|
1527
|
-
violations.push({ code: error?.detail?.reason ?? 'plan_shape_too_serial', path: '/hard_dependencies',
|
|
1528
|
-
task_ids: error?.detail?.critical_path_task_ids ?? [],
|
|
1529
|
-
next_action: 'reconsider_parallel_seams_or_pass_serialization_reviewed' });
|
|
1530
|
-
}
|
|
1531
1443
|
}
|
|
1532
1444
|
} catch (error) {
|
|
1533
1445
|
plannedPlan = null;
|
|
@@ -1586,7 +1498,7 @@ async function migrateDryRun({ repoRoot, inputRef, serializationReviewed = false
|
|
|
1586
1498
|
},
|
|
1587
1499
|
},
|
|
1588
1500
|
next_action: bounded.length === 0
|
|
1589
|
-
? `lattice todo migrate --input ${inputRef}
|
|
1501
|
+
? `lattice todo migrate --input ${inputRef}`
|
|
1590
1502
|
: 'correct_all_reported_violations_and_rerun_dry_run',
|
|
1591
1503
|
result_digest: '',
|
|
1592
1504
|
};
|
|
@@ -3916,17 +3828,13 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3916
3828
|
&& argv[1] === '--input' && isTodoRef(argv[2])
|
|
3917
3829
|
&& argv[3] === '--dry-run' && argv[4] === '--json'
|
|
3918
3830
|
&& (argv.length === 5 || argv[5] === '--serialization-reviewed')) {
|
|
3919
|
-
action = (repoRoot) => migrateDryRun({
|
|
3920
|
-
repoRoot, inputRef: argv[2], serializationReviewed: argv.length === 6,
|
|
3921
|
-
});
|
|
3831
|
+
action = (repoRoot) => migrateDryRun({ repoRoot, inputRef: argv[2] });
|
|
3922
3832
|
} else if ((argv.length === 3 || argv.length === 4 || argv.length === 5)
|
|
3923
3833
|
&& argv[0] === 'migrate' && argv[1] === '--input' && isTodoRef(argv[2])
|
|
3924
3834
|
&& (argv.length === 3
|
|
3925
3835
|
|| (argv.length === 4 && ['--json', '--serialization-reviewed'].includes(argv[3]))
|
|
3926
3836
|
|| (argv.length === 5 && argv[3] === '--serialization-reviewed' && argv[4] === '--json'))) {
|
|
3927
|
-
action = (repoRoot) => migrate({
|
|
3928
|
-
repoRoot, inputRef: argv[2], serializationReviewed: argv[3] === '--serialization-reviewed',
|
|
3929
|
-
});
|
|
3837
|
+
action = (repoRoot) => migrate({ repoRoot, inputRef: argv[2] });
|
|
3930
3838
|
} else if (argv.length === 5 && argv[0] === 'revise'
|
|
3931
3839
|
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
3932
3840
|
&& argv[3] === '--input' && isTodoRef(argv[4])) {
|
|
@@ -3986,8 +3894,7 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3986
3894
|
&& (argv.length === 7 || argv[7] === '--serial-confirmed')))) {
|
|
3987
3895
|
const overrideReason = argv.length >= 7 ? argv[6] : null;
|
|
3988
3896
|
action = (repoRoot) => startTask({ repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
3989
|
-
overrideReason, parallelFrontier: argv.length === 6
|
|
3990
|
-
serialConfirmed: argv.length === 8 });
|
|
3897
|
+
overrideReason, parallelFrontier: argv.length === 6 });
|
|
3991
3898
|
} else if (argv.length === 7 && argv[0] === 'retract'
|
|
3992
3899
|
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
3993
3900
|
&& argv[3] === '--task' && isTodoIdentifier(argv[4])
|
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import { TodoStoreError } from './todo-store.mjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* 依存グラフの「直列度」を分類する既定閾値。
|
|
5
5
|
*
|
|
6
6
|
* `serialization_ratio = critical_path_length / task_count` がこれを超えると、
|
|
7
7
|
* 依存連鎖が全task数の半分を超えて連なっている=大半のtaskが並列候補ではなく
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 水準として選んだ。
|
|
8
|
+
* 一本の鎖に押し込まれていることを意味する。結果の`dispatch_shape`がこの事実を
|
|
9
|
+
* 載せる。拒否には使わない(ADR 0180)。
|
|
10
|
+
* 実測(parent-child-repair: task 26, critical path約20, ratio≈0.77)を確実に
|
|
11
|
+
* 超える一方、緩やかな分岐を持つ通常のplanまでは拾わない水準として選んだ。
|
|
13
12
|
*/
|
|
14
13
|
export const DISPATCH_SHAPE_SERIALIZATION_THRESHOLD = 0.5;
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
18
|
-
* 3〜5 task
|
|
19
|
-
* (並列化する意味のある規模でない)機構がノイズになるだけなので、
|
|
20
|
-
* 6 task未満は常に素通りさせる。
|
|
16
|
+
* 直列度分類の対象にする最小task数。
|
|
17
|
+
* 3〜5 task程度の一直線は並列化する意味のある規模でないので、excessiveとはしない。
|
|
21
18
|
*/
|
|
22
19
|
export const DISPATCH_SHAPE_MIN_TASK_COUNT_FOR_GATE = 6;
|
|
23
20
|
|
|
21
|
+
/** 互換のため受理するだけのflag名。門には使わない(ADR 0180)。 */
|
|
24
22
|
export const DISPATCH_SHAPE_SERIALIZATION_REVIEWED_FLAG = '--serialization-reviewed';
|
|
25
23
|
|
|
26
24
|
function compareText(left, right) {
|
|
@@ -128,40 +126,12 @@ export function computeTodoDispatchShape({ taskIds, edges }) {
|
|
|
128
126
|
};
|
|
129
127
|
}
|
|
130
128
|
|
|
131
|
-
/** dispatch_shape
|
|
129
|
+
/** dispatch_shapeが、観測上「直列に寄っている」規模・度合いかを分類する。拒否には使わない。 */
|
|
132
130
|
export function isTodoDispatchShapeSerializationExcessive(shape) {
|
|
133
131
|
return shape.task_count >= DISPATCH_SHAPE_MIN_TASK_COUNT_FOR_GATE
|
|
134
132
|
&& Number(shape.serialization_ratio) > DISPATCH_SHAPE_SERIALIZATION_THRESHOLD;
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
/**
|
|
138
|
-
* dispatch_shapeが直列に寄りすぎているplanを、再考なしでは通さない。
|
|
139
|
-
*
|
|
140
|
-
* `todo start`の`PARALLEL_DISPATCH_RECONSIDER`と同じ二段階(一度突き返し、
|
|
141
|
-
* 再考を経たflagが無ければ通さない)をplan作成時点へ前倒しする。ここで
|
|
142
|
-
* 拒否する場合、呼び出し側はまだstoreへ何も書いていないこと(初期化/追加を
|
|
143
|
-
* この呼び出しより後で行うこと)。
|
|
144
|
-
*/
|
|
145
|
-
export function assertTodoDispatchShapeReviewed({ shape, reviewed }) {
|
|
146
|
-
if (!isTodoDispatchShapeSerializationExcessive(shape) || reviewed) return;
|
|
147
|
-
throw new TodoStoreError('PARALLEL_DISPATCH_RECONSIDER', 'plan_shape_too_serial', undefined, {
|
|
148
|
-
task_count: shape.task_count,
|
|
149
|
-
critical_path_length: shape.critical_path_length,
|
|
150
|
-
max_frontier_width: shape.max_frontier_width,
|
|
151
|
-
serialization_ratio: shape.serialization_ratio,
|
|
152
|
-
critical_path_task_ids: shape.critical_path_task_ids,
|
|
153
|
-
default_policy: 'all_ready_parallel_by_default',
|
|
154
|
-
serialization_reviewed_flag: DISPATCH_SHAPE_SERIALIZATION_REVIEWED_FLAG,
|
|
155
|
-
guidance: `並列を検討しなさい。task ${shape.task_count}件のうちcritical pathが`
|
|
156
|
-
+ `${shape.critical_path_length}段(serialization_ratio ${shape.serialization_ratio})で、`
|
|
157
|
-
+ '大半のtaskが並列候補ではなく一本の依存鎖に押し込まれている。'
|
|
158
|
-
+ 'critical_path_task_idsに沿った依存のうち、実際には干渉しない組を'
|
|
159
|
-
+ 'hard_dependencies/joinsから外せないか見直す'
|
|
160
|
-
+ '(実行主体が足りないなら増やす。増やせないことは直列化の理由にならない)。'
|
|
161
|
-
+ `検討した上でなお直列でよいなら ${DISPATCH_SHAPE_SERIALIZATION_REVIEWED_FLAG} を付けて再実行する。`,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
135
|
/**
|
|
166
136
|
* plan/extractionの`tasks`・`hard_dependencies`・`joins`(nodeRef形式)から、
|
|
167
137
|
* 「このproject_id/plan_keyの内側だけ」に絞ったdispatch形状を計算する高レベル入口。
|