@quolu/lattice 0.60.8 → 0.61.1

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 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が複数なら最初のstartに`--parallel-frontier`を付けます。subsetだけを直列着手する場合は
222
- `--override-reason <reason>`で理由を残しますが、**その申告は一度突き返されます**。
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> --serial-confirmed
228
+ lattice todo start --plan <key> --task <id> --override-reason <reason>
227
229
  ```
228
230
 
229
- 直列の申告に対して`PARALLEL_DISPATCH_RECONSIDER`を返し、並列の再検討を促してから、
230
- 同じ理由に`--serial-confirmed`を付けた再実行だけを通します。規則を文書へ書くだけでは
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`)を計算して結果へ載せ、直列度が閾値を超えるplanを一度突き返します。
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方針を宣言する開始gateです。Lattice自身がAI hostのagentを
253
- 起動するものではなく、実際のdispatchはhostが行います。宣言後もready全件が着手されたかは
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/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.60.8",
3
+ "version": "0.61.1",
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
- # 依存グラフがほぼ一直線(serialization_ratioが閾値超)なら一度突き返す。
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
- # 依存グラフがほぼ一直線なら一度突き返し、再考後の --serialization-reviewed で通す
96
+ # 結果へdispatch_shapeを載せる。--serialization-reviewedは互換のため受理するだけで門ではない
98
97
  start --plan <key> --task <id> [--parallel-frontier|--override-reason <text> [--serial-confirmed]]
99
- # 既定は全ready同時dispatch。直列にするには理由の申告後、再考を経て --serial-confirmed が要る
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
@@ -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, serializationReviewed = false }) {
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のgateはstore初期化より前に判定する(拒否時にstoreへ何も書かないため、
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 { 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' });
@@ -0,0 +1,44 @@
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 async function observeWindowsWorkerProcess(pid) {
25
+ if (!Number.isSafeInteger(pid) || pid < 1) {
26
+ throw new Error('pidが正整数でない');
27
+ }
28
+ const filter = JSON.stringify(`ProcessId=${pid}`);
29
+ const script = `Get-CimInstance Win32_Process -Filter ${filter} | Select-Object ProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress`;
30
+ const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', script], {
31
+ encoding: 'utf8',
32
+ });
33
+ const row = JSON.parse(stdout.trim() || 'null');
34
+ if (!row || Number(row.ProcessId) !== pid) throw new Error(`Win32_Processを観測できない: ${pid}`);
35
+ const argv = String(row.CommandLine || '').trim();
36
+ const startedIdentity = parseWindowsCreationDate(row.CreationDate);
37
+ if (!argv || !startedIdentity) throw new Error('Windows worker process identityを完全観測できない');
38
+ return {
39
+ pid,
40
+ process_group_id: pid,
41
+ started_identity: startedIdentity,
42
+ argv,
43
+ };
44
+ }
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, serialConfirmed = false,
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, serializationReviewed = false }) {
1171
+ async function migrate({ repoRoot, inputRef }) {
1248
1172
  const extraction = await readMigrationInput(repoRoot, inputRef);
1249
- // dispatch_shapeのgateはappendTodoExtraction(store書込み)より前に判定する必要がある
1250
- // (拒否時にstoreへ何も書かないため、再考後の再実行がplan_key_already_existsで
1251
- // 詰まらない)。unresolved/空集合の2つの早期gateは、compileTodoExtraction内部の
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, serializationReviewed = false }) {
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}${serializationReviewed ? ' --serialization-reviewed' : ''}`
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
- * 一本の鎖に押し込まれていることを意味する。`todo start`側が既に持つ
9
- * `all_ready_parallel_by_default`方針をplan作成時点まで前倒しする出発点として
10
- * 0.5を採る。実測(parent-child-repair: task 26, critical path約20,
11
- * ratio≈0.77)を確実に超える一方、緩やかな分岐を持つ通常のplanまでは拾わない
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
- * 閾値判定の対象にする最小task数。
18
- * 3〜5 task程度の一直線planを毎回突き返しても再考の余地がなく
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形状を計算する高レベル入口。