@quolu/lattice 0.38.0 → 0.39.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 +22 -16
- package/README.md +11 -7
- package/bin/lattice-mcp.mjs +0 -0
- package/bin/lattice.mjs +1 -1
- package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +49 -1
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -0
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -0
- package/docs/schemas/lattice.todo_revision.v2.schema.json +41 -1
- package/package.json +3 -1
- package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.js +68 -16
- package/sensor/dist/extraction/tree-sitter.js.map +1 -1
- package/src/cli-help.mjs +7 -7
- package/src/project-cli.mjs +32 -15
- package/src/seam-cost.mjs +3 -3
- package/src/todo-cli.mjs +360 -218
- package/src/todo-contracts.mjs +46 -8
- package/src/todo-dashboard-registry.mjs +17 -3
- package/src/todo-gantt-html-independence.mjs +14 -3
- package/src/todo-gantt-html-shared.mjs +5 -2
- package/src/todo-gantt-layout.mjs +1 -1
- package/src/todo-gantt-scope.mjs +1 -1
- package/src/todo-migration.mjs +46 -9
- package/src/todo-revision.mjs +10 -8
- package/src/todo-status.mjs +3 -3
- package/src/todo-store.mjs +74 -31
package/src/todo-cli.mjs
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { createHash
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
3
|
import {
|
|
4
|
-
lstat, mkdir,
|
|
4
|
+
lstat, mkdir, readFile, realpath, writeFile,
|
|
5
5
|
} from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { parseTree } from 'jsonc-parser';
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
TODO_DESIGN_MEMO_PROMPT,
|
|
11
12
|
canonicalizeTodoArtifact,
|
|
12
13
|
digestTodoArtifact,
|
|
13
14
|
exactRecord,
|
|
14
15
|
isTodoDigest,
|
|
16
|
+
isTodoDesignMemo,
|
|
15
17
|
isTodoIdentifier,
|
|
16
18
|
isTodoRef,
|
|
17
19
|
todoSelfDigest,
|
|
18
20
|
validateEvidenceDescriptor,
|
|
19
21
|
} from './todo-contracts.mjs';
|
|
20
22
|
import { projectTodoChainV1 } from './todo-chain.mjs';
|
|
21
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
adoptTodoDashboardActivity,
|
|
25
|
+
ensureTodoDashboardActivity,
|
|
26
|
+
} from './todo-dashboard-registry.mjs';
|
|
22
27
|
import { resolveProjectIdentity } from './project-identity.mjs';
|
|
23
28
|
import { layoutTodoGantt } from './todo-gantt-layout.mjs';
|
|
24
29
|
import { TODO_GANTT_SCOPES } from './todo-gantt-scope.mjs';
|
|
@@ -26,7 +31,6 @@ import { loadTodoGanttPresentation } from './todo-gantt-presentation.mjs';
|
|
|
26
31
|
import { startTodoGanttLiveServer } from './todo-gantt-live.mjs';
|
|
27
32
|
import {
|
|
28
33
|
renderTodoGanttHtml,
|
|
29
|
-
TODO_GANTT_HTML_MAX_BYTES,
|
|
30
34
|
TODO_GANTT_RENDERER_VERSION,
|
|
31
35
|
} from './todo-gantt-html.mjs';
|
|
32
36
|
import { verifyNarrativeAnchors } from './todo-narrative-anchor.mjs';
|
|
@@ -35,6 +39,7 @@ import {
|
|
|
35
39
|
applyPhaseTodoRevision,
|
|
36
40
|
applyTodoRevision,
|
|
37
41
|
applyTodoRevisionSet,
|
|
42
|
+
buildTodoPlan,
|
|
38
43
|
createTodoStoreWriter,
|
|
39
44
|
TodoStoreError,
|
|
40
45
|
isPhaselessTodoPlanSchema,
|
|
@@ -54,7 +59,9 @@ import {
|
|
|
54
59
|
} from './todo-store.mjs';
|
|
55
60
|
import {
|
|
56
61
|
appendTodoExtraction,
|
|
62
|
+
compileTodoExtraction,
|
|
57
63
|
explainTodoExtraction,
|
|
64
|
+
TODO_EXTRACTION_SCHEMA_V3,
|
|
58
65
|
validateTodoExtraction,
|
|
59
66
|
} from './todo-migration.mjs';
|
|
60
67
|
import {
|
|
@@ -112,9 +119,6 @@ import {
|
|
|
112
119
|
} from './todo-note-store.mjs';
|
|
113
120
|
|
|
114
121
|
const CLI_ERROR_SCHEMA = 'lattice.cli_error.v2';
|
|
115
|
-
const DEFAULT_GANTT_REF = '.lattice/generated/gantt.html';
|
|
116
|
-
const GANTT_DESCRIPTOR_SUFFIX = '.status.json';
|
|
117
|
-
const MAX_GANTT_DESCRIPTOR_BYTES = 65_536;
|
|
118
122
|
const DEFAULT_GANTT_SCOPE = 'live';
|
|
119
123
|
const MAX_MIGRATION_INPUT_BYTES = 8_388_608;
|
|
120
124
|
const MAX_NOTE_INPUT_BYTES = 16_384;
|
|
@@ -138,7 +142,7 @@ const TODO_SCHEMA_COMMANDS = Object.freeze({
|
|
|
138
142
|
'revise-phase': {
|
|
139
143
|
title: 'lattice.phase_todo_revision.v3', file: 'lattice.phase_todo_revision.v3.schema.json',
|
|
140
144
|
},
|
|
141
|
-
migrate: { title: 'lattice.todo_extraction.
|
|
145
|
+
migrate: { title: 'lattice.todo_extraction.v3', file: 'lattice.todo_extraction.v3.schema.json' },
|
|
142
146
|
});
|
|
143
147
|
|
|
144
148
|
async function runTodoSchemaCommand(command, stdout) {
|
|
@@ -149,12 +153,6 @@ async function runTodoSchemaCommand(command, stdout) {
|
|
|
149
153
|
stdout.write(`${JSON.stringify(schema)}\n`);
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
function usageFailure(stderr, argv) {
|
|
153
|
-
const received = argv.length === 0 ? '(none)' : argv.join(' ').replace(/[\r\n]/gu, ' ');
|
|
154
|
-
stderr.write(`lattice todo: unsupported command or arguments: ${received}\n`);
|
|
155
|
-
return 2;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
156
|
function typedFailure(stderr, error) {
|
|
159
157
|
const payload = {
|
|
160
158
|
schema: CLI_ERROR_SCHEMA,
|
|
@@ -169,6 +167,18 @@ function typedFailure(stderr, error) {
|
|
|
169
167
|
return 1;
|
|
170
168
|
}
|
|
171
169
|
|
|
170
|
+
const TODO_COMMAND_NAMES = Object.freeze([
|
|
171
|
+
'status', 'show', 'note', 'bindings', 'independence', 'seam-profile', 'seam-proposal',
|
|
172
|
+
'verify', 'snapshot', 'gantt', 'dashboard', 'phase', 'migrate', 'start', 'block',
|
|
173
|
+
'unblock', 'done', 'reopen', 'evidence', 'revise', 'revise-phase', 'revise-set',
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
function typedArgumentFailure(stderr, code, message, detail) {
|
|
177
|
+
const payload = { schema: CLI_ERROR_SCHEMA, code, message, detail };
|
|
178
|
+
stderr.write(`${JSON.stringify(payload)}\n`);
|
|
179
|
+
return 2;
|
|
180
|
+
}
|
|
181
|
+
|
|
172
182
|
function resolveRepoRoot(cwd) {
|
|
173
183
|
try {
|
|
174
184
|
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
@@ -257,7 +267,7 @@ function hasDuplicateJsonKey(node) {
|
|
|
257
267
|
return false;
|
|
258
268
|
}
|
|
259
269
|
|
|
260
|
-
async function readMigrationInput(repoRoot, inputRef) {
|
|
270
|
+
async function readMigrationInput(repoRoot, inputRef, { requireValid = true } = {}) {
|
|
261
271
|
const canonicalRoot = await realpath(repoRoot);
|
|
262
272
|
const absolute = path.resolve(canonicalRoot, inputRef);
|
|
263
273
|
if (!within(canonicalRoot, absolute) || absolute === canonicalRoot) {
|
|
@@ -295,12 +305,30 @@ async function readMigrationInput(repoRoot, inputRef) {
|
|
|
295
305
|
try { extraction = JSON.parse(text); } catch {
|
|
296
306
|
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
297
307
|
}
|
|
308
|
+
if (!requireValid) return extraction;
|
|
309
|
+
if (extraction?.schema !== TODO_EXTRACTION_SCHEMA_V3) {
|
|
310
|
+
throw new TodoStoreError('DESIGN_MEMO_REQUIRED', 'todo_extraction_v3_required', undefined, {
|
|
311
|
+
design_memo_prompt: TODO_DESIGN_MEMO_PROMPT,
|
|
312
|
+
next_action: 'lattice todo migrate --schema --json',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
298
315
|
if (!validateTodoExtraction(extraction)) {
|
|
299
316
|
// 「schema_invalid」だけでは何のfieldがどう壊れているか分からない(ADR 0130の案内規律)。
|
|
300
317
|
// explainは可否判定を変えず、診断だけを追加する。
|
|
301
318
|
const explained = explainTodoExtraction(extraction);
|
|
302
|
-
|
|
303
|
-
|
|
319
|
+
const detail = explained.valid ? undefined : {
|
|
320
|
+
violation_kind: explained.reason,
|
|
321
|
+
pointer: explained.path,
|
|
322
|
+
violation_reason: explained.reason,
|
|
323
|
+
violation_path: explained.path,
|
|
324
|
+
...(explained.task_id === undefined ? {} : { task_id: explained.task_id }),
|
|
325
|
+
...(explained.expected === undefined ? {} : { expected: explained.expected }),
|
|
326
|
+
...(explained.actual === undefined ? {} : { actual: explained.actual }),
|
|
327
|
+
...(explained.path.endsWith('/design_memo')
|
|
328
|
+
? { design_memo_prompt: TODO_DESIGN_MEMO_PROMPT } : {}),
|
|
329
|
+
next_action: 'correct_the_reported_pointer_then_rerun_migrate_dry_run',
|
|
330
|
+
};
|
|
331
|
+
throw new TodoStoreError('INVALID_TODO_EXTRACTION', 'schema_invalid', undefined, detail);
|
|
304
332
|
}
|
|
305
333
|
return extraction;
|
|
306
334
|
}
|
|
@@ -515,6 +543,8 @@ async function mutate({
|
|
|
515
543
|
event: { kind, task_id: taskId, actor, payload: eventPayload },
|
|
516
544
|
});
|
|
517
545
|
const task = snapshot.tasks.find(({ task_id: current }) => current === event.task_id);
|
|
546
|
+
const authoredTask = plan.tasks.find(({ task_id: current }) => current === event.task_id);
|
|
547
|
+
if (authoredTask === undefined) throw new TypeError('mutation task content is missing');
|
|
518
548
|
// advisoryは呼び出し側(startTask)がstart用に既に組んでいればそれを尊重し、無ければ
|
|
519
549
|
// done時だけ終端監査の要否を調べる。block/unblock/reopenはnullのまま(既存挙動を変えない)。
|
|
520
550
|
// Phase状態はsnapshot(v1にはphasesキーが無い)でなく、appendTodoEventが別途返す
|
|
@@ -525,7 +555,7 @@ async function mutate({
|
|
|
525
555
|
throw new TypeError('start mutation requires note context');
|
|
526
556
|
}
|
|
527
557
|
const result = {
|
|
528
|
-
schema: includesNoteContext ? 'lattice.todo_mutation_result.
|
|
558
|
+
schema: includesNoteContext ? 'lattice.todo_mutation_result.v4' : 'lattice.todo_mutation_result.v2',
|
|
529
559
|
project_id: event.project_id,
|
|
530
560
|
plan_key: event.plan_key,
|
|
531
561
|
plan_version: event.plan_version,
|
|
@@ -537,13 +567,23 @@ async function mutate({
|
|
|
537
567
|
snapshot_digest: snapshot.snapshot_digest,
|
|
538
568
|
status: task.status,
|
|
539
569
|
advisory: resolvedAdvisory,
|
|
540
|
-
...(includesNoteContext ? {
|
|
570
|
+
...(includesNoteContext ? {
|
|
571
|
+
design_memo: designMemoProjection(authoredTask),
|
|
572
|
+
note_context: noteContext,
|
|
573
|
+
} : {}),
|
|
541
574
|
result_digest: '',
|
|
542
575
|
};
|
|
543
576
|
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
544
577
|
return result;
|
|
545
578
|
}
|
|
546
579
|
|
|
580
|
+
function designMemoProjection(task) {
|
|
581
|
+
if (typeof task.design_memo === 'string') {
|
|
582
|
+
return { status: 'available', markdown: task.design_memo, prompt: TODO_DESIGN_MEMO_PROMPT };
|
|
583
|
+
}
|
|
584
|
+
return { status: 'missing_legacy', markdown: null, prompt: TODO_DESIGN_MEMO_PROMPT };
|
|
585
|
+
}
|
|
586
|
+
|
|
547
587
|
/**
|
|
548
588
|
* 着手しようとしているtaskについて、記録済みの独立性から助言を組む(ADR 0128 Decision 5)。
|
|
549
589
|
*
|
|
@@ -931,7 +971,7 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
|
|
|
931
971
|
|
|
932
972
|
const imported = await appendTodoExtraction({ repoRoot, extraction });
|
|
933
973
|
const result = {
|
|
934
|
-
schema: 'lattice.todo_migrate_result.
|
|
974
|
+
schema: 'lattice.todo_migrate_result.v2',
|
|
935
975
|
project_id: imported.plan.project_id,
|
|
936
976
|
plan_key: imported.plan.plan_key,
|
|
937
977
|
plan_version: imported.plan.plan_version,
|
|
@@ -953,6 +993,230 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
|
|
|
953
993
|
// 留める。extraction経由のmigrateは常にphase無しplan(todo_plan.v2)を作るが、将来の
|
|
954
994
|
// 拡張に備えisPhaselessTodoPlanSchemaで動的に判定する。
|
|
955
995
|
terminal_audit_required: isPhaselessTodoPlanSchema(imported.plan.schema),
|
|
996
|
+
phase_guidance: isPhaselessTodoPlanSchema(imported.plan.schema) ? {
|
|
997
|
+
capability: 'acquire_phase',
|
|
998
|
+
preserves_completed_state: true,
|
|
999
|
+
schema_command: 'lattice todo revise-phase --schema --json',
|
|
1000
|
+
required_state_policy: 'acquire_phase',
|
|
1001
|
+
next_action: `lattice todo revise-phase --plan ${imported.plan.plan_key} --input <phase-revision.json>`,
|
|
1002
|
+
} : null,
|
|
1003
|
+
result_digest: '',
|
|
1004
|
+
};
|
|
1005
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
1006
|
+
return result;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function extractionFreshnessViolations(repoRoot, extraction) {
|
|
1010
|
+
let reachable;
|
|
1011
|
+
try {
|
|
1012
|
+
reachable = new Set(execFileSync('git', ['rev-list', '--objects', '--all'], {
|
|
1013
|
+
cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
1014
|
+
}).split('\n').filter(Boolean).map((line) => line.split(' ')[0]));
|
|
1015
|
+
} catch {
|
|
1016
|
+
return [{ code: 'source_reachability_unreadable', path: '/tasks', task_ids: [],
|
|
1017
|
+
next_action: 'git fsck --connectivity-only' }];
|
|
1018
|
+
}
|
|
1019
|
+
const violations = [];
|
|
1020
|
+
for (const [index, task] of (Array.isArray(extraction.tasks) ? extraction.tasks : []).entries()) {
|
|
1021
|
+
const source = task?.source;
|
|
1022
|
+
if (source === null || typeof source !== 'object'
|
|
1023
|
+
|| !['checked', 'unchecked'].includes(source.checkbox_state)
|
|
1024
|
+
|| task.narrative_ref !== source.origin_plan_ref) continue;
|
|
1025
|
+
const at = `/tasks/${index}/source`;
|
|
1026
|
+
if (!reachable.has(source.source_commit)) {
|
|
1027
|
+
violations.push({ code: 'source_commit_unreachable', path: `${at}/source_commit`,
|
|
1028
|
+
task_ids: [task.task_id].filter(isTodoIdentifier), next_action: 'commit_or_reference_source_commit' });
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
let markdown;
|
|
1032
|
+
try {
|
|
1033
|
+
markdown = execFileSync('git', ['show', `${source.source_commit}:${source.origin_plan_ref}`], {
|
|
1034
|
+
cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 8_388_608,
|
|
1035
|
+
});
|
|
1036
|
+
} catch {
|
|
1037
|
+
violations.push({ code: 'source_path_unreadable', path: `${at}/origin_plan_ref`,
|
|
1038
|
+
task_ids: [task.task_id].filter(isTodoIdentifier), next_action: 'correct_pinned_source_ref' });
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
const line = markdown.split('\n')[source.origin_line - 1];
|
|
1042
|
+
const checkbox = /^\s*[-*+]\s+\[([ xX])\]/u.exec(line ?? '');
|
|
1043
|
+
const actual = checkbox === null ? null : checkbox[1].toLowerCase() === 'x' ? 'checked' : 'unchecked';
|
|
1044
|
+
if (actual !== source.checkbox_state) {
|
|
1045
|
+
violations.push({ code: 'source_checkbox_state_mismatch', path: `${at}/checkbox_state`,
|
|
1046
|
+
task_ids: [task.task_id].filter(isTodoIdentifier), next_action: 'refresh_extraction_from_pinned_source' });
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return violations;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function extractionAuthoringViolations(extraction, now = new Date()) {
|
|
1053
|
+
const violations = [];
|
|
1054
|
+
const tasks = Array.isArray(extraction?.tasks) ? extraction.tasks : [];
|
|
1055
|
+
for (const [index, task] of tasks.entries()) {
|
|
1056
|
+
const taskIds = isTodoIdentifier(task?.task_id) ? [task.task_id] : [];
|
|
1057
|
+
if (task?.disposition === 'register_done'
|
|
1058
|
+
&& task?.completion?.done_mode !== 'historical_import') {
|
|
1059
|
+
violations.push({ code: 'enum_mismatch', path: `/tasks/${index}/completion/done_mode`,
|
|
1060
|
+
task_ids: taskIds, expected: 'historical_import',
|
|
1061
|
+
actual: task?.completion?.done_mode ?? null,
|
|
1062
|
+
next_action: 'use_historical_import_for_imported_completion' });
|
|
1063
|
+
}
|
|
1064
|
+
if (task?.migration_context !== null && typeof task?.migration_context === 'object'
|
|
1065
|
+
&& !Array.isArray(task.migration_context.notes)) {
|
|
1066
|
+
violations.push({ code: 'expected_array', path: `/tasks/${index}/migration_context/notes`,
|
|
1067
|
+
task_ids: taskIds, expected: 'array', actual: typeof task.migration_context.notes,
|
|
1068
|
+
next_action: 'replace_notes_with_a_bounded_string_array' });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (typeof extraction?.extraction_digest === 'string') {
|
|
1072
|
+
const expected = todoSelfDigest(extraction, 'extraction_digest');
|
|
1073
|
+
if (extraction.extraction_digest !== expected) {
|
|
1074
|
+
violations.push({ code: 'extraction_digest_mismatch', path: '/extraction_digest', task_ids: [],
|
|
1075
|
+
expected, actual: extraction.extraction_digest,
|
|
1076
|
+
next_action: 'replace_extraction_digest_with_expected' });
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
const edges = Array.isArray(extraction?.hard_dependencies) ? extraction.hard_dependencies : [];
|
|
1080
|
+
const edgeKey = (edge) => [edge?.from?.project_id, edge?.from?.plan_key, edge?.from?.task_id,
|
|
1081
|
+
edge?.to?.project_id, edge?.to?.plan_key, edge?.to?.task_id].join('\0');
|
|
1082
|
+
for (let index = 1; index < edges.length; index += 1) {
|
|
1083
|
+
if (edgeKey(edges[index - 1]) >= edgeKey(edges[index])) {
|
|
1084
|
+
violations.push({ code: 'array_not_sorted', path: `/hard_dependencies/${index}`, task_ids: [],
|
|
1085
|
+
expected: 'strict ascending order', sort_key: 'from.project_id, from.plan_key, from.task_id, '
|
|
1086
|
+
+ 'to.project_id, to.plan_key, to.task_id',
|
|
1087
|
+
next_action: 'sort_hard_dependencies_by_the_reported_key' });
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const registered = new Set(tasks.filter(({ disposition }) => typeof disposition === 'string'
|
|
1092
|
+
&& disposition.startsWith('register_')).map(({ task_id: taskId }) => taskId));
|
|
1093
|
+
const unresolvedLocal = (ref) => ref?.project_id === extraction?.project_id
|
|
1094
|
+
&& ref?.plan_key === extraction?.plan_key && !registered.has(ref?.task_id);
|
|
1095
|
+
for (const [index, edge] of edges.entries()) {
|
|
1096
|
+
for (const side of ['from', 'to']) {
|
|
1097
|
+
if (unresolvedLocal(edge?.[side])) {
|
|
1098
|
+
violations.push({ code: 'local_ref_unresolved', path: `/hard_dependencies/${index}/${side}`,
|
|
1099
|
+
task_ids: isTodoIdentifier(edge[side].task_id) ? [edge[side].task_id] : [],
|
|
1100
|
+
expected: 'task_id registered in this extraction', actual: edge[side].task_id ?? null,
|
|
1101
|
+
next_action: 'register_the_task_or_reference_its_actual_project_and_plan' });
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
const recordedAt = Date.parse(extraction?.recorded_at);
|
|
1106
|
+
const maxFutureSkewMs = 5 * 60 * 1_000;
|
|
1107
|
+
if (Number.isFinite(recordedAt) && recordedAt > now.valueOf() + maxFutureSkewMs) {
|
|
1108
|
+
violations.push({ code: 'future_clock_skew', path: '/recorded_at', task_ids: [],
|
|
1109
|
+
expected: { current_time: now.toISOString(), max_future_skew_ms: maxFutureSkewMs },
|
|
1110
|
+
actual: extraction.recorded_at, next_action: 'correct_recorded_at_and_recompute_extraction_digest' });
|
|
1111
|
+
}
|
|
1112
|
+
return violations;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
async function migrateDryRun({ repoRoot, inputRef, serializationReviewed = false }) {
|
|
1116
|
+
const extraction = await readMigrationInput(repoRoot, inputRef, { requireValid: false });
|
|
1117
|
+
const violations = [];
|
|
1118
|
+
const tasks = Array.isArray(extraction?.tasks) ? extraction.tasks : [];
|
|
1119
|
+
const missingMemoIds = tasks.filter((task) => !isTodoDesignMemo(task?.design_memo))
|
|
1120
|
+
.map((task) => task?.task_id).filter(isTodoIdentifier).slice(0, 64);
|
|
1121
|
+
if (extraction?.schema !== TODO_EXTRACTION_SCHEMA_V3 || missingMemoIds.length > 0) {
|
|
1122
|
+
violations.push({ code: 'design_memo_required', path: '/tasks', task_ids: missingMemoIds,
|
|
1123
|
+
prompt: TODO_DESIGN_MEMO_PROMPT, next_action: 'lattice todo migrate --schema --json' });
|
|
1124
|
+
}
|
|
1125
|
+
const schemaValid = extraction?.schema === TODO_EXTRACTION_SCHEMA_V3
|
|
1126
|
+
&& validateTodoExtraction(extraction);
|
|
1127
|
+
if (!schemaValid) {
|
|
1128
|
+
const explained = explainTodoExtraction(extraction);
|
|
1129
|
+
if (!explained.valid && !explained.path.endsWith('/design_memo')) {
|
|
1130
|
+
violations.push({ code: explained.reason, path: explained.path,
|
|
1131
|
+
task_ids: isTodoIdentifier(explained.task_id) ? [explained.task_id] : [],
|
|
1132
|
+
...(explained.expected === undefined ? {} : { expected: explained.expected }),
|
|
1133
|
+
...(explained.actual === undefined ? {} : { actual: explained.actual }),
|
|
1134
|
+
next_action: 'correct_extraction_input' });
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
for (const violation of extractionAuthoringViolations(extraction)) {
|
|
1138
|
+
if (!violations.some((entry) => entry.code === violation.code && entry.path === violation.path)) {
|
|
1139
|
+
violations.push(violation);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const unresolvedTaskIds = tasks
|
|
1143
|
+
.filter(({ disposition }) => disposition === 'unknown_requires_evidence')
|
|
1144
|
+
.map(({ task_id: taskId }) => taskId).filter(isTodoIdentifier);
|
|
1145
|
+
if (unresolvedTaskIds.length > 0) {
|
|
1146
|
+
violations.push({ code: 'unknown_requires_evidence', path: '/tasks',
|
|
1147
|
+
task_ids: unresolvedTaskIds.slice(0, 64), next_action: 'adjudicate_task_disposition' });
|
|
1148
|
+
}
|
|
1149
|
+
const registered = tasks.filter(({ disposition }) => typeof disposition === 'string'
|
|
1150
|
+
&& disposition.startsWith('register_'));
|
|
1151
|
+
if (registered.length === 0) {
|
|
1152
|
+
violations.push({ code: 'no_registered_tasks', path: '/tasks', task_ids: [],
|
|
1153
|
+
next_action: 'register_at_least_one_task' });
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
let dispatchShape = null;
|
|
1157
|
+
let plannedPlan = null;
|
|
1158
|
+
if (schemaValid && unresolvedTaskIds.length === 0 && registered.length > 0) {
|
|
1159
|
+
try {
|
|
1160
|
+
const compiled = compileTodoExtraction(extraction, repoRoot);
|
|
1161
|
+
plannedPlan = buildTodoPlan(compiled.plan);
|
|
1162
|
+
dispatchShape = computeTodoDispatchShapeForPlan({
|
|
1163
|
+
projectId: extraction.project_id, planKey: extraction.plan_key,
|
|
1164
|
+
taskIds: registered.map(({ task_id: taskId }) => taskId),
|
|
1165
|
+
hardDependencies: extraction.hard_dependencies, joins: extraction.joins,
|
|
1166
|
+
});
|
|
1167
|
+
try {
|
|
1168
|
+
assertTodoDispatchShapeReviewed({ shape: dispatchShape, reviewed: serializationReviewed });
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
violations.push({ code: error?.detail?.reason ?? 'plan_shape_too_serial', path: '/hard_dependencies',
|
|
1171
|
+
task_ids: error?.detail?.critical_path_task_ids ?? [],
|
|
1172
|
+
next_action: 'reconsider_parallel_seams_or_pass_serialization_reviewed' });
|
|
1173
|
+
}
|
|
1174
|
+
violations.push(...extractionFreshnessViolations(repoRoot, extraction));
|
|
1175
|
+
let manifestPresent = false;
|
|
1176
|
+
try {
|
|
1177
|
+
const stats = await lstat(path.join(repoRoot, '.lattice', 'todo', 'manifest.json'));
|
|
1178
|
+
manifestPresent = stats.isFile() && !stats.isSymbolicLink();
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
1181
|
+
}
|
|
1182
|
+
if (manifestPresent) {
|
|
1183
|
+
const store = await readTodoStore({ repoRoot });
|
|
1184
|
+
if (store.project_id !== extraction.project_id) {
|
|
1185
|
+
violations.push({ code: 'project_id_mismatch', path: '/project_id', task_ids: [],
|
|
1186
|
+
next_action: 'use_the_active_store_project_id' });
|
|
1187
|
+
}
|
|
1188
|
+
if (store.members.some(({ plan }) => plan.plan_key === extraction.plan_key)) {
|
|
1189
|
+
violations.push({ code: 'plan_key_already_exists', path: '/plan_key', task_ids: [],
|
|
1190
|
+
next_action: 'choose_a_new_plan_key_or_use_revision' });
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
violations.push({ code: error?.detail?.reason ?? 'topology_invalid', path: '/hard_dependencies',
|
|
1195
|
+
task_ids: [], next_action: 'correct_extraction_topology' });
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
const bounded = violations.slice(0, 64);
|
|
1199
|
+
const result = {
|
|
1200
|
+
schema: 'lattice.todo_migrate_dry_run_result.v1',
|
|
1201
|
+
valid: bounded.length === 0,
|
|
1202
|
+
project_id: isTodoIdentifier(extraction?.project_id) ? extraction.project_id : null,
|
|
1203
|
+
plan_key: isTodoIdentifier(extraction?.plan_key) ? extraction.plan_key : null,
|
|
1204
|
+
violations: bounded,
|
|
1205
|
+
overflow_count: Math.max(0, violations.length - bounded.length),
|
|
1206
|
+
planned: plannedPlan === null ? null : {
|
|
1207
|
+
plan_schema: plannedPlan.schema,
|
|
1208
|
+
task_count: registered.length,
|
|
1209
|
+
topology_digest: plannedPlan.topology_digest,
|
|
1210
|
+
dispatch_shape: {
|
|
1211
|
+
task_count: dispatchShape.task_count,
|
|
1212
|
+
critical_path_length: dispatchShape.critical_path_length,
|
|
1213
|
+
max_frontier_width: dispatchShape.max_frontier_width,
|
|
1214
|
+
serialization_ratio: dispatchShape.serialization_ratio,
|
|
1215
|
+
},
|
|
1216
|
+
},
|
|
1217
|
+
next_action: bounded.length === 0
|
|
1218
|
+
? `lattice todo migrate --input ${inputRef}${serializationReviewed ? ' --serialization-reviewed' : ''}`
|
|
1219
|
+
: 'correct_all_reported_violations_and_rerun_dry_run',
|
|
956
1220
|
result_digest: '',
|
|
957
1221
|
};
|
|
958
1222
|
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
@@ -998,6 +1262,25 @@ async function status({ repoRoot }) {
|
|
|
998
1262
|
return projectTodoStatus(await readTodoStore({ repoRoot }));
|
|
999
1263
|
}
|
|
1000
1264
|
|
|
1265
|
+
async function adoptDashboardRoot({ repoRoot, env }) {
|
|
1266
|
+
const store = await readTodoStoreStable({ repoRoot });
|
|
1267
|
+
const actor = mutationActor(env);
|
|
1268
|
+
const identity = await resolveProjectIdentity({ repoRoot, projectId: store.project_id, env });
|
|
1269
|
+
await adoptTodoDashboardActivity({
|
|
1270
|
+
repoRoot, projectId: store.project_id, displayName: identity.displayName,
|
|
1271
|
+
sessionId: actor.session, env,
|
|
1272
|
+
});
|
|
1273
|
+
const result = {
|
|
1274
|
+
schema: 'lattice.todo_dashboard_adopt_result.v1',
|
|
1275
|
+
project_id: store.project_id,
|
|
1276
|
+
adopted: true,
|
|
1277
|
+
next_action: `open /projects/${encodeURIComponent(store.project_id)}/ on the dynamic dashboard`,
|
|
1278
|
+
result_digest: '',
|
|
1279
|
+
};
|
|
1280
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
1281
|
+
return result;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1001
1284
|
async function todoDetail({ repoRoot, planKey, taskId }) {
|
|
1002
1285
|
const store = await readTodoStore({ repoRoot });
|
|
1003
1286
|
const [member] = selectMembers(store, planKey);
|
|
@@ -1008,11 +1291,12 @@ async function todoDetail({ repoRoot, planKey, taskId }) {
|
|
|
1008
1291
|
repoRoot, store, planKey, taskId: task.task_id,
|
|
1009
1292
|
});
|
|
1010
1293
|
const result = {
|
|
1011
|
-
schema: 'lattice.todo_detail_result.
|
|
1294
|
+
schema: 'lattice.todo_detail_result.v2',
|
|
1012
1295
|
project_id: store.project_id,
|
|
1013
1296
|
plan_key: planKey,
|
|
1014
1297
|
plan_version: member.plan.plan_version,
|
|
1015
1298
|
task,
|
|
1299
|
+
design_memo: designMemoProjection(task),
|
|
1016
1300
|
state,
|
|
1017
1301
|
note_context: context,
|
|
1018
1302
|
result_digest: '',
|
|
@@ -1762,107 +2046,6 @@ async function loadNarratives(store, repoRoot) {
|
|
|
1762
2046
|
return { narratives, bindings };
|
|
1763
2047
|
}
|
|
1764
2048
|
|
|
1765
|
-
async function resolveOutput(repoRoot, outputRef) {
|
|
1766
|
-
const canonicalRoot = await realpath(repoRoot);
|
|
1767
|
-
const absolute = path.resolve(canonicalRoot, outputRef);
|
|
1768
|
-
if (!within(canonicalRoot, absolute) || absolute === canonicalRoot) {
|
|
1769
|
-
throw new TodoStoreError('OUTPUT_PATH_INVALID', 'output_path_outside_repo', undefined, { output_ref: outputRef });
|
|
1770
|
-
}
|
|
1771
|
-
let cursor = path.dirname(absolute);
|
|
1772
|
-
const missing = [];
|
|
1773
|
-
while (cursor !== canonicalRoot) {
|
|
1774
|
-
try {
|
|
1775
|
-
const stats = await lstat(cursor);
|
|
1776
|
-
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
1777
|
-
throw new TodoStoreError('OUTPUT_PATH_INVALID', 'unsafe_output_parent', undefined, { output_ref: outputRef });
|
|
1778
|
-
}
|
|
1779
|
-
const resolved = await realpath(cursor);
|
|
1780
|
-
if (resolved !== cursor || !within(canonicalRoot, resolved)) {
|
|
1781
|
-
throw new TodoStoreError('OUTPUT_PATH_INVALID', 'output_parent_alias_or_escape', undefined, { output_ref: outputRef });
|
|
1782
|
-
}
|
|
1783
|
-
break;
|
|
1784
|
-
} catch (error) {
|
|
1785
|
-
if (error instanceof TodoStoreError) throw error;
|
|
1786
|
-
if (error?.code !== 'ENOENT') throw error;
|
|
1787
|
-
missing.push(cursor);
|
|
1788
|
-
cursor = path.dirname(cursor);
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
let target;
|
|
1792
|
-
try { target = await lstat(absolute); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
1793
|
-
if (target !== undefined && (target.isSymbolicLink() || !target.isFile())) {
|
|
1794
|
-
throw new TodoStoreError('OUTPUT_PATH_INVALID', 'unsafe_output_target', undefined, { output_ref: outputRef });
|
|
1795
|
-
}
|
|
1796
|
-
return { absolute, missing, target };
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
async function atomicWriteOutput(repoRoot, outputRef, html) {
|
|
1800
|
-
const { absolute } = await resolveOutput(repoRoot, outputRef);
|
|
1801
|
-
await mkdir(path.dirname(absolute), { recursive: true });
|
|
1802
|
-
// Re-resolve after mkdir so a raced alias cannot redirect the write.
|
|
1803
|
-
const checked = await resolveOutput(repoRoot, outputRef);
|
|
1804
|
-
const temporary = path.join(path.dirname(checked.absolute),
|
|
1805
|
-
`.${path.basename(checked.absolute)}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`);
|
|
1806
|
-
let handle;
|
|
1807
|
-
try {
|
|
1808
|
-
handle = await open(temporary, 'wx', 0o600);
|
|
1809
|
-
await handle.writeFile(html, 'utf8');
|
|
1810
|
-
await handle.sync();
|
|
1811
|
-
await handle.close();
|
|
1812
|
-
handle = null;
|
|
1813
|
-
await rename(temporary, checked.absolute);
|
|
1814
|
-
} finally {
|
|
1815
|
-
if (handle) await handle.close();
|
|
1816
|
-
await rm(temporary, { force: true });
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
function ganttDescriptorRef(outputRef) {
|
|
1821
|
-
return `${outputRef}${GANTT_DESCRIPTOR_SUFFIX}`;
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
// v2 records the scope the artifact was drawn at. Without it `gantt status`
|
|
1825
|
-
// would re-render at the default scope and report a `--scope all` artifact as
|
|
1826
|
-
// stale even though nothing in the store had moved.
|
|
1827
|
-
function validateGanttArtifactDescriptor(value) {
|
|
1828
|
-
return exactRecord(value, [
|
|
1829
|
-
'schema', 'project_id', 'output_ref', 'manifest_digest', 'renderer_version',
|
|
1830
|
-
'scope', 'html_digest', 'artifact_digest',
|
|
1831
|
-
]) && value.schema === 'lattice.todo_gantt_artifact.v2'
|
|
1832
|
-
&& isTodoIdentifier(value.project_id) && isTodoRef(value.output_ref)
|
|
1833
|
-
&& isTodoDigest(value.manifest_digest)
|
|
1834
|
-
&& typeof value.renderer_version === 'string'
|
|
1835
|
-
&& /^lattice\.todo_gantt_renderer\.v[1-9][0-9]*$/u.test(value.renderer_version)
|
|
1836
|
-
&& TODO_GANTT_SCOPES.includes(value.scope)
|
|
1837
|
-
&& isTodoDigest(value.html_digest) && isTodoDigest(value.artifact_digest)
|
|
1838
|
-
&& value.artifact_digest === todoSelfDigest(value, 'artifact_digest');
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
async function readOptionalOutput(repoRoot, outputRef, maxBytes) {
|
|
1842
|
-
const resolved = await resolveOutput(repoRoot, outputRef);
|
|
1843
|
-
if (resolved.target === undefined) return null;
|
|
1844
|
-
if (resolved.target.size > maxBytes) {
|
|
1845
|
-
throw new TodoStoreError('GANTT_ARTIFACT_INVALID', 'artifact_size_limit_exceeded', undefined,
|
|
1846
|
-
{ output_ref: outputRef });
|
|
1847
|
-
}
|
|
1848
|
-
return readFile(resolved.absolute);
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
function parseGanttDescriptor(bytes, descriptorRef) {
|
|
1852
|
-
let value;
|
|
1853
|
-
try {
|
|
1854
|
-
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
1855
|
-
value = JSON.parse(text);
|
|
1856
|
-
if (`${canonicalizeTodoArtifact(value)}\n` !== text || !validateGanttArtifactDescriptor(value)) {
|
|
1857
|
-
throw new Error('non-canonical descriptor');
|
|
1858
|
-
}
|
|
1859
|
-
} catch {
|
|
1860
|
-
throw new TodoStoreError('GANTT_ARTIFACT_INVALID', 'descriptor_invalid', undefined,
|
|
1861
|
-
{ descriptor_ref: descriptorRef });
|
|
1862
|
-
}
|
|
1863
|
-
return value;
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
2049
|
/**
|
|
1867
2050
|
* 図が描く全planについて独立性を投影する(ADR 0129 Decision 4)。
|
|
1868
2051
|
*
|
|
@@ -2091,86 +2274,11 @@ export async function renderTodoGanttForProject({
|
|
|
2091
2274
|
return { store, metadata, memberBindings, rendered };
|
|
2092
2275
|
}
|
|
2093
2276
|
|
|
2094
|
-
/**
|
|
2277
|
+
/** 公開配信面はToDo本体の設計メモを表示し、append-only作業記録は除外する。 */
|
|
2095
2278
|
export async function renderPublicTodoGanttForProject(options = {}) {
|
|
2096
2279
|
return renderTodoGanttForProject({ ...options, includeNotes: false });
|
|
2097
2280
|
}
|
|
2098
2281
|
|
|
2099
|
-
async function gantt({ repoRoot, outputRef, env, scope = DEFAULT_GANTT_SCOPE }) {
|
|
2100
|
-
const { store, metadata, memberBindings, rendered } = await renderTodoGanttForProject({
|
|
2101
|
-
repoRoot, env, scope,
|
|
2102
|
-
});
|
|
2103
|
-
await atomicWriteOutput(repoRoot, outputRef, rendered.html);
|
|
2104
|
-
const descriptor = { schema: 'lattice.todo_gantt_artifact.v2', project_id: store.project_id,
|
|
2105
|
-
output_ref: outputRef, manifest_digest: metadata.manifest_digest,
|
|
2106
|
-
renderer_version: TODO_GANTT_RENDERER_VERSION, scope, html_digest: rendered.html_digest,
|
|
2107
|
-
artifact_digest: '' };
|
|
2108
|
-
descriptor.artifact_digest = todoSelfDigest(descriptor, 'artifact_digest');
|
|
2109
|
-
await atomicWriteOutput(repoRoot, ganttDescriptorRef(outputRef),
|
|
2110
|
-
`${canonicalizeTodoArtifact(descriptor)}\n`);
|
|
2111
|
-
const result = {
|
|
2112
|
-
schema: 'lattice.todo_gantt_result.v1',
|
|
2113
|
-
project_id: store.project_id,
|
|
2114
|
-
output_ref: outputRef,
|
|
2115
|
-
scope,
|
|
2116
|
-
folded_task_count: metadata.folded_task_count,
|
|
2117
|
-
manifest_digest: metadata.manifest_digest,
|
|
2118
|
-
member_bindings: memberBindings,
|
|
2119
|
-
narrative_bindings_digest: metadata.narrative_bindings_digest,
|
|
2120
|
-
chain_digest: metadata.chain_digest,
|
|
2121
|
-
layout_digest: metadata.layout_digest,
|
|
2122
|
-
renderer_version: TODO_GANTT_RENDERER_VERSION,
|
|
2123
|
-
html_digest: rendered.html_digest,
|
|
2124
|
-
result_digest: '',
|
|
2125
|
-
};
|
|
2126
|
-
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
2127
|
-
return result;
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
async function ganttStatus({ repoRoot, outputRef, env }) {
|
|
2131
|
-
const descriptorRef = ganttDescriptorRef(outputRef);
|
|
2132
|
-
const [htmlBytes, descriptorBytes] = await Promise.all([
|
|
2133
|
-
readOptionalOutput(repoRoot, outputRef, TODO_GANTT_HTML_MAX_BYTES),
|
|
2134
|
-
readOptionalOutput(repoRoot, descriptorRef, MAX_GANTT_DESCRIPTOR_BYTES),
|
|
2135
|
-
]);
|
|
2136
|
-
if ((htmlBytes === null) !== (descriptorBytes === null)) {
|
|
2137
|
-
throw new TodoStoreError('GANTT_ARTIFACT_INVALID', 'artifact_pair_incomplete', undefined,
|
|
2138
|
-
{ output_ref: outputRef, descriptor_ref: descriptorRef });
|
|
2139
|
-
}
|
|
2140
|
-
if (htmlBytes === null) {
|
|
2141
|
-
const store = await readTodoStore({ repoRoot });
|
|
2142
|
-
const result = { schema: 'lattice.todo_gantt_status_result.v1', project_id: store.project_id,
|
|
2143
|
-
output_ref: outputRef, descriptor_ref: descriptorRef, artifact_status: 'missing',
|
|
2144
|
-
current_manifest_digest: store.manifest.manifest_digest, artifact_manifest_digest: null,
|
|
2145
|
-
html_digest: null, renderer_version: null, scope: null, result_digest: '' };
|
|
2146
|
-
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
2147
|
-
return result;
|
|
2148
|
-
}
|
|
2149
|
-
const descriptor = parseGanttDescriptor(descriptorBytes, descriptorRef);
|
|
2150
|
-
const htmlDigest = createHash('sha256').update(htmlBytes).digest('hex');
|
|
2151
|
-
if (descriptor.output_ref !== outputRef || descriptor.html_digest !== htmlDigest) {
|
|
2152
|
-
throw new TodoStoreError('GANTT_ARTIFACT_INVALID', 'artifact_digest_mismatch', undefined,
|
|
2153
|
-
{ output_ref: outputRef, descriptor_ref: descriptorRef });
|
|
2154
|
-
}
|
|
2155
|
-
// Re-render at the artifact's own scope: comparing a `--scope all` artifact
|
|
2156
|
-
// against a default-scope render would report a false `stale`.
|
|
2157
|
-
const current = await renderTodoGanttForProject({ repoRoot, env, scope: descriptor.scope });
|
|
2158
|
-
if (descriptor.project_id !== current.store.project_id) {
|
|
2159
|
-
throw new TodoStoreError('GANTT_ARTIFACT_INVALID', 'artifact_project_mismatch', undefined,
|
|
2160
|
-
{ output_ref: outputRef });
|
|
2161
|
-
}
|
|
2162
|
-
const artifactStatus = descriptor.renderer_version === TODO_GANTT_RENDERER_VERSION
|
|
2163
|
-
&& descriptor.manifest_digest === current.metadata.manifest_digest
|
|
2164
|
-
&& descriptor.html_digest === current.rendered.html_digest ? 'current' : 'stale';
|
|
2165
|
-
const result = { schema: 'lattice.todo_gantt_status_result.v1',
|
|
2166
|
-
project_id: current.store.project_id, output_ref: outputRef, descriptor_ref: descriptorRef,
|
|
2167
|
-
artifact_status: artifactStatus, current_manifest_digest: current.metadata.manifest_digest,
|
|
2168
|
-
artifact_manifest_digest: descriptor.manifest_digest, html_digest: descriptor.html_digest,
|
|
2169
|
-
renderer_version: descriptor.renderer_version, scope: descriptor.scope, result_digest: '' };
|
|
2170
|
-
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
2171
|
-
return result;
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
2282
|
async function serveGantt({ repoRoot, port, stdout, env, scope = DEFAULT_GANTT_SCOPE }) {
|
|
2175
2283
|
const initialStore = await readTodoStoreStable({ repoRoot });
|
|
2176
2284
|
const identity = await resolveProjectIdentity({ repoRoot, projectId: initialStore.project_id, env });
|
|
@@ -2189,7 +2297,10 @@ async function serveGantt({ repoRoot, port, stdout, env, scope = DEFAULT_GANTT_S
|
|
|
2189
2297
|
repoRoot, store: await readTodoStoreStable({ repoRoot }),
|
|
2190
2298
|
}),
|
|
2191
2299
|
});
|
|
2192
|
-
const result = { schema: 'lattice.todo_gantt_live_result.
|
|
2300
|
+
const result = { schema: 'lattice.todo_gantt_live_result.v3', project_id: live.projectId,
|
|
2301
|
+
resource_scope: 'project', selection_scope: scope,
|
|
2302
|
+
included_plan_keys: initialStore.members.map(({ descriptor }) => descriptor.plan_key).sort(),
|
|
2303
|
+
media_type: 'text/html; charset=utf-8', dynamic: true,
|
|
2193
2304
|
host: live.host, port: live.port, project_path: live.projectPath,
|
|
2194
2305
|
url: live.url, events_url: live.eventsUrl };
|
|
2195
2306
|
stdout.write(`${JSON.stringify(result)}\n`);
|
|
@@ -2283,10 +2394,18 @@ async function verify({ repoRoot, requestedPlanKey }) {
|
|
|
2283
2394
|
? sourceInventory.active.length : null,
|
|
2284
2395
|
excluded_tombstone_count: reconciled
|
|
2285
2396
|
? sourceInventory.excluded_tombstones.length : null,
|
|
2397
|
+
reconciliation_guidance: {
|
|
2398
|
+
meaning: 'source_inventory_verification_state',
|
|
2399
|
+
lifecycle_blocked: false,
|
|
2400
|
+
dashboard_visibility_blocked: false,
|
|
2401
|
+
schema_command: reconciled ? null : 'lattice todo revise --schema --json',
|
|
2402
|
+
next_action: reconciled ? null
|
|
2403
|
+
: `lattice todo revise --plan ${member.descriptor.plan_key} --input <revision.json>`,
|
|
2404
|
+
},
|
|
2286
2405
|
};
|
|
2287
2406
|
});
|
|
2288
2407
|
const result = {
|
|
2289
|
-
schema: 'lattice.todo_verify_result.
|
|
2408
|
+
schema: 'lattice.todo_verify_result.v3',
|
|
2290
2409
|
project_id: store.project_id,
|
|
2291
2410
|
requested_plan_key: requestedPlanKey,
|
|
2292
2411
|
verified_members: verifiedMembers,
|
|
@@ -2327,6 +2446,14 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2327
2446
|
throw new TypeError('runTodoCli optionsが不正');
|
|
2328
2447
|
}
|
|
2329
2448
|
|
|
2449
|
+
if (argv[0] === 'migrate' && argv[1] === '--input'
|
|
2450
|
+
&& typeof argv[2] === 'string' && path.isAbsolute(argv[2])) {
|
|
2451
|
+
return typedArgumentFailure(stderr, 'INPUT_OUTSIDE_REPOSITORY', 'absolute_input_path_rejected', {
|
|
2452
|
+
argument: '--input', expected: 'repo-relative path', actual: 'absolute path',
|
|
2453
|
+
next_action: 'place_the_input_inside_the_repository_and_pass_a_repo_relative_path',
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2330
2457
|
// `--schema --json`はstoreを読まない決定的な出力(`plan create --schema`と同じ規律)。
|
|
2331
2458
|
// 通常dispatchより前に処理し、repoRoot解決やdashboard daemon起動を経由させない。
|
|
2332
2459
|
if (argv.length === 3 && argv[1] === '--schema' && argv[2] === '--json'
|
|
@@ -2345,6 +2472,9 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2345
2472
|
if ((argv.length === 1 && argv[0] === 'status')
|
|
2346
2473
|
|| (argv.length === 2 && argv[0] === 'status' && argv[1] === '--json')) {
|
|
2347
2474
|
action = (repoRoot) => status({ repoRoot });
|
|
2475
|
+
} else if (argv.length === 3 && argv[0] === 'dashboard'
|
|
2476
|
+
&& argv[1] === 'adopt' && argv[2] === '--json') {
|
|
2477
|
+
action = (repoRoot) => adoptDashboardRoot({ repoRoot, env });
|
|
2348
2478
|
} else if (argv.length === 6 && argv[0] === 'show'
|
|
2349
2479
|
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
2350
2480
|
&& argv[3] === '--task' && isTodoIdentifier(argv[4]) && argv[5] === '--json') {
|
|
@@ -2432,22 +2562,6 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2432
2562
|
} else if (argv.length === 4 && argv[0] === 'snapshot' && argv[1] === '--rebuild'
|
|
2433
2563
|
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])) {
|
|
2434
2564
|
action = (repoRoot) => rebuildSnapshot({ repoRoot, planKey: argv[3] });
|
|
2435
|
-
} else if (argv.length === 1 && argv[0] === 'gantt') {
|
|
2436
|
-
action = (repoRoot) => gantt({ repoRoot, outputRef: DEFAULT_GANTT_REF, env });
|
|
2437
|
-
} else if (argv.length === 3 && argv[0] === 'gantt' && argv[1] === '--scope'
|
|
2438
|
-
&& TODO_GANTT_SCOPES.includes(argv[2])) {
|
|
2439
|
-
action = (repoRoot) => gantt({ repoRoot, outputRef: DEFAULT_GANTT_REF, env, scope: argv[2] });
|
|
2440
|
-
} else if (argv.length === 3 && argv[0] === 'gantt' && argv[1] === '--out'
|
|
2441
|
-
&& isTodoRef(argv[2])) {
|
|
2442
|
-
action = (repoRoot) => gantt({ repoRoot, outputRef: argv[2], env });
|
|
2443
|
-
} else if (argv.length === 5 && argv[0] === 'gantt' && argv[1] === '--out'
|
|
2444
|
-
&& isTodoRef(argv[2]) && argv[3] === '--scope' && TODO_GANTT_SCOPES.includes(argv[4])) {
|
|
2445
|
-
action = (repoRoot) => gantt({ repoRoot, outputRef: argv[2], env, scope: argv[4] });
|
|
2446
|
-
} else if (argv.length === 2 && argv[0] === 'gantt' && argv[1] === 'status') {
|
|
2447
|
-
action = (repoRoot) => ganttStatus({ repoRoot, outputRef: DEFAULT_GANTT_REF, env });
|
|
2448
|
-
} else if (argv.length === 4 && argv[0] === 'gantt' && argv[1] === 'status'
|
|
2449
|
-
&& argv[2] === '--out' && isTodoRef(argv[3])) {
|
|
2450
|
-
action = (repoRoot) => ganttStatus({ repoRoot, outputRef: argv[3], env });
|
|
2451
2565
|
} else if (argv.length === 4 && argv[0] === 'gantt' && argv[1] === 'serve'
|
|
2452
2566
|
&& argv[2] === '--port' && /^(?:0|[1-9][0-9]{0,4})$/u.test(argv[3])
|
|
2453
2567
|
&& Number(argv[3]) <= 65_535) {
|
|
@@ -2459,6 +2573,19 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2459
2573
|
action = (repoRoot) => serveGantt({
|
|
2460
2574
|
repoRoot, port: Number(argv[3]), stdout, env, scope: argv[5],
|
|
2461
2575
|
});
|
|
2576
|
+
} else if (argv[0] === 'gantt') {
|
|
2577
|
+
action = () => {
|
|
2578
|
+
throw new TodoStoreError('STATIC_GANTT_RETIRED', 'dynamic_dashboard_only', undefined, {
|
|
2579
|
+
next_action: 'lattice todo status --json',
|
|
2580
|
+
});
|
|
2581
|
+
};
|
|
2582
|
+
} else if ((argv.length === 5 || argv.length === 6) && argv[0] === 'migrate'
|
|
2583
|
+
&& argv[1] === '--input' && isTodoRef(argv[2])
|
|
2584
|
+
&& argv[3] === '--dry-run' && argv[4] === '--json'
|
|
2585
|
+
&& (argv.length === 5 || argv[5] === '--serialization-reviewed')) {
|
|
2586
|
+
action = (repoRoot) => migrateDryRun({
|
|
2587
|
+
repoRoot, inputRef: argv[2], serializationReviewed: argv.length === 6,
|
|
2588
|
+
});
|
|
2462
2589
|
} else if ((argv.length === 3 || argv.length === 4) && argv[0] === 'migrate' && argv[1] === '--input'
|
|
2463
2590
|
&& isTodoRef(argv[2]) && (argv.length === 3 || argv[3] === '--serialization-reviewed')) {
|
|
2464
2591
|
action = (repoRoot) => migrate({
|
|
@@ -2553,12 +2680,27 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2553
2680
|
action = (repoRoot) => mutate({ repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
2554
2681
|
kind: 'reopen', payload: { reason: argv[6], override_reason: overrideReason }, evidenceRef: null });
|
|
2555
2682
|
}
|
|
2556
|
-
if (action === null)
|
|
2683
|
+
if (action === null) {
|
|
2684
|
+
const command = typeof argv[0] === 'string' ? argv[0] : null;
|
|
2685
|
+
if (command !== null && !TODO_COMMAND_NAMES.includes(command)) {
|
|
2686
|
+
return typedArgumentFailure(stderr, 'UNKNOWN_SUBCOMMAND', 'todo_subcommand_unknown', {
|
|
2687
|
+
command, available_commands: TODO_COMMAND_NAMES,
|
|
2688
|
+
next_action: 'lattice todo --help',
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
return typedArgumentFailure(stderr, 'INVALID_ARGUMENTS', 'todo_arguments_invalid', {
|
|
2692
|
+
command, next_action: command === null ? 'lattice todo --help' : `lattice todo ${command} --help`,
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2557
2695
|
|
|
2558
2696
|
try {
|
|
2559
2697
|
const repoRoot = resolveRepoRoot(cwd);
|
|
2560
2698
|
const manualServe = argv[0] === 'gantt' && argv[1] === 'serve';
|
|
2561
|
-
|
|
2699
|
+
const dashboardAdopt = argv[0] === 'dashboard' && argv[1] === 'adopt';
|
|
2700
|
+
const migrationDryRun = argv[0] === 'migrate' && argv.includes('--dry-run');
|
|
2701
|
+
if (!manualServe && !dashboardAdopt && !migrationDryRun) {
|
|
2702
|
+
await ensureActiveProjectDashboard({ repoRoot, env });
|
|
2703
|
+
}
|
|
2562
2704
|
const result = await action(repoRoot);
|
|
2563
2705
|
if (result !== null) stdout.write(`${JSON.stringify(result)}\n`);
|
|
2564
2706
|
return 0;
|