@quolu/lattice 0.35.0 → 0.36.0
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 +28 -1
- package/README.md +8 -0
- package/bin/lattice.mjs +9 -1
- package/docs/bridge-setup.md +13 -0
- package/package.json +1 -1
- package/src/cli-help.mjs +12 -7
- package/src/project-cli.mjs +103 -1
- package/src/todo-cli.mjs +103 -10
- package/src/todo-contracts.mjs +2 -2
- package/src/todo-gantt-html-shared.mjs +2 -1
- package/src/todo-gantt-layout.mjs +13 -3
- package/src/todo-gantt-scope.mjs +27 -2
- package/src/todo-migration.mjs +108 -0
- package/src/todo-revision.mjs +568 -2
- package/src/todo-status.mjs +5 -2
- package/src/todo-store.mjs +155 -33
package/README.ja.md
CHANGED
|
@@ -197,7 +197,34 @@ lattice todo migrate --input <extraction.json> --serialization-reviewed
|
|
|
197
197
|
`active_set`と`next_ready`で観測できます。
|
|
198
198
|
ToDo完了は軽量確認までで、所属ToDoが全てdoneになったPhaseは`gate_ready`となり、`todo phase review`後に
|
|
199
199
|
required evidenceを束縛した`todo phase accept`で重監査の判断を記録します。監査回数やPhase数を自動追加する
|
|
200
|
-
機能ではありません。
|
|
200
|
+
機能ではありません。
|
|
201
|
+
|
|
202
|
+
**監査の既定は「有り」です。** phaseを持たないplanも終端に重監査が要ります(予約Phase
|
|
203
|
+
`terminal-audit`を暗黙に1つ持ちます)。全taskがdoneになった状態は「完走」ではなく`gate_ready`=
|
|
204
|
+
**監査待ち**であり、工程図のlive scopeはそのplanのToDoを畳みません——完走扱いで図から消えることが
|
|
205
|
+
「閉じた」の可視表現なので、監査の記録なしにそこへ行かせません。作成時に拒否はせず、
|
|
206
|
+
`todo migrate`/`plan create`の結果と最後のdoneのadvisoryで`terminal_audit_required`を通知します。
|
|
207
|
+
**終端監査はToDoのdispatch可否へ影響しません**(Phaseは重監査の順序だけを制御し、開始順はToDo DAGが決めます)。
|
|
208
|
+
規約は[ADR 0147](docs/adr/0147-audit-is-on-by-default.md)が正です。
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
lattice todo phase status --plan <key> # phase無しplanでも暗黙Phaseを返す(implicit: true)
|
|
212
|
+
lattice todo phase review --plan <key> --phase terminal-audit --reason <text>
|
|
213
|
+
lattice todo phase accept --plan <key> --phase terminal-audit --input <file>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
誤ってphase無しで作ったplanへ後からPhaseを被せる場合は、`revise-phase`の
|
|
217
|
+
`state_policy: acquire_phase`でdone状態を保ったまま獲得できます(未割当→割当の向きだけを許し、
|
|
218
|
+
既にphaseを持つtaskの付け替えは拒否します)。
|
|
219
|
+
|
|
220
|
+
契約のJSON Schemaは各入口から取れます。入力が合わないときは、違反フィールドのpathが
|
|
221
|
+
error detailの`violation_path`へ載ります(配列のソート違反は`/tasks/1`のようにindexまで名指しします)。
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
lattice plan create --schema --json # 既定は最新v3
|
|
225
|
+
lattice todo revise-phase --schema --json
|
|
226
|
+
lattice plan show <plan_key> --json # planのtask・依存・phase・状態を1コマンドで読む
|
|
227
|
+
```Phase状態は
|
|
201
228
|
`lattice todo phase status --plan <key>`、閲覧中に進捗が更新される工程表は
|
|
202
229
|
`lattice todo gantt serve --port 0`で確認できます。live viewerはloopback-only、read-onlyで、
|
|
203
230
|
`/projects/<project_id>/`というproject固有URLを返します。別projectからそれぞれ起動すれば、独立port・独立SSE経路で同時表示できます。
|
package/README.md
CHANGED
|
@@ -150,6 +150,14 @@ point of use.
|
|
|
150
150
|
transform that cannot be verified is not adopted. A finding that cannot be independently
|
|
151
151
|
re-derived is not recorded.
|
|
152
152
|
|
|
153
|
+
**Heavy audit is on by default.** A plan without explicit phases still carries an implicit
|
|
154
|
+
terminal audit: every task being done means `gate_ready` — *awaiting audit* — not finished. The
|
|
155
|
+
live dependency diagram refuses to fold such a plan away, because folding is how the product
|
|
156
|
+
says "closed", and nothing gets there without an evidence-bound `phase accept`. Creation is never
|
|
157
|
+
rejected over it; the requirement is reported instead. And the audit gate never touches dispatch:
|
|
158
|
+
phases order reviews, the ToDo DAG orders work
|
|
159
|
+
([ADR 0147](docs/adr/0147-audit-is-on-by-default.md)).
|
|
160
|
+
|
|
153
161
|
## Patent
|
|
154
162
|
|
|
155
163
|
The design in this repository is the subject of a Japanese patent application:
|
package/bin/lattice.mjs
CHANGED
|
@@ -50,13 +50,21 @@ if (help !== null) {
|
|
|
50
50
|
process.exitCode = projectCliFailure(process.stderr, error);
|
|
51
51
|
}
|
|
52
52
|
} else if (args.length === 5 && args[0] === 'plan' && args[1] === 'create'
|
|
53
|
-
&& args[2] === '--schema-version' && ['2', '3'].includes(args[3]) && args[4] === '--json') {
|
|
53
|
+
&& args[2] === '--schema-version' && ['1', '2', '3'].includes(args[3]) && args[4] === '--json') {
|
|
54
54
|
const { projectCliFailure, runPlanCreateSchema } = await import('../src/project-cli.mjs');
|
|
55
55
|
try {
|
|
56
56
|
process.exitCode = await runPlanCreateSchema({ stdout: process.stdout, version: Number(args[3]) });
|
|
57
57
|
} catch (error) {
|
|
58
58
|
process.exitCode = projectCliFailure(process.stderr, error);
|
|
59
59
|
}
|
|
60
|
+
} else if (args.length === 4 && args[0] === 'plan' && args[1] === 'show'
|
|
61
|
+
&& typeof args[2] === 'string' && args[2].length > 0 && args[3] === '--json') {
|
|
62
|
+
const { projectCliFailure, runPlanShow } = await import('../src/project-cli.mjs');
|
|
63
|
+
try {
|
|
64
|
+
process.exitCode = await runPlanShow({ cwd: process.cwd(), planKey: args[2], stdout: process.stdout });
|
|
65
|
+
} catch (error) {
|
|
66
|
+
process.exitCode = projectCliFailure(process.stderr, error);
|
|
67
|
+
}
|
|
60
68
|
} else if (args.length === 2 && args[0] === 'factory-diagnostics' && args[1] === '--json') {
|
|
61
69
|
const { buildFactoryDiagnostics } = await import('../src/factory-diagnostics.mjs');
|
|
62
70
|
const diagnostics = await buildFactoryDiagnostics();
|
package/docs/bridge-setup.md
CHANGED
|
@@ -107,6 +107,19 @@ public hostname `lattice.kitepon.dev` を次のoriginへ対応付ける。
|
|
|
107
107
|
3. **Cloudflare public HTTPS**: `https://lattice.kitepon.dev/projects/`がredirectなしで200となり、一覧から開いた
|
|
108
108
|
`/projects/<project_id>/`のHTML titleが`Lattice — <project名> 依存工程図`である。
|
|
109
109
|
|
|
110
|
+
公開viewerの404も、ブラウザとAPIの両契約を別々に確認する。未知URLへ`Accept: text/html`を
|
|
111
|
+
付けたrequestはHTTP 404かつ`Content-Type: text/html`で、`noindex, nofollow`と
|
|
112
|
+
`/projects/`、`https://kitepon.dev/`への戻り先を持つ。`Accept: application/json`では
|
|
113
|
+
HTTP 404かつ`Content-Type: application/json`で、既存の
|
|
114
|
+
`lattice.todo_gantt_http_error.v1`を返す。
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
curl --silent --show-error --include --header 'Accept: text/html' \
|
|
118
|
+
https://lattice.kitepon.dev/unknown
|
|
119
|
+
curl --silent --show-error --include --header 'Accept: application/json' \
|
|
120
|
+
https://lattice.kitepon.dev/unknown
|
|
121
|
+
```
|
|
122
|
+
|
|
110
123
|
外部gateはHTMLだけで閉じず、各projectの
|
|
111
124
|
`https://lattice.kitepon.dev/projects/<project_id>/events`も確認する。応答は200かつ
|
|
112
125
|
`Content-Type: text/event-stream`で、接続直後に`event: state`と現在の`head_digest`を返さなければならない。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
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
|
@@ -26,8 +26,9 @@ Commands:
|
|
|
26
26
|
create --input <file> [--serialization-reviewed]
|
|
27
27
|
# 依存グラフがほぼ一直線(serialization_ratioが閾値超)なら一度突き返す。
|
|
28
28
|
# 再考した上でなお直列でよいなら --serialization-reviewed を付けて再実行する
|
|
29
|
-
create --schema --json
|
|
30
|
-
create --schema-version <2|3> --json
|
|
29
|
+
create --schema --json # 既定は最新版(v3)のJSON Schemaを返す
|
|
30
|
+
create --schema-version <1|2|3> --json
|
|
31
|
+
show <plan_key> --json # task・依存・phase・状態をplan本体から1コマンドで投影する
|
|
31
32
|
compile --request <request.json>
|
|
32
33
|
compile --schema --json # lattice.run_request.v1 の JSON Schema を出す
|
|
33
34
|
verify --request <request.json> --plan <plan.json>
|
|
@@ -91,6 +92,9 @@ Write commands:
|
|
|
91
92
|
revise --plan <key> --input <file>
|
|
92
93
|
revise-phase --plan <key> --input <file>
|
|
93
94
|
revise-set --input <file>
|
|
95
|
+
<revise|revise-phase|revise-set|migrate> --schema --json
|
|
96
|
+
# 実際に受理する最新契約のJSON Schemaを返す(storeを読まない)。
|
|
97
|
+
# 入力が契約に合わないときは、違反フィールドのpathがerror detailへ載る
|
|
94
98
|
phase review --plan <key> --phase <id> --reason <text>
|
|
95
99
|
phase <accept|reject> --plan <key> --phase <id> --input <file>
|
|
96
100
|
phase reopen --plan <key> --phase <id> --reason <text> [--override-reason <text>]
|
|
@@ -135,7 +139,8 @@ registerはLATTICE_BRIDGE_REGISTRAR_SSH_HOSTとLATTICE_BRIDGE_REGISTRAR_SCRIPT
|
|
|
135
139
|
const SUBCOMMAND_USAGE = Object.freeze({
|
|
136
140
|
status: 'status --json',
|
|
137
141
|
'session-context': 'session-context --json',
|
|
138
|
-
'plan create': 'plan create --input <file> [--serialization-reviewed] | --schema --json | --schema-version <2|3> --json',
|
|
142
|
+
'plan create': 'plan create --input <file> [--serialization-reviewed] | --schema --json | --schema-version <1|2|3> --json',
|
|
143
|
+
'plan show': 'plan show <plan_key> --json',
|
|
139
144
|
'plan compile': 'plan compile --request <request.json> | --schema --json',
|
|
140
145
|
'plan verify': 'plan verify --request <request.json> --plan <plan.json>',
|
|
141
146
|
'run start': 'run start --request <request.json> --executor <adapter> | --schema --json',
|
|
@@ -177,10 +182,10 @@ const SUBCOMMAND_USAGE = Object.freeze({
|
|
|
177
182
|
'todo reopen': 'todo reopen --plan <key> --task <id> --reason <text> [--override-reason <text>]',
|
|
178
183
|
'todo evidence': 'todo evidence promote --plan <key> --task <id> --evidence <file>',
|
|
179
184
|
'todo evidence promote': 'todo evidence promote --plan <key> --task <id> --evidence <file>',
|
|
180
|
-
'todo revise': 'todo revise --plan <key> --input <file>',
|
|
181
|
-
'todo revise-phase': 'todo revise-phase --plan <key> --input <file>',
|
|
182
|
-
'todo revise-set': 'todo revise-set --input <file>',
|
|
183
|
-
'todo migrate': 'todo migrate --input <extraction.json> [--serialization-reviewed]',
|
|
185
|
+
'todo revise': 'todo revise --plan <key> --input <file> | --schema --json',
|
|
186
|
+
'todo revise-phase': 'todo revise-phase --plan <key> --input <file> | --schema --json',
|
|
187
|
+
'todo revise-set': 'todo revise-set --input <file> | --schema --json',
|
|
188
|
+
'todo migrate': 'todo migrate --input <extraction.json> [--serialization-reviewed] | --schema --json',
|
|
184
189
|
'sensor init': 'sensor init [path] --json',
|
|
185
190
|
'sensor sync': 'sensor sync [path] --json',
|
|
186
191
|
'runtime-errors snapshot': 'runtime-errors snapshot [--after-cursor <n>] [--limit <n>] --json',
|
package/src/project-cli.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
buildTodoPlan,
|
|
23
23
|
createTodoStoreWriter,
|
|
24
24
|
initializeAuthoredTodoStore,
|
|
25
|
+
isPhaselessTodoPlanSchema,
|
|
25
26
|
readTodoIndependenceArtifact,
|
|
26
27
|
readTodoStore,
|
|
27
28
|
TodoStoreError,
|
|
@@ -481,6 +482,9 @@ export async function runPlanCreate({ cwd, inputRef, stdout, serializationReview
|
|
|
481
482
|
max_frontier_width: dispatchShape.max_frontier_width,
|
|
482
483
|
serialization_ratio: dispatchShape.serialization_ratio,
|
|
483
484
|
},
|
|
485
|
+
// ADR 0147裁定3: phase無し(v3)のplan createは拒否せず、終端監査が要ることを結果へ
|
|
486
|
+
// 明示するに留める。phase入力(v4/v5)ならfalse——既存のPhase gateがそのまま重監査を担う。
|
|
487
|
+
terminal_audit_required: isPhaselessTodoPlanSchema(member.plan.schema),
|
|
484
488
|
result_digest: '',
|
|
485
489
|
};
|
|
486
490
|
result.result_digest = resultDigest(result);
|
|
@@ -488,7 +492,16 @@ export async function runPlanCreate({ cwd, inputRef, stdout, serializationReview
|
|
|
488
492
|
return 0;
|
|
489
493
|
}
|
|
490
494
|
|
|
491
|
-
|
|
495
|
+
/**
|
|
496
|
+
* `--schema --json`(版指定なし)の既定はCURRENT_CREATE_INPUT_SCHEMAと同じv3にする。
|
|
497
|
+
* 既定がv1のままだと、素の`--schema`を叩いたAIが古いv1(Phaseを表現できない)を
|
|
498
|
+
* 受け取り、実運用で通らない入力を作ってしまう(実際に踏んだ)。
|
|
499
|
+
* `--schema-version 1`は互換のため引き続き取得できる(bin/lattice.mjs側で許可)。
|
|
500
|
+
*
|
|
501
|
+
* 「どの版を返したか」は返すJSON Schema自身の`title`(例: `lattice.plan_create_input.v3`)が
|
|
502
|
+
* 既に機械可読に持っている。壊さずに追加のkeyを足す理由が無いので足さない。
|
|
503
|
+
*/
|
|
504
|
+
export async function runPlanCreateSchema({ stdout, version = 3 }) {
|
|
492
505
|
if (![1, 2, 3].includes(version)) throw new TypeError('unsupported plan create schema version');
|
|
493
506
|
const expected = version === 3 ? DECOUPLED_PHASE_CREATE_INPUT_SCHEMA
|
|
494
507
|
: version === 2 ? PHASE_CREATE_INPUT_SCHEMA : CREATE_INPUT_SCHEMA;
|
|
@@ -504,6 +517,95 @@ export async function runPlanCreateSchema({ stdout, version = 1 }) {
|
|
|
504
517
|
}
|
|
505
518
|
}
|
|
506
519
|
|
|
520
|
+
const PLAN_SHOW_RESULT_SCHEMA = 'lattice.plan_show_result.v1';
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* `todo bindings`はcompile_binding付きtaskだけを投影するので、通常planでは空配列を返す。
|
|
524
|
+
* それを見て「planが空だ」と誤読された実績があるため、plan本体(task・依存・phase・状態)を
|
|
525
|
+
* 1コマンドで読める面を別に持つ。読み出しは既存store readerとtodo status projectionの
|
|
526
|
+
* 再利用に留め、journalを独自に再実装しない。
|
|
527
|
+
*/
|
|
528
|
+
export async function runPlanShow({ cwd, planKey, stdout }) {
|
|
529
|
+
const repoRoot = resolveRepoRoot(cwd);
|
|
530
|
+
if (repoRoot === null) throw new TodoStoreError('REPO_UNRESOLVED', 'git_toplevel_unresolved');
|
|
531
|
+
const store = await readTodoStore({ repoRoot });
|
|
532
|
+
const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
|
|
533
|
+
if (member === undefined) {
|
|
534
|
+
// 既存read commands(independence compile等)と同じcode/reasonを踏襲する。
|
|
535
|
+
// 未知のplan_keyを「store不整合」と同じ扱いにするのはこのCLI全体の既定であり、
|
|
536
|
+
// ここだけ別codeへ逸れると呼び出し側の分岐が増える。
|
|
537
|
+
throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active', undefined, {
|
|
538
|
+
plan_key: planKey, next_action: 'check_active_plans_via_status',
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
const { plan, tasks, phases } = member;
|
|
542
|
+
const phaseInput = ['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(plan.schema);
|
|
543
|
+
const stateByTaskId = new Map(tasks.map((state) => [state.task_id, state]));
|
|
544
|
+
// snapshot artifactの形式(v1にはphasesキーが無い)には縛られない導出ビューを読む
|
|
545
|
+
// (readTodoStoreが常に member.phases として埋める。ADR 0147)。
|
|
546
|
+
const phaseStatusById = new Map((phases ?? []).map((phase) => [phase.phase_id, phase.status]));
|
|
547
|
+
|
|
548
|
+
// 依存の本数: このplanの中でそのtaskへ入ってくるhard_dependencies辺と、joinで
|
|
549
|
+
// 合流するafter辺の合計。cross-plan参照は数えない(plan showは単一planの投影のため)。
|
|
550
|
+
const dependsOnCount = new Map(plan.tasks.map((task) => [task.task_id, 0]));
|
|
551
|
+
for (const edge of plan.hard_dependencies) {
|
|
552
|
+
if (edge.to.plan_key === planKey && dependsOnCount.has(edge.to.task_id)) {
|
|
553
|
+
dependsOnCount.set(edge.to.task_id, dependsOnCount.get(edge.to.task_id) + 1);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const join of plan.joins) {
|
|
557
|
+
if (join.before.plan_key === planKey && dependsOnCount.has(join.before.task_id)) {
|
|
558
|
+
dependsOnCount.set(join.before.task_id, dependsOnCount.get(join.before.task_id) + join.after.length);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const taskList = plan.tasks.map((task) => ({
|
|
563
|
+
task_id: task.task_id,
|
|
564
|
+
title: task.title,
|
|
565
|
+
lane: task.lane,
|
|
566
|
+
phase_id: phaseInput ? task.phase_id : null,
|
|
567
|
+
state: stateByTaskId.get(task.task_id).status,
|
|
568
|
+
depends_on_count: dependsOnCount.get(task.task_id) ?? 0,
|
|
569
|
+
}));
|
|
570
|
+
|
|
571
|
+
const phaseList = phaseInput ? plan.phases.map((phase) => ({
|
|
572
|
+
phase_id: phase.phase_id,
|
|
573
|
+
title: phase.title,
|
|
574
|
+
gate_policy: phase.gate_policy,
|
|
575
|
+
predecessor_phase_ids: phase.predecessor_phase_ids,
|
|
576
|
+
status: phaseStatusById.get(phase.phase_id) ?? null,
|
|
577
|
+
})) : [];
|
|
578
|
+
|
|
579
|
+
// dispatch形状はplan create時に既に計算している同じ関数を再利用する。
|
|
580
|
+
// critical path長・frontier幅を独自に計算し直さない。
|
|
581
|
+
const dispatchShape = computeTodoDispatchShapeForPlan({
|
|
582
|
+
projectId: plan.project_id, planKey,
|
|
583
|
+
taskIds: plan.tasks.map(({ task_id: taskId }) => taskId),
|
|
584
|
+
hardDependencies: plan.hard_dependencies, joins: plan.joins,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const result = {
|
|
588
|
+
schema: PLAN_SHOW_RESULT_SCHEMA,
|
|
589
|
+
project_id: plan.project_id,
|
|
590
|
+
plan_key: plan.plan_key,
|
|
591
|
+
plan_version: plan.plan_version,
|
|
592
|
+
plan_schema: plan.schema,
|
|
593
|
+
has_phases: phaseInput,
|
|
594
|
+
phases: phaseList,
|
|
595
|
+
tasks: taskList,
|
|
596
|
+
topology: {
|
|
597
|
+
task_count: dispatchShape.task_count,
|
|
598
|
+
critical_path_length: dispatchShape.critical_path_length,
|
|
599
|
+
max_frontier_width: dispatchShape.max_frontier_width,
|
|
600
|
+
serialization_ratio: dispatchShape.serialization_ratio,
|
|
601
|
+
},
|
|
602
|
+
result_digest: '',
|
|
603
|
+
};
|
|
604
|
+
result.result_digest = resultDigest(result);
|
|
605
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
606
|
+
return 0;
|
|
607
|
+
}
|
|
608
|
+
|
|
507
609
|
export function projectStatusFailure({ cwd, stdout, cliVersion, error }) {
|
|
508
610
|
const result = invalidStatus({
|
|
509
611
|
cliVersion, repoRoot: resolveRepoRoot(cwd),
|
package/src/todo-cli.mjs
CHANGED
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
applyTodoRevisionSet,
|
|
38
38
|
createTodoStoreWriter,
|
|
39
39
|
TodoStoreError,
|
|
40
|
+
isPhaselessTodoPlanSchema,
|
|
41
|
+
TERMINAL_AUDIT_PHASE_ID,
|
|
40
42
|
readTodoIndependenceArtifact,
|
|
41
43
|
readTodoSeamProposalArtifact,
|
|
42
44
|
readTodoStore,
|
|
@@ -52,6 +54,7 @@ import {
|
|
|
52
54
|
} from './todo-store.mjs';
|
|
53
55
|
import {
|
|
54
56
|
appendTodoExtraction,
|
|
57
|
+
explainTodoExtraction,
|
|
55
58
|
validateTodoExtraction,
|
|
56
59
|
} from './todo-migration.mjs';
|
|
57
60
|
import {
|
|
@@ -97,6 +100,7 @@ import {
|
|
|
97
100
|
validateWitnessDraft,
|
|
98
101
|
} from './witness-scaffold.mjs';
|
|
99
102
|
import {
|
|
103
|
+
explainPhaseTodoRevision, explainTodoRevision, explainTodoRevisionSet,
|
|
100
104
|
parseTodoSourceRef, todoLegacyReconciliationDigest, validatePhaseTodoRevision,
|
|
101
105
|
validateTodoRevision, validateTodoRevisionSet,
|
|
102
106
|
} from './todo-revision.mjs';
|
|
@@ -113,6 +117,31 @@ const ACTOR_ENV_KEYS = Object.freeze([
|
|
|
113
117
|
'LATTICE_TODO_ACTOR_AGENT',
|
|
114
118
|
]);
|
|
115
119
|
|
|
120
|
+
/**
|
|
121
|
+
* `revise` / `revise-set` / `revise-phase` / `migrate`が実際に受理する最新契約のJSON
|
|
122
|
+
* Schemaを配布物から読む入口(`project-cli.mjs`の`runPlanCreateSchema`と同じ作法)。
|
|
123
|
+
*
|
|
124
|
+
* schemaを取る手段がCLIに無いと、AIはsrcを読んで必須keyを数えるしかなくなる
|
|
125
|
+
* (実運用で`phase_todo_revision.v3`の必須12 keyを試行錯誤で当てた)。storeは読まない
|
|
126
|
+
* ——`plan create --schema`と同じく決定的な出力・exit 0にする。
|
|
127
|
+
*/
|
|
128
|
+
const TODO_SCHEMA_COMMANDS = Object.freeze({
|
|
129
|
+
revise: { title: 'lattice.todo_revision.v2', file: 'lattice.todo_revision.v2.schema.json' },
|
|
130
|
+
'revise-set': { title: 'lattice.todo_revision_set.v3', file: 'lattice.todo_revision_set.v3.schema.json' },
|
|
131
|
+
'revise-phase': {
|
|
132
|
+
title: 'lattice.phase_todo_revision.v3', file: 'lattice.phase_todo_revision.v3.schema.json',
|
|
133
|
+
},
|
|
134
|
+
migrate: { title: 'lattice.todo_extraction.v2', file: 'lattice.todo_extraction.v2.schema.json' },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
async function runTodoSchemaCommand(command, stdout) {
|
|
138
|
+
const spec = TODO_SCHEMA_COMMANDS[command];
|
|
139
|
+
const schemaUrl = new URL(`../docs/schemas/${spec.file}`, import.meta.url);
|
|
140
|
+
const schema = JSON.parse(await readFile(schemaUrl, 'utf8'));
|
|
141
|
+
if (schema?.title !== spec.title) throw new TypeError(`bundled ${command} schema invalid`);
|
|
142
|
+
stdout.write(`${JSON.stringify(schema)}\n`);
|
|
143
|
+
}
|
|
144
|
+
|
|
116
145
|
function usageFailure(stderr, argv) {
|
|
117
146
|
const received = argv.length === 0 ? '(none)' : argv.join(' ').replace(/[\r\n]/gu, ' ');
|
|
118
147
|
stderr.write(`lattice todo: unsupported command or arguments: ${received}\n`);
|
|
@@ -223,7 +252,11 @@ async function readMigrationInput(repoRoot, inputRef) {
|
|
|
223
252
|
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
224
253
|
}
|
|
225
254
|
if (!validateTodoExtraction(extraction)) {
|
|
226
|
-
|
|
255
|
+
// 「schema_invalid」だけでは何のfieldがどう壊れているか分からない(ADR 0130の案内規律)。
|
|
256
|
+
// explainは可否判定を変えず、診断だけを追加する。
|
|
257
|
+
const explained = explainTodoExtraction(extraction);
|
|
258
|
+
throw new TodoStoreError('INVALID_TODO_EXTRACTION', 'schema_invalid', undefined,
|
|
259
|
+
explained.valid ? undefined : { violation_reason: explained.reason, violation_path: explained.path });
|
|
227
260
|
}
|
|
228
261
|
return extraction;
|
|
229
262
|
}
|
|
@@ -232,6 +265,7 @@ async function readRevisionInput(repoRoot, inputRef, {
|
|
|
232
265
|
validate = validateTodoRevision,
|
|
233
266
|
invalidCode = 'REVISION_INVALID',
|
|
234
267
|
invalidReason = 'revision_schema_or_digest_invalid',
|
|
268
|
+
explain = explainTodoRevision,
|
|
235
269
|
} = {}) {
|
|
236
270
|
const canonicalRoot = await realpath(repoRoot);
|
|
237
271
|
const absolute = path.resolve(canonicalRoot, inputRef);
|
|
@@ -268,7 +302,16 @@ async function readRevisionInput(repoRoot, inputRef, {
|
|
|
268
302
|
try { revision = JSON.parse(text.slice(0, -1)); } catch {
|
|
269
303
|
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
270
304
|
}
|
|
271
|
-
if (!validate(revision))
|
|
305
|
+
if (!validate(revision)) {
|
|
306
|
+
// 「schema_or_digest_invalid」だけでは何のfieldがどう壊れているか分からない
|
|
307
|
+
// (ADR 0130の案内規律)。explainは可否判定を変えず、診断だけを追加する。
|
|
308
|
+
// 呼び出し元がexplainを渡さない(phase decision入力等)場合はdetail無しのまま。
|
|
309
|
+
const explained = explain === null ? null : explain(revision);
|
|
310
|
+
throw new TodoStoreError(invalidCode, invalidReason, undefined,
|
|
311
|
+
explained === null || explained.valid ? undefined : {
|
|
312
|
+
violation_reason: explained.reason, violation_path: explained.path,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
272
315
|
if (text !== `${canonicalizeTodoArtifact(revision)}\n`) {
|
|
273
316
|
throw new TodoStoreError(invalidCode, 'non_canonical_revision_bytes');
|
|
274
317
|
}
|
|
@@ -373,6 +416,25 @@ function mutationActor(env) {
|
|
|
373
416
|
return { host: entries[0].value, session: entries[1].value, agent: entries[2].value };
|
|
374
417
|
}
|
|
375
418
|
|
|
419
|
+
/**
|
|
420
|
+
* phase無しplanで、この変異の結果terminal-audit Phaseがgate_ready(全task done・未監査)に
|
|
421
|
+
* なっていれば助言を返す(ADR 0147)。doneの結果だけを見て機械的に判定するので、既にreview
|
|
422
|
+
* まで進んでいれば`gate_ready`ではなくなり、二重に案内しない。phase付きplanや、まだ
|
|
423
|
+
* pending taskが残っているplanではterminal-audit Phase自体が無い/gate_readyでないので、
|
|
424
|
+
* このヘルパはnullを返し既存の`advisory: null`の挙動を変えない。
|
|
425
|
+
*/
|
|
426
|
+
function terminalAuditDoneAdvisory(plan, phases) {
|
|
427
|
+
if (!isPhaselessTodoPlanSchema(plan.schema)) return null;
|
|
428
|
+
const phase = phases.find(({ phase_id }) => phase_id === TERMINAL_AUDIT_PHASE_ID);
|
|
429
|
+
if (phase?.status !== 'gate_ready') return null;
|
|
430
|
+
return {
|
|
431
|
+
terminal_audit_required: true, phase_id: TERMINAL_AUDIT_PHASE_ID, status: phase.status,
|
|
432
|
+
guidance: '全taskがdoneになった。このplanはphaseを持たないため、終端の重監査'
|
|
433
|
+
+ '(todo phase review --plan <key> --phase terminal-audit → todo phase accept)を'
|
|
434
|
+
+ '経るまで「閉じた」ことにはならない。',
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
376
438
|
async function mutate({
|
|
377
439
|
repoRoot, env, planKey, taskId, kind, payload, evidenceRef, advisory = null,
|
|
378
440
|
}) {
|
|
@@ -383,13 +445,18 @@ async function mutate({
|
|
|
383
445
|
if (kind === 'done' && payload === 'evidence_promotion') {
|
|
384
446
|
eventPayload = { done_mode: 'evidence_promotion', imported: true, evidence };
|
|
385
447
|
}
|
|
386
|
-
const { event, snapshot } = await appendTodoEvent({
|
|
448
|
+
const { event, snapshot, plan, phases } = await appendTodoEvent({
|
|
387
449
|
repoRoot,
|
|
388
450
|
writer: createTodoStoreWriter({ caller: 'g5-authoring' }),
|
|
389
451
|
planKey,
|
|
390
452
|
event: { kind, task_id: taskId, actor, payload: eventPayload },
|
|
391
453
|
});
|
|
392
454
|
const task = snapshot.tasks.find(({ task_id: current }) => current === event.task_id);
|
|
455
|
+
// advisoryは呼び出し側(startTask)がstart用に既に組んでいればそれを尊重し、無ければ
|
|
456
|
+
// done時だけ終端監査の要否を調べる。block/unblock/reopenはnullのまま(既存挙動を変えない)。
|
|
457
|
+
// Phase状態はsnapshot(v1にはphasesキーが無い)でなく、appendTodoEventが別途返す
|
|
458
|
+
// 導出ビュー`phases`から読む。
|
|
459
|
+
const resolvedAdvisory = advisory ?? (kind === 'done' ? terminalAuditDoneAdvisory(plan, phases) : null);
|
|
393
460
|
const result = {
|
|
394
461
|
schema: 'lattice.todo_mutation_result.v2',
|
|
395
462
|
project_id: event.project_id,
|
|
@@ -402,7 +469,7 @@ async function mutate({
|
|
|
402
469
|
journal_head_digest: event.event_digest,
|
|
403
470
|
snapshot_digest: snapshot.snapshot_digest,
|
|
404
471
|
status: task.status,
|
|
405
|
-
advisory,
|
|
472
|
+
advisory: resolvedAdvisory,
|
|
406
473
|
result_digest: '',
|
|
407
474
|
};
|
|
408
475
|
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
@@ -599,6 +666,8 @@ async function phaseDecision({ repoRoot, env, planKey, phaseId, outcome, inputRe
|
|
|
599
666
|
const input = await readRevisionInput(repoRoot, inputRef, {
|
|
600
667
|
validate: (value) => validatePhaseDecisionInput(value, outcome),
|
|
601
668
|
invalidCode: 'PHASE_DECISION_INVALID', invalidReason: 'phase_decision_schema_or_digest_invalid',
|
|
669
|
+
// phase decision入力はrevision契約と別形状。既定のrevision explainを誤って当てない。
|
|
670
|
+
explain: null,
|
|
602
671
|
});
|
|
603
672
|
const payload = outcome === 'accept'
|
|
604
673
|
? { review_event_digest: input.review_event_digest, decision_evidence: input.decision_evidence,
|
|
@@ -609,11 +678,13 @@ async function phaseDecision({ repoRoot, env, planKey, phaseId, outcome, inputRe
|
|
|
609
678
|
}
|
|
610
679
|
|
|
611
680
|
async function phaseMutation({ repoRoot, env, planKey, phaseId, kind, payload }) {
|
|
612
|
-
const { event, snapshot } = await appendTodoEvent({
|
|
681
|
+
const { event, snapshot, phases } = await appendTodoEvent({
|
|
613
682
|
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), planKey,
|
|
614
683
|
event: { kind, phase_id: phaseId, actor: mutationActor(env), payload },
|
|
615
684
|
});
|
|
616
|
-
|
|
685
|
+
// snapshot.phasesはv1(phase無しplan)には存在しない。導出ビュー`phases`を見る
|
|
686
|
+
// (これは暗黙のterminal-audit Phaseにも常に埋まっている)。
|
|
687
|
+
const phase = phases.find(({ phase_id: current }) => current === phaseId);
|
|
617
688
|
if (phase === undefined) throw new TodoStoreError('STORE_INCONSISTENT', 'phase_not_active');
|
|
618
689
|
const result = {
|
|
619
690
|
schema: 'lattice.phase_mutation_result.v1', project_id: event.project_id,
|
|
@@ -629,14 +700,16 @@ async function phaseMutation({ repoRoot, env, planKey, phaseId, kind, payload })
|
|
|
629
700
|
async function phaseStatus({ repoRoot, planKey }) {
|
|
630
701
|
const store = await readTodoStore({ repoRoot });
|
|
631
702
|
const [member] = selectMembers(store, planKey);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
703
|
+
// ADR 0147以降、phase無しplan(v1/v2/v3)もreadTodoStoreが導出済みの暗黙terminal-audit
|
|
704
|
+
// Phaseをmember.phasesへ積んでいる(snapshot artifactの形式は変えない・v1にはphasesキーが
|
|
705
|
+
// 無いのでsnapshot.phasesは直接読まない)。ここでPHASE_UNAVAILABLEへ拒否せず、その暗黙Phase
|
|
706
|
+
// をそのまま返す——`implicit`で機械可読に「宣言されたPhaseではない」ことを示す。
|
|
707
|
+
const implicit = isPhaselessTodoPlanSchema(member.plan.schema);
|
|
635
708
|
const result = {
|
|
636
709
|
schema: 'lattice.phase_status_result.v1', project_id: store.project_id,
|
|
637
710
|
plan_key: member.plan.plan_key, plan_version: member.plan.plan_version,
|
|
638
711
|
journal_head_digest: member.journal.events.at(-1).event_digest,
|
|
639
|
-
phases: member.
|
|
712
|
+
implicit, phases: member.phases, result_digest: '',
|
|
640
713
|
};
|
|
641
714
|
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
642
715
|
return result;
|
|
@@ -689,6 +762,10 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
|
|
|
689
762
|
max_frontier_width: dispatchShape.max_frontier_width,
|
|
690
763
|
serialization_ratio: dispatchShape.serialization_ratio,
|
|
691
764
|
},
|
|
765
|
+
// ADR 0147裁定3: phase無しplanの作成は拒否せず、終端監査が要ることを結果へ明示するに
|
|
766
|
+
// 留める。extraction経由のmigrateは常にphase無しplan(todo_plan.v2)を作るが、将来の
|
|
767
|
+
// 拡張に備えisPhaselessTodoPlanSchemaで動的に判定する。
|
|
768
|
+
terminal_audit_required: isPhaselessTodoPlanSchema(imported.plan.schema),
|
|
692
769
|
result_digest: '',
|
|
693
770
|
};
|
|
694
771
|
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
@@ -711,6 +788,7 @@ async function reviseSet({ repoRoot, env, inputRef }) {
|
|
|
711
788
|
validate: validateTodoRevisionSet,
|
|
712
789
|
invalidCode: 'REVISION_SET_INVALID',
|
|
713
790
|
invalidReason: 'revision_set_schema_invalid',
|
|
791
|
+
explain: explainTodoRevisionSet,
|
|
714
792
|
});
|
|
715
793
|
return applyTodoRevisionSet({
|
|
716
794
|
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), revisionSet,
|
|
@@ -722,6 +800,7 @@ async function revisePhase({ repoRoot, env, planKey, inputRef }) {
|
|
|
722
800
|
const revision = await readRevisionInput(repoRoot, inputRef, {
|
|
723
801
|
validate: validatePhaseTodoRevision, invalidCode: 'REVISION_INVALID',
|
|
724
802
|
invalidReason: 'phase_revision_schema_or_digest_invalid',
|
|
803
|
+
explain: explainPhaseTodoRevision,
|
|
725
804
|
});
|
|
726
805
|
if (revision.plan_key !== planKey) throw new TodoStoreError('REVISION_INVALID', 'requested_plan_mismatch');
|
|
727
806
|
return applyPhaseTodoRevision({ repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }),
|
|
@@ -1922,6 +2001,20 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
1922
2001
|
throw new TypeError('runTodoCli optionsが不正');
|
|
1923
2002
|
}
|
|
1924
2003
|
|
|
2004
|
+
// `--schema --json`はstoreを読まない決定的な出力(`plan create --schema`と同じ規律)。
|
|
2005
|
+
// 通常dispatchより前に処理し、repoRoot解決やdashboard daemon起動を経由させない。
|
|
2006
|
+
if (argv.length === 3 && argv[1] === '--schema' && argv[2] === '--json'
|
|
2007
|
+
&& Object.hasOwn(TODO_SCHEMA_COMMANDS, argv[0])) {
|
|
2008
|
+
try {
|
|
2009
|
+
await runTodoSchemaCommand(argv[0], stdout);
|
|
2010
|
+
return 0;
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
return typedFailure(stderr, {
|
|
2013
|
+
code: 'INTERNAL_FAILURE', message: error?.constructor?.name ?? 'Error',
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1925
2018
|
let action = null;
|
|
1926
2019
|
if ((argv.length === 1 && argv[0] === 'status')
|
|
1927
2020
|
|| (argv.length === 2 && argv[0] === 'status' && argv[1] === '--json')) {
|
package/src/todo-contracts.mjs
CHANGED
|
@@ -375,8 +375,8 @@ function validStateMigration(value) {
|
|
|
375
375
|
'from_task_id', 'to_task_id', 'state_policy', 'state',
|
|
376
376
|
]) && isTodoIdentifier(entry.from_task_id)
|
|
377
377
|
&& (entry.to_task_id === 'removed' || isTodoIdentifier(entry.to_task_id))
|
|
378
|
-
&& ['carry', 'carry_reconciled_metadata', 'reset_pending', 'removed'].includes(entry.state_policy)
|
|
379
|
-
&& ((['carry', 'carry_reconciled_metadata'].includes(entry.state_policy)
|
|
378
|
+
&& ['carry', 'carry_reconciled_metadata', 'reset_pending', 'removed', 'acquire_phase'].includes(entry.state_policy)
|
|
379
|
+
&& ((['carry', 'carry_reconciled_metadata', 'acquire_phase'].includes(entry.state_policy)
|
|
380
380
|
&& entry.to_task_id !== 'removed' && validCarriedState(entry.state))
|
|
381
381
|
|| (entry.state_policy === 'reset_pending' && entry.to_task_id !== 'removed' && entry.state === null)
|
|
382
382
|
|| (entry.state_policy === 'removed' && entry.to_task_id === 'removed' && entry.state === null)))
|
|
@@ -117,7 +117,8 @@ export function renderPhaseProgress(readModel) {
|
|
|
117
117
|
const settledRows = [];
|
|
118
118
|
for (const member of readModel.members) {
|
|
119
119
|
if (!['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(member.plan.schema)) continue;
|
|
120
|
-
|
|
120
|
+
// snapshot artifactの形式には縛られない導出ビュー(member.phases)を読む(ADR 0147)。
|
|
121
|
+
const phases = new Map(member.phases.map((phase) => [phase.phase_id, phase]));
|
|
121
122
|
for (const phase of member.plan.phases) {
|
|
122
123
|
const tasks = member.plan.tasks.filter((task) => task.phase_id === phase.phase_id);
|
|
123
124
|
const states = new Map(member.tasks.map((task) => [task.task_id, task.status]));
|
|
@@ -153,7 +153,9 @@ function normalizeInput(readModel, chainProjection) {
|
|
|
153
153
|
}
|
|
154
154
|
const { plan } = member;
|
|
155
155
|
const statusByTask = new Map(member.tasks.map((task) => [task.task_id, task]));
|
|
156
|
-
|
|
156
|
+
// snapshot artifactの形式(v1にはphasesキーが無い)には縛られない導出ビューを読む
|
|
157
|
+
// (readTodoStoreが常にmember.phasesとして埋める。ADR 0147)。
|
|
158
|
+
const statusByPhase = new Map((member.phases ?? [])
|
|
157
159
|
.map((phase) => [phase.phase_id, phase.status]));
|
|
158
160
|
for (const task of plan.tasks) {
|
|
159
161
|
if (!plain(task) || typeof task.task_id !== 'string' || typeof task.lane !== 'string') {
|
|
@@ -174,7 +176,14 @@ function normalizeInput(readModel, chainProjection) {
|
|
|
174
176
|
status: state.status,
|
|
175
177
|
plan_schema: plan.schema ?? null,
|
|
176
178
|
phase_id: task.phase_id ?? null,
|
|
177
|
-
|
|
179
|
+
// ADR 0147: phase無しplan(task.phase_idが無い世代)も、終端に暗黙のterminal-audit
|
|
180
|
+
// Phaseを1つ持つ(todo-store.mjsのphasesOf/TERMINAL_AUDIT_PHASE_ID)。ここで素通しせず
|
|
181
|
+
// nullのままにすると、gantt scope側(todo-gantt-scope.mjs)が「監査未了のplanを畳まない」
|
|
182
|
+
// 判定に使える材料を一切受け取れない。'terminal-audit'はTERMINAL_AUDIT_PHASE_IDと同じ
|
|
183
|
+
// 予約IDで、phase_readyの判定(v4だけを見る既存分岐)には影響しない。
|
|
184
|
+
phase_status: task.phase_id === undefined
|
|
185
|
+
? statusByPhase.get('terminal-audit') ?? null
|
|
186
|
+
: statusByPhase.get(task.phase_id) ?? null,
|
|
178
187
|
phase_ready: plan.schema !== 'lattice.todo_plan.v4'
|
|
179
188
|
|| statusByPhase.get(task.phase_id) === 'active',
|
|
180
189
|
});
|
|
@@ -236,7 +245,8 @@ function readyTaskKeys(readModel, nodes, nodesByKey, incoming) {
|
|
|
236
245
|
const phaseStatuses = new Map();
|
|
237
246
|
const phaseAcceptIncoming = new Map(nodes.map(({ key }) => [key, new Set()]));
|
|
238
247
|
for (const member of readModel.members) {
|
|
239
|
-
|
|
248
|
+
// snapshot artifactの形式には縛られない導出ビュー(member.phases)を読む(ADR 0147)。
|
|
249
|
+
for (const phase of member.phases ?? []) {
|
|
240
250
|
phaseStatuses.set(JSON.stringify([
|
|
241
251
|
member.plan.project_id, member.plan.plan_key, phase.phase_id,
|
|
242
252
|
]), phase.status);
|
package/src/todo-gantt-scope.mjs
CHANGED
|
@@ -38,10 +38,35 @@ function compareText(left, right) {
|
|
|
38
38
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* ADR 0147: phase無しplan(v1/v2/v3)は終端の暗黙Phase(terminal-audit)がacceptedになるまで
|
|
43
|
+
* 「閉じた」ことにならない。全taskがdoneでも監査未了なら、そのplanのToDoは生きた作業と同じ
|
|
44
|
+
* 扱い(distance 0)にして畳ませない——畳んでしまうと監査待ちであることが図から消え、
|
|
45
|
+
* ADR 0147が塞ごうとした「一度も重監査を通らず完走した」事故と外形が同じになる。
|
|
46
|
+
*
|
|
47
|
+
* v4/v5(phaseを宣言したplan)はこの判定の対象外にする——既存のPhase gateが重監査を担って
|
|
48
|
+
* おり、ここで同じ規律を足すとPhase単位の既存fold挙動を変えてしまう(非目標)。
|
|
49
|
+
* `phase_status`はレイアウト層(todo-gantt-layout.mjs)がplanの世代を問わず埋める。
|
|
50
|
+
* phase無しplanのtaskにはphase_idフィールド自体が無いため、そこでは暗黙Phaseの状態を
|
|
51
|
+
* 埋める。フィールドが無い/nullの入力(既存test・素のnode)は従来どおり対象外(false)になる。
|
|
52
|
+
*
|
|
53
|
+
* 対象は`gate_ready`(全task doneで監査待ち)・`reviewing`(監査中)・`rejected`
|
|
54
|
+
* (監査が通らず要フォロー)の3状態だけに絞る。`active`(一部taskがまだpending)は、
|
|
55
|
+
* 他のtaskが図に残っている限りplanが未完了だと分かるので対象にしない——ここまで
|
|
56
|
+
* 広げると、完走していない枝の通常foldまで止めてしまい既存挙動を変える。
|
|
57
|
+
*/
|
|
58
|
+
const AUDIT_PENDING_PHASE_STATUSES = new Set(['gate_ready', 'reviewing', 'rejected']);
|
|
59
|
+
function auditPending(node) {
|
|
60
|
+
if (!AUDIT_PENDING_PHASE_STATUSES.has(node.phase_status ?? null)) return false;
|
|
61
|
+
const schema = node.plan_schema ?? null;
|
|
62
|
+
return schema !== 'lattice.todo_plan.v4' && schema !== 'lattice.todo_plan.v5';
|
|
63
|
+
}
|
|
64
|
+
|
|
41
65
|
/**
|
|
42
66
|
* Forward distance from each node to the nearest live (non-done) node, over the
|
|
43
67
|
* dependency DAG. A live node is at distance 0; a node with no live descendant
|
|
44
|
-
* is at Infinity.
|
|
68
|
+
* is at Infinity. A done node whose plan's terminal audit (ADR 0147) has not
|
|
69
|
+
* been accepted is also pinned at distance 0 — it must not fold away silently.
|
|
45
70
|
*
|
|
46
71
|
* Edges always increase the wave (`assignWaves` is a longest-path layering), so
|
|
47
72
|
* visiting nodes in descending wave order guarantees every successor is settled
|
|
@@ -54,7 +79,7 @@ function distanceToLive(nodes, edges, wave) {
|
|
|
54
79
|
const ordered = [...nodes].sort((left, right) => wave.get(right.key) - wave.get(left.key)
|
|
55
80
|
|| compareText(right.key, left.key));
|
|
56
81
|
for (const node of ordered) {
|
|
57
|
-
if (node.status !== 'done') {
|
|
82
|
+
if (node.status !== 'done' || auditPending(node)) {
|
|
58
83
|
distance.set(node.key, 0);
|
|
59
84
|
continue;
|
|
60
85
|
}
|