@quolu/lattice 0.37.0 → 0.38.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 +15 -0
- package/README.md +16 -0
- package/bin/lattice-dashboard.mjs +2 -2
- package/package.json +1 -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 -0
- package/src/seam-cost.mjs +3 -3
- package/src/todo-cli.mjs +241 -8
- package/src/todo-contracts.mjs +66 -0
- package/src/todo-gantt-html-independence.mjs +29 -3
- package/src/todo-gantt-html-style.mjs +6 -0
- package/src/todo-gantt-html.mjs +28 -6
- package/src/todo-note-store.mjs +492 -0
package/src/cli-help.mjs
CHANGED
|
@@ -61,6 +61,8 @@ Commands:
|
|
|
61
61
|
|
|
62
62
|
Read commands:
|
|
63
63
|
status [--json]
|
|
64
|
+
show --plan <key> --task <id> --json # 個別ToDoと最新bounded note contextを追加操作なしで返す
|
|
65
|
+
note list --plan <key> [--task <id>] --json # note全履歴の診断面(通常read/startでは不要)
|
|
64
66
|
bindings [--plan <key>] [--json] # compile_binding付きTaskをTODO identityつきで投影する
|
|
65
67
|
independence [--plan <key>] [--json] # readyを検証済み並列・要直列・未検査へ分けて投影する
|
|
66
68
|
seam-profile --plan <key> --file <path> [--json] # 係争fileの切断コスト内訳を投影する(read-only)
|
|
@@ -73,6 +75,8 @@ Read commands:
|
|
|
73
75
|
phase status --plan <key>
|
|
74
76
|
|
|
75
77
|
Write commands:
|
|
78
|
+
note --plan <key> --task <id> (--message <text>|--input <file>)
|
|
79
|
+
# ToDoへ作業継続に必要な方針・調査結果・注意をappend-onlyで追記する
|
|
76
80
|
migrate --input <extraction.json> [--serialization-reviewed]
|
|
77
81
|
# 既存storeへplanを追加する(plan createは空store初期化専用)。
|
|
78
82
|
# 依存グラフがほぼ一直線なら一度突き返し、再考後の --serialization-reviewed で通す
|
|
@@ -168,6 +172,9 @@ const SUBCOMMAND_USAGE = Object.freeze({
|
|
|
168
172
|
'run seam resolve': 'run seam resolve --run .lattice/runs/<id> --finding <digest> --input <seam-request.json>',
|
|
169
173
|
'event verify': 'event verify --run .lattice/runs/<id>',
|
|
170
174
|
'todo status': 'todo status [--json]',
|
|
175
|
+
'todo show': 'todo show --plan <key> --task <id> --json',
|
|
176
|
+
'todo note': 'todo note --plan <key> --task <id> (--message <text>|--input <file>) | list --plan <key> [--task <id>] --json',
|
|
177
|
+
'todo note list': 'todo note list --plan <key> [--task <id>] --json',
|
|
171
178
|
'todo bindings': 'todo bindings [--plan <key>] [--json]',
|
|
172
179
|
'todo independence': 'todo independence [--plan <key>] [--json] | compile --plan <key> --input <file> | witness migrate --plan <key>',
|
|
173
180
|
'todo seam-profile': 'todo seam-profile --plan <key> --file <path> [--json]',
|
package/src/seam-cost.mjs
CHANGED
|
@@ -244,9 +244,9 @@ export function classifySeamCost({
|
|
|
244
244
|
// 盲点の申告(計画の不変条件4)。見えていないものを「共有なし」と言わない。
|
|
245
245
|
// 3文字未満の名前(i, db等)はloop/parameterのnoiseが支配的なので辺にしない。
|
|
246
246
|
value_ref_name_filter: 'names-under-3-chars-invisible-in-edges',
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
write_distinction: 'ts-js-
|
|
247
|
+
// TS/JS/ArkTS/Go/Python/Javaはwasmとkernelの両経路で同じwrite判定を持つ。
|
|
248
|
+
// それ以外のvalue-ref対応言語はmetadata.writeが未配線なので、範囲を明記する。
|
|
249
|
+
write_distinction: 'ts-js-arkts-go-python-java-all-routes',
|
|
250
250
|
imports_analysis: 'esm-only',
|
|
251
251
|
callees_truncated: [...new Set(truncatedSymbols)].sort(compareText),
|
|
252
252
|
body_missing: [...new Set(bodyMissing)].sort(compareText),
|
package/src/todo-cli.mjs
CHANGED
|
@@ -104,6 +104,12 @@ import {
|
|
|
104
104
|
parseTodoSourceRef, todoLegacyReconciliationDigest, validatePhaseTodoRevision,
|
|
105
105
|
validateTodoRevision, validateTodoRevisionSet,
|
|
106
106
|
} from './todo-revision.mjs';
|
|
107
|
+
import {
|
|
108
|
+
appendTodoNote,
|
|
109
|
+
readTodoNoteContext,
|
|
110
|
+
readTodoNoteContextsForPlan,
|
|
111
|
+
readTodoNoteEvents,
|
|
112
|
+
} from './todo-note-store.mjs';
|
|
107
113
|
|
|
108
114
|
const CLI_ERROR_SCHEMA = 'lattice.cli_error.v2';
|
|
109
115
|
const DEFAULT_GANTT_REF = '.lattice/generated/gantt.html';
|
|
@@ -111,6 +117,7 @@ const GANTT_DESCRIPTOR_SUFFIX = '.status.json';
|
|
|
111
117
|
const MAX_GANTT_DESCRIPTOR_BYTES = 65_536;
|
|
112
118
|
const DEFAULT_GANTT_SCOPE = 'live';
|
|
113
119
|
const MAX_MIGRATION_INPUT_BYTES = 8_388_608;
|
|
120
|
+
const MAX_NOTE_INPUT_BYTES = 16_384;
|
|
114
121
|
const ACTOR_ENV_KEYS = Object.freeze([
|
|
115
122
|
'LATTICE_TODO_ACTOR_HOST',
|
|
116
123
|
'LATTICE_TODO_ACTOR_SESSION',
|
|
@@ -183,6 +190,43 @@ function selectMembers(store, requestedPlanKey) {
|
|
|
183
190
|
return [member];
|
|
184
191
|
}
|
|
185
192
|
|
|
193
|
+
function selectNoteTask(member, requestedTaskId) {
|
|
194
|
+
const exact = member.plan.tasks.find(({ task_id: taskId }) => taskId === requestedTaskId);
|
|
195
|
+
if (exact !== undefined) return exact;
|
|
196
|
+
const folded = requestedTaskId.toLowerCase();
|
|
197
|
+
const matches = member.plan.tasks.filter(({ task_id: taskId }) => taskId.toLowerCase() === folded);
|
|
198
|
+
if (matches.length !== 1) {
|
|
199
|
+
throw new TodoStoreError('NOTE_TASK_NOT_FOUND', 'note_task_not_active', undefined, {
|
|
200
|
+
plan_key: member.plan.plan_key, task_id: requestedTaskId,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return matches[0];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function readNoteTextInput(repoRoot, inputRef) {
|
|
207
|
+
if (!isTodoRef(inputRef)) throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_outside_repo');
|
|
208
|
+
const canonicalRoot = await realpath(repoRoot);
|
|
209
|
+
const absolute = path.resolve(canonicalRoot, inputRef);
|
|
210
|
+
if (!within(canonicalRoot, absolute) || absolute === canonicalRoot) {
|
|
211
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_outside_repo');
|
|
212
|
+
}
|
|
213
|
+
let metadata;
|
|
214
|
+
try { metadata = await lstat(absolute); } catch {
|
|
215
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_missing');
|
|
216
|
+
}
|
|
217
|
+
if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.size > MAX_NOTE_INPUT_BYTES) {
|
|
218
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'unsafe_or_oversized_note_input');
|
|
219
|
+
}
|
|
220
|
+
const resolved = await realpath(absolute);
|
|
221
|
+
if (resolved !== absolute || !within(canonicalRoot, resolved)) {
|
|
222
|
+
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_alias_or_escape');
|
|
223
|
+
}
|
|
224
|
+
const bytes = await readFile(resolved);
|
|
225
|
+
if (bytes.length > MAX_NOTE_INPUT_BYTES) throw new TodoStoreError('INPUT_TOO_LARGE', 'note_input_too_large');
|
|
226
|
+
try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes); }
|
|
227
|
+
catch { throw new TodoStoreError('INPUT_UNREADABLE', 'note_input_invalid_utf8'); }
|
|
228
|
+
}
|
|
229
|
+
|
|
186
230
|
function taskRef(plan, taskId) {
|
|
187
231
|
return { project_id: plan.project_id, plan_key: plan.plan_key, task_id: taskId };
|
|
188
232
|
}
|
|
@@ -455,6 +499,7 @@ function terminalAuditDoneAdvisory(plan, phases) {
|
|
|
455
499
|
|
|
456
500
|
async function mutate({
|
|
457
501
|
repoRoot, env, planKey, taskId, kind, payload, evidenceRef, advisory = null,
|
|
502
|
+
noteContext = null,
|
|
458
503
|
}) {
|
|
459
504
|
const actor = mutationActor(env);
|
|
460
505
|
const evidence = evidenceRef === null ? null : await readEvidenceInput(repoRoot, evidenceRef);
|
|
@@ -475,8 +520,12 @@ async function mutate({
|
|
|
475
520
|
// Phase状態はsnapshot(v1にはphasesキーが無い)でなく、appendTodoEventが別途返す
|
|
476
521
|
// 導出ビュー`phases`から読む。
|
|
477
522
|
const resolvedAdvisory = advisory ?? (kind === 'done' ? terminalAuditDoneAdvisory(plan, phases) : null);
|
|
523
|
+
const includesNoteContext = kind === 'start';
|
|
524
|
+
if (includesNoteContext && noteContext === null) {
|
|
525
|
+
throw new TypeError('start mutation requires note context');
|
|
526
|
+
}
|
|
478
527
|
const result = {
|
|
479
|
-
schema: 'lattice.todo_mutation_result.v2',
|
|
528
|
+
schema: includesNoteContext ? 'lattice.todo_mutation_result.v3' : 'lattice.todo_mutation_result.v2',
|
|
480
529
|
project_id: event.project_id,
|
|
481
530
|
plan_key: event.plan_key,
|
|
482
531
|
plan_version: event.plan_version,
|
|
@@ -488,6 +537,7 @@ async function mutate({
|
|
|
488
537
|
snapshot_digest: snapshot.snapshot_digest,
|
|
489
538
|
status: task.status,
|
|
490
539
|
advisory: resolvedAdvisory,
|
|
540
|
+
...(includesNoteContext ? { note_context: noteContext } : {}),
|
|
491
541
|
result_digest: '',
|
|
492
542
|
};
|
|
493
543
|
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
@@ -655,13 +705,33 @@ async function startTask({
|
|
|
655
705
|
});
|
|
656
706
|
}
|
|
657
707
|
}
|
|
658
|
-
const
|
|
708
|
+
const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
|
|
709
|
+
if (member === undefined) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active');
|
|
710
|
+
const taskMatches = member.plan.tasks.filter(({ task_id: candidate }) => (
|
|
711
|
+
candidate.toLowerCase() === taskId.toLowerCase()
|
|
712
|
+
));
|
|
713
|
+
if (taskMatches.length === 0) {
|
|
714
|
+
throw new TodoStoreError('TASK_NOT_FOUND', 'task_not_found', undefined, {
|
|
715
|
+
requested_task_id: taskId,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
if (taskMatches.length > 1) {
|
|
719
|
+
throw new TodoStoreError('TASK_ID_AMBIGUOUS', 'task_id_case_ambiguous', undefined, {
|
|
720
|
+
requested_task_id: taskId,
|
|
721
|
+
matching_task_ids: taskMatches.map(({ task_id: candidate }) => candidate).sort(),
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
const resolvedTaskId = readyTask?.task_id ?? taskMatches[0].task_id;
|
|
725
|
+
// noteはjournal appendより前に読む。読めなければstart自体を止め、部分進行を作らない。
|
|
726
|
+
const { context: noteContext } = await readTodoNoteContext({
|
|
727
|
+
repoRoot, store, planKey, taskId: resolvedTaskId,
|
|
728
|
+
});
|
|
659
729
|
// 助言はjournalへ書く前に確定させる。計算できないならstart自体を止める。
|
|
660
730
|
const advisory = await startAdvisory({
|
|
661
731
|
repoRoot, store, projection, planKey, taskId: resolvedTaskId,
|
|
662
732
|
});
|
|
663
733
|
return mutate({ repoRoot, env, planKey, taskId: resolvedTaskId, kind: 'start',
|
|
664
|
-
payload: { override_reason: overrideReason }, evidenceRef: null, advisory });
|
|
734
|
+
payload: { override_reason: overrideReason }, evidenceRef: null, advisory, noteContext });
|
|
665
735
|
}
|
|
666
736
|
|
|
667
737
|
function validatePhaseDecisionInput(value, outcome) {
|
|
@@ -928,6 +998,93 @@ async function status({ repoRoot }) {
|
|
|
928
998
|
return projectTodoStatus(await readTodoStore({ repoRoot }));
|
|
929
999
|
}
|
|
930
1000
|
|
|
1001
|
+
async function todoDetail({ repoRoot, planKey, taskId }) {
|
|
1002
|
+
const store = await readTodoStore({ repoRoot });
|
|
1003
|
+
const [member] = selectMembers(store, planKey);
|
|
1004
|
+
const task = selectNoteTask(member, taskId);
|
|
1005
|
+
const state = member.tasks.find(({ task_id: current }) => current === task.task_id);
|
|
1006
|
+
if (state === undefined) throw new TodoStoreError('STORE_INCONSISTENT', 'task_state_missing');
|
|
1007
|
+
const { context } = await readTodoNoteContext({
|
|
1008
|
+
repoRoot, store, planKey, taskId: task.task_id,
|
|
1009
|
+
});
|
|
1010
|
+
const result = {
|
|
1011
|
+
schema: 'lattice.todo_detail_result.v1',
|
|
1012
|
+
project_id: store.project_id,
|
|
1013
|
+
plan_key: planKey,
|
|
1014
|
+
plan_version: member.plan.plan_version,
|
|
1015
|
+
task,
|
|
1016
|
+
state,
|
|
1017
|
+
note_context: context,
|
|
1018
|
+
result_digest: '',
|
|
1019
|
+
};
|
|
1020
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
1021
|
+
return result;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function appendNote({ repoRoot, env, planKey, taskId, message, inputRef, supersedes }) {
|
|
1025
|
+
const store = await readTodoStore({ repoRoot });
|
|
1026
|
+
const [member] = selectMembers(store, planKey);
|
|
1027
|
+
const task = selectNoteTask(member, taskId);
|
|
1028
|
+
const body = inputRef === null ? message : await readNoteTextInput(repoRoot, inputRef);
|
|
1029
|
+
const projectedBeforeAppend = await readTodoNoteContext({
|
|
1030
|
+
repoRoot, store, planKey, taskId: task.task_id,
|
|
1031
|
+
});
|
|
1032
|
+
const event = await appendTodoNote({
|
|
1033
|
+
repoRoot,
|
|
1034
|
+
projectId: store.project_id,
|
|
1035
|
+
planKey,
|
|
1036
|
+
planVersion: member.plan.plan_version,
|
|
1037
|
+
taskId: task.task_id,
|
|
1038
|
+
actor: mutationActor(env),
|
|
1039
|
+
recordedAt: new Date().toISOString(),
|
|
1040
|
+
body,
|
|
1041
|
+
supersedes,
|
|
1042
|
+
eligibleSupersedes: projectedBeforeAppend.history.map(({ event_digest: digest }) => digest),
|
|
1043
|
+
});
|
|
1044
|
+
const { context } = await readTodoNoteContext({ repoRoot, store, planKey, taskId: task.task_id });
|
|
1045
|
+
const result = {
|
|
1046
|
+
schema: 'lattice.todo_note_append_result.v1',
|
|
1047
|
+
project_id: store.project_id,
|
|
1048
|
+
plan_key: planKey,
|
|
1049
|
+
task_id: task.task_id,
|
|
1050
|
+
event,
|
|
1051
|
+
note_context: context,
|
|
1052
|
+
result_digest: '',
|
|
1053
|
+
};
|
|
1054
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
1055
|
+
return result;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
async function listNotes({ repoRoot, planKey, taskId }) {
|
|
1059
|
+
const store = await readTodoStore({ repoRoot });
|
|
1060
|
+
const [member] = selectMembers(store, planKey);
|
|
1061
|
+
const chain = await readTodoNoteEvents({ repoRoot, planKey });
|
|
1062
|
+
let notes = chain.events;
|
|
1063
|
+
let archived = [];
|
|
1064
|
+
let resolvedTaskId = null;
|
|
1065
|
+
if (taskId !== null) {
|
|
1066
|
+
const task = selectNoteTask(member, taskId);
|
|
1067
|
+
resolvedTaskId = task.task_id;
|
|
1068
|
+
const projected = await readTodoNoteContext({
|
|
1069
|
+
repoRoot, store, planKey, taskId: task.task_id,
|
|
1070
|
+
});
|
|
1071
|
+
notes = projected.history;
|
|
1072
|
+
archived = projected.archived;
|
|
1073
|
+
}
|
|
1074
|
+
const result = {
|
|
1075
|
+
schema: 'lattice.todo_note_list_result.v1',
|
|
1076
|
+
project_id: store.project_id,
|
|
1077
|
+
plan_key: planKey,
|
|
1078
|
+
requested_task_id: resolvedTaskId,
|
|
1079
|
+
notes,
|
|
1080
|
+
archived,
|
|
1081
|
+
note_head_digest: chain.head_digest,
|
|
1082
|
+
result_digest: '',
|
|
1083
|
+
};
|
|
1084
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
1085
|
+
return result;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
931
1088
|
async function bindings({ repoRoot, requestedPlanKey }) {
|
|
932
1089
|
return projectTodoBindings(await readTodoStore({ repoRoot }), { requestedPlanKey });
|
|
933
1090
|
}
|
|
@@ -1721,12 +1878,13 @@ function parseGanttDescriptor(bytes, descriptorRef) {
|
|
|
1721
1878
|
* 一つの関数だけが組む。
|
|
1722
1879
|
*/
|
|
1723
1880
|
export async function ganttLiveHeadDigest({ repoRoot, store }) {
|
|
1724
|
-
const [independence, seamProposals] = await Promise.all([
|
|
1881
|
+
const [independence, seamProposals, noteHeads] = await Promise.all([
|
|
1725
1882
|
independenceForGantt({ repoRoot, store }),
|
|
1726
1883
|
seamProposalsForGantt({ repoRoot, store }),
|
|
1884
|
+
noteHeadsForGantt({ repoRoot, store }),
|
|
1727
1885
|
]);
|
|
1728
1886
|
return digestTodoArtifact({
|
|
1729
|
-
schema: 'lattice.todo_gantt_live_head.
|
|
1887
|
+
schema: 'lattice.todo_gantt_live_head.v2',
|
|
1730
1888
|
manifest_digest: store.manifest.manifest_digest,
|
|
1731
1889
|
independence: independence === null ? null : independence.map((entry) => ({
|
|
1732
1890
|
plan_key: entry.plan_key,
|
|
@@ -1738,9 +1896,46 @@ export async function ganttLiveHeadDigest({ repoRoot, store }) {
|
|
|
1738
1896
|
coverage: entry.coverage,
|
|
1739
1897
|
projection_digest: digestTodoArtifact(entry),
|
|
1740
1898
|
})),
|
|
1899
|
+
note_heads: noteHeads,
|
|
1741
1900
|
});
|
|
1742
1901
|
}
|
|
1743
1902
|
|
|
1903
|
+
async function noteHeadsForGantt({ repoRoot, store }) {
|
|
1904
|
+
const bindings = [];
|
|
1905
|
+
for (const member of store.members) {
|
|
1906
|
+
try {
|
|
1907
|
+
const chain = await readTodoNoteEvents({ repoRoot, planKey: member.plan.plan_key });
|
|
1908
|
+
bindings.push({ plan_key: member.plan.plan_key, note_head_digest: chain.head_digest, error: null });
|
|
1909
|
+
} catch (error) {
|
|
1910
|
+
if (typeof error?.code !== 'string' || !error.code.startsWith('NOTE_')) throw error;
|
|
1911
|
+
bindings.push({ plan_key: member.plan.plan_key, note_head_digest: null, error: error.code });
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
return bindings;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
async function notesForGantt({ repoRoot, store }) {
|
|
1918
|
+
const contexts = [];
|
|
1919
|
+
const warnings = [];
|
|
1920
|
+
const headBindings = [];
|
|
1921
|
+
for (const member of store.members) {
|
|
1922
|
+
try {
|
|
1923
|
+
const projected = await readTodoNoteContextsForPlan({
|
|
1924
|
+
repoRoot, store, planKey: member.plan.plan_key,
|
|
1925
|
+
});
|
|
1926
|
+
contexts.push(...projected.contexts);
|
|
1927
|
+
headBindings.push({
|
|
1928
|
+
plan_key: member.plan.plan_key, note_head_digest: projected.note_head_digest,
|
|
1929
|
+
});
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
if (typeof error?.code !== 'string' || !error.code.startsWith('NOTE_')) throw error;
|
|
1932
|
+
warnings.push({ plan_key: member.plan.plan_key, code: error.code, message: error.message });
|
|
1933
|
+
headBindings.push({ plan_key: member.plan.plan_key, note_head_digest: null });
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
return { contexts, warnings, headBindings };
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1744
1939
|
async function independenceForGantt({ repoRoot, store }) {
|
|
1745
1940
|
const frontier = computeReadyFrontier(store);
|
|
1746
1941
|
const status = projectTodoStatus(store);
|
|
@@ -1829,7 +2024,7 @@ async function seamProposalsForGantt({ repoRoot, store }) {
|
|
|
1829
2024
|
|
|
1830
2025
|
export async function renderTodoGanttForProject({
|
|
1831
2026
|
repoRoot, stable = false, displayName = null, env = process.env, readModel = null,
|
|
1832
|
-
scope = DEFAULT_GANTT_SCOPE,
|
|
2027
|
+
scope = DEFAULT_GANTT_SCOPE, includeNotes = true,
|
|
1833
2028
|
}) {
|
|
1834
2029
|
const store = readModel
|
|
1835
2030
|
?? (stable ? await readTodoStoreStable({ repoRoot }) : await readTodoStore({ repoRoot }));
|
|
@@ -1839,9 +2034,11 @@ export async function renderTodoGanttForProject({
|
|
|
1839
2034
|
const presentation = await loadTodoGanttPresentation({ repoRoot, readModel: store });
|
|
1840
2035
|
const topology = mergedTopology(store);
|
|
1841
2036
|
const chain = projectTodoChainV1(topology);
|
|
1842
|
-
const [independence, seamProposals] = await Promise.all([
|
|
2037
|
+
const [independence, seamProposals, notes] = await Promise.all([
|
|
1843
2038
|
independenceForGantt({ repoRoot, store }),
|
|
1844
2039
|
seamProposalsForGantt({ repoRoot, store }),
|
|
2040
|
+
includeNotes ? notesForGantt({ repoRoot, store })
|
|
2041
|
+
: { contexts: null, warnings: [], headBindings: [] },
|
|
1845
2042
|
]);
|
|
1846
2043
|
const layout = layoutTodoGantt(store, chain, { scope, independence, seamProposals });
|
|
1847
2044
|
// When the diagram hides history, the page also carries the full diagram so
|
|
@@ -1875,6 +2072,7 @@ export async function renderTodoGanttForProject({
|
|
|
1875
2072
|
presentation_digest: presentation.presentation_digest,
|
|
1876
2073
|
chain_digest: digestTodoArtifact(chain),
|
|
1877
2074
|
layout_digest: digestTodoArtifact(layout),
|
|
2075
|
+
note_bindings_digest: digestTodoArtifact(notes.headBindings),
|
|
1878
2076
|
renderer_version: TODO_GANTT_RENDERER_VERSION,
|
|
1879
2077
|
project_display_name: identity.displayName,
|
|
1880
2078
|
folded_task_count: layout.scope.folded_task_count,
|
|
@@ -1887,10 +2085,17 @@ export async function renderTodoGanttForProject({
|
|
|
1887
2085
|
anchorOutcomes,
|
|
1888
2086
|
presentation,
|
|
1889
2087
|
metadata,
|
|
2088
|
+
noteContexts: notes.contexts,
|
|
2089
|
+
noteWarnings: notes.warnings,
|
|
1890
2090
|
});
|
|
1891
2091
|
return { store, metadata, memberBindings, rendered };
|
|
1892
2092
|
}
|
|
1893
2093
|
|
|
2094
|
+
/** 公開配信面は呼び出し側の既定値忘れに依存せず、常にnote本文を除外する。 */
|
|
2095
|
+
export async function renderPublicTodoGanttForProject(options = {}) {
|
|
2096
|
+
return renderTodoGanttForProject({ ...options, includeNotes: false });
|
|
2097
|
+
}
|
|
2098
|
+
|
|
1894
2099
|
async function gantt({ repoRoot, outputRef, env, scope = DEFAULT_GANTT_SCOPE }) {
|
|
1895
2100
|
const { store, metadata, memberBindings, rendered } = await renderTodoGanttForProject({
|
|
1896
2101
|
repoRoot, env, scope,
|
|
@@ -1975,7 +2180,7 @@ async function serveGantt({ repoRoot, port, stdout, env, scope = DEFAULT_GANTT_S
|
|
|
1975
2180
|
port,
|
|
1976
2181
|
render: async () => {
|
|
1977
2182
|
const store = await readTodoStoreStable({ repoRoot });
|
|
1978
|
-
const { rendered } = await
|
|
2183
|
+
const { rendered } = await renderPublicTodoGanttForProject({
|
|
1979
2184
|
repoRoot, stable: true, displayName: identity.displayName, scope, readModel: store,
|
|
1980
2185
|
});
|
|
1981
2186
|
return { html: rendered.html, head_digest: await ganttLiveHeadDigest({ repoRoot, store }) };
|
|
@@ -2020,6 +2225,10 @@ async function ensureActiveProjectDashboard({ repoRoot, env }) {
|
|
|
2020
2225
|
async function verify({ repoRoot, requestedPlanKey }) {
|
|
2021
2226
|
const store = await readTodoStore({ repoRoot });
|
|
2022
2227
|
const members = selectMembers(store, requestedPlanKey);
|
|
2228
|
+
// note chainはlifecycle storeと独立だが、verifyは両方を検査する。破損を空扱いしない。
|
|
2229
|
+
for (const member of members) {
|
|
2230
|
+
await readTodoNoteEvents({ repoRoot, planKey: member.plan.plan_key });
|
|
2231
|
+
}
|
|
2023
2232
|
const verifiedSourceInventories = new Map();
|
|
2024
2233
|
for (const member of members) {
|
|
2025
2234
|
const unverified = member.tasks.find((task) => task.evidence_unverified);
|
|
@@ -2136,6 +2345,10 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2136
2345
|
if ((argv.length === 1 && argv[0] === 'status')
|
|
2137
2346
|
|| (argv.length === 2 && argv[0] === 'status' && argv[1] === '--json')) {
|
|
2138
2347
|
action = (repoRoot) => status({ repoRoot });
|
|
2348
|
+
} else if (argv.length === 6 && argv[0] === 'show'
|
|
2349
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
2350
|
+
&& argv[3] === '--task' && isTodoIdentifier(argv[4]) && argv[5] === '--json') {
|
|
2351
|
+
action = (repoRoot) => todoDetail({ repoRoot, planKey: argv[2], taskId: argv[4] });
|
|
2139
2352
|
} else if ((argv.length === 1 && argv[0] === 'bindings')
|
|
2140
2353
|
|| (argv.length === 2 && argv[0] === 'bindings' && argv[1] === '--json')) {
|
|
2141
2354
|
action = (repoRoot) => bindings({ repoRoot, requestedPlanKey: null });
|
|
@@ -2143,6 +2356,26 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2143
2356
|
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
2144
2357
|
&& (argv.length === 3 || argv[3] === '--json')) {
|
|
2145
2358
|
action = (repoRoot) => bindings({ repoRoot, requestedPlanKey: argv[2] });
|
|
2359
|
+
} else if ((argv.length === 7 || argv.length === 9) && argv[0] === 'note'
|
|
2360
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
2361
|
+
&& argv[3] === '--task' && isTodoIdentifier(argv[4])
|
|
2362
|
+
&& ['--message', '--input'].includes(argv[5])
|
|
2363
|
+
&& ((argv[5] === '--message' && argv[6].length > 0)
|
|
2364
|
+
|| (argv[5] === '--input' && isTodoRef(argv[6])))
|
|
2365
|
+
&& (argv.length === 7 || (argv[7] === '--supersedes' && isTodoDigest(argv[8])))) {
|
|
2366
|
+
action = (repoRoot) => appendNote({
|
|
2367
|
+
repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
2368
|
+
message: argv[5] === '--message' ? argv[6] : null,
|
|
2369
|
+
inputRef: argv[5] === '--input' ? argv[6] : null,
|
|
2370
|
+
supersedes: argv[8] ?? null,
|
|
2371
|
+
});
|
|
2372
|
+
} else if (argv.length === 5 && argv[0] === 'note' && argv[1] === 'list'
|
|
2373
|
+
&& argv[2] === '--plan' && isTodoIdentifier(argv[3]) && argv[4] === '--json') {
|
|
2374
|
+
action = (repoRoot) => listNotes({ repoRoot, planKey: argv[3], taskId: null });
|
|
2375
|
+
} else if (argv.length === 7 && argv[0] === 'note' && argv[1] === 'list'
|
|
2376
|
+
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
2377
|
+
&& argv[4] === '--task' && isTodoIdentifier(argv[5]) && argv[6] === '--json') {
|
|
2378
|
+
action = (repoRoot) => listNotes({ repoRoot, planKey: argv[3], taskId: argv[5] });
|
|
2146
2379
|
} else if (argv.length === 5 && argv[0] === 'independence' && argv[1] === 'witness'
|
|
2147
2380
|
&& argv[2] === 'migrate' && argv[3] === '--plan' && isTodoIdentifier(argv[4])) {
|
|
2148
2381
|
action = (repoRoot) => independenceWitnessMigrate({ repoRoot, planKey: argv[4] });
|
package/src/todo-contracts.mjs
CHANGED
|
@@ -9,6 +9,8 @@ export const TODO_EVENT_KINDS = Object.freeze([
|
|
|
9
9
|
// 新しいevent schema版は作らない。
|
|
10
10
|
'phase_close_unaudited',
|
|
11
11
|
]);
|
|
12
|
+
export const TODO_NOTE_EVENT_SCHEMA = 'lattice.todo_note_event.v1';
|
|
13
|
+
export const TODO_NOTE_CONTEXT_SCHEMA = 'lattice.todo_note_context.v1';
|
|
12
14
|
export const TODO_LIMITS = Object.freeze({
|
|
13
15
|
tasksPerPlan: 512,
|
|
14
16
|
edgesPerPlan: 2_048,
|
|
@@ -16,11 +18,14 @@ export const TODO_LIMITS = Object.freeze({
|
|
|
16
18
|
journalSegmentBytes: 1_048_576,
|
|
17
19
|
snapshotBytes: 8_388_608,
|
|
18
20
|
narrativeSectionBytes: 262_144,
|
|
21
|
+
noteBodyBytes: 16_384,
|
|
22
|
+
noteContextBytes: 65_536,
|
|
19
23
|
});
|
|
20
24
|
|
|
21
25
|
const DIGEST = /^[0-9a-f]{64}$/;
|
|
22
26
|
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
23
27
|
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
28
|
+
const NOTE_FORBIDDEN_CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
|
|
24
29
|
|
|
25
30
|
export const isTodoDigest = (value) => typeof value === 'string' && DIGEST.test(value);
|
|
26
31
|
export const isTodoIdentifier = (value) => typeof value === 'string' && IDENTIFIER.test(value);
|
|
@@ -89,6 +94,67 @@ export function todoSelfDigest(value, field) {
|
|
|
89
94
|
return digestTodoArtifact(projection);
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
const noteBody = (value) => typeof value === 'string' && value.length > 0
|
|
98
|
+
&& Buffer.byteLength(value, 'utf8') <= TODO_LIMITS.noteBodyBytes
|
|
99
|
+
&& !NOTE_FORBIDDEN_CONTROL.test(value);
|
|
100
|
+
|
|
101
|
+
/** lifecycle journalとは独立したtask note event v1を検証する。 */
|
|
102
|
+
export function validateTodoNoteEvent(value) {
|
|
103
|
+
try {
|
|
104
|
+
return exactRecord(value, [
|
|
105
|
+
'schema', 'project_id', 'plan_key', 'task_id', 'plan_version', 'sequence',
|
|
106
|
+
'previous_digest', 'actor', 'recorded_at', 'body', 'supersedes', 'event_digest',
|
|
107
|
+
]) && value.schema === TODO_NOTE_EVENT_SCHEMA
|
|
108
|
+
&& isTodoIdentifier(value.project_id) && isTodoIdentifier(value.plan_key)
|
|
109
|
+
&& isTodoIdentifier(value.task_id) && isTodoIdentifier(value.plan_version)
|
|
110
|
+
&& Number.isSafeInteger(value.sequence) && value.sequence >= 1
|
|
111
|
+
&& (value.sequence === 1 ? value.previous_digest === null : isTodoDigest(value.previous_digest))
|
|
112
|
+
&& actor(value.actor) && isStrictTodoTimestamp(value.recorded_at) && noteBody(value.body)
|
|
113
|
+
&& (value.supersedes === null || isTodoDigest(value.supersedes))
|
|
114
|
+
&& isTodoDigest(value.event_digest) && value.supersedes !== value.event_digest
|
|
115
|
+
&& value.event_digest === todoSelfDigest(value, 'event_digest');
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function noteContextEntry(value) {
|
|
122
|
+
return exactRecord(value, [
|
|
123
|
+
'event_digest', 'origin_plan_version', 'origin_task_id', 'actor', 'recorded_at',
|
|
124
|
+
'body', 'supersedes', 'superseded_by', 'correction_state',
|
|
125
|
+
]) && isTodoDigest(value.event_digest) && isTodoIdentifier(value.origin_plan_version)
|
|
126
|
+
&& isTodoIdentifier(value.origin_task_id) && actor(value.actor)
|
|
127
|
+
&& isStrictTodoTimestamp(value.recorded_at) && noteBody(value.body)
|
|
128
|
+
&& (value.supersedes === null || isTodoDigest(value.supersedes))
|
|
129
|
+
&& (value.superseded_by === null || isTodoDigest(value.superseded_by))
|
|
130
|
+
&& ['current', 'superseded'].includes(value.correction_state)
|
|
131
|
+
&& ((value.correction_state === 'current' && value.superseded_by === null)
|
|
132
|
+
|| (value.correction_state === 'superseded' && isTodoDigest(value.superseded_by)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 個別ToDo詳細とstart結果へ必ず同梱するbounded note contextを検証する。 */
|
|
136
|
+
export function validateTodoNoteContext(value) {
|
|
137
|
+
try {
|
|
138
|
+
if (!exactRecord(value, [
|
|
139
|
+
'schema', 'project_id', 'plan_key', 'task_id', 'notes', 'note_head_digest',
|
|
140
|
+
'overflow_count', 'full_history_command', 'context_digest',
|
|
141
|
+
]) || value.schema !== TODO_NOTE_CONTEXT_SCHEMA
|
|
142
|
+
|| !isTodoIdentifier(value.project_id) || !isTodoIdentifier(value.plan_key)
|
|
143
|
+
|| !isTodoIdentifier(value.task_id) || !Array.isArray(value.notes)
|
|
144
|
+
|| value.notes.length > TODO_LIMITS.tasksPerPlan || !value.notes.every(noteContextEntry)
|
|
145
|
+
|| !(value.note_head_digest === null || isTodoDigest(value.note_head_digest))
|
|
146
|
+
|| !isNonNegativeSafeInteger(value.overflow_count)
|
|
147
|
+
|| value.full_history_command !== `lattice todo note list --plan ${value.plan_key} --task ${value.task_id} --json`
|
|
148
|
+
|| !isTodoDigest(value.context_digest)
|
|
149
|
+
|| value.context_digest !== todoSelfDigest(value, 'context_digest')) return false;
|
|
150
|
+
if ((value.notes.length === 0) !== (value.note_head_digest === null)) return false;
|
|
151
|
+
return value.notes.reduce((bytes, note) => bytes + Buffer.byteLength(note.body, 'utf8'), 0)
|
|
152
|
+
<= TODO_LIMITS.noteContextBytes;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
92
158
|
const nullableDigest = (value) => value === null || isTodoDigest(value);
|
|
93
159
|
const nullableText = (value) => value === null || (typeof value === 'string' && value.length > 0 && Buffer.byteLength(value) <= 16_384);
|
|
94
160
|
const actor = (value) => exactRecord(value, ['host', 'session', 'agent'])
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { DOCUMENT_STATUS, SEVERABILITY_LABEL, escapeHtmlAttribute, escapeHtmlText, foldIndex, planActivity, presentationLookup, refKey, renderPhaseProgress, renderRelationList, renderSeamProposalOverview, renderTaskIndex, statusMarkup, taskReference } from './todo-gantt-html-shared.mjs';
|
|
2
|
+
import { renderTodoMarkdown } from './todo-markdown-renderer.mjs';
|
|
3
|
+
|
|
4
|
+
function renderNoteContext(context) {
|
|
5
|
+
if (context === null) {
|
|
6
|
+
return '<section class="work-log"><h2>作業記録</h2><p class="note-warning">作業記録を読み取れません。概要の読取警告を確認してください。</p></section>';
|
|
7
|
+
}
|
|
8
|
+
if (context.notes.length === 0) {
|
|
9
|
+
return '<section class="work-log"><h2>作業記録</h2><p>記録はありません。</p></section>';
|
|
10
|
+
}
|
|
11
|
+
const entries = context.notes.map((note) => {
|
|
12
|
+
const rendered = renderTodoMarkdown(note.body);
|
|
13
|
+
const filtering = rendered.discarded.length === 0 ? ''
|
|
14
|
+
: '<p class="note-warning">安全上表示できない要素を除外しました。</p>';
|
|
15
|
+
const correction = note.correction_state === 'superseded'
|
|
16
|
+
? `訂正済み(後継 ${note.superseded_by.slice(0, 12)}…)` : '現行';
|
|
17
|
+
return `<article class="work-log-entry"><header><span>${escapeHtmlText(note.recorded_at)}</span><span>${escapeHtmlText(correction)}</span></header><div class="work-log-body">${rendered.html}</div>${filtering}<p class="work-log-origin">来歴: ${escapeHtmlText(note.origin_plan_version)}/${escapeHtmlText(note.origin_task_id)}・記録者 ${escapeHtmlText(`${note.actor.host}/${note.actor.agent}`)}</p></article>`;
|
|
18
|
+
}).join('');
|
|
19
|
+
const overflow = context.overflow_count === 0 ? ''
|
|
20
|
+
: `<p class="note-warning">ほか ${context.overflow_count}件は上限のため省略。全履歴: <code>${escapeHtmlText(context.full_history_command)}</code></p>`;
|
|
21
|
+
return `<section class="work-log"><h2>作業記録</h2>${entries}${overflow}<p class="work-log-head">note head: <code>${escapeHtmlText(context.note_head_digest ?? 'none')}</code></p></section>`;
|
|
22
|
+
}
|
|
2
23
|
|
|
3
24
|
/**
|
|
4
25
|
* 図の外が語るための独立性要約を、plan単位の投影から引ける形へ畳む(ADR 0129 Decision 3)。
|
|
@@ -63,7 +84,9 @@ export function renderIndependenceNote(ref, node, summary) {
|
|
|
63
84
|
return `<p class="readiness-note"><strong>並列可否:</strong> 要直列です。</p><ul class="independence-conflicts">${items}</ul>`;
|
|
64
85
|
}
|
|
65
86
|
|
|
66
|
-
export function renderRightPane(
|
|
87
|
+
export function renderRightPane(
|
|
88
|
+
sections, layout, presentation, readModel, notesEnabled = false, noteWarnings = [],
|
|
89
|
+
) {
|
|
67
90
|
const lookup = presentationLookup(presentation);
|
|
68
91
|
const sectionByKey = new Map(sections.map((section) => [refKey(section.ref), section]));
|
|
69
92
|
const nodeByKey = new Map(layout.nodes.map((node) => [refKey(node.ref), node]));
|
|
@@ -104,7 +127,9 @@ export function renderRightPane(sections, layout, presentation, readModel) {
|
|
|
104
127
|
const dispatchSummary = `${readyHeadline}${independenceNote}`;
|
|
105
128
|
const activeLinks = active.length === 0 ? '<p>作業中の工程はありません。</p>'
|
|
106
129
|
: `<ul class="active-list">${active.map((section) => `<li><button type="button" data-select-node-key="${escapeHtmlAttribute(refKey(section.ref))}">${escapeHtmlText(taskReference(section, lookup))} — ${escapeHtmlText(section.task.title)}</button></li>`).join('')}</ul>`;
|
|
107
|
-
const
|
|
130
|
+
const noteWarningMarkup = noteWarnings.length === 0 ? ''
|
|
131
|
+
: `<section class="gantt-warning"><h2>作業記録の読取警告</h2><ul>${noteWarnings.map((warning) => `<li><code>${escapeHtmlText(warning.plan_key)}</code>: <strong>${escapeHtmlText(warning.code)}</strong> — ${escapeHtmlText(warning.message)}</li>`).join('')}</ul></section>`;
|
|
132
|
+
const overview = `<section class="right-overview" data-right-panel="overview"><h1>工程を選択してください</h1><p>左の依存工程図から工程を選ぶと、題名・状態・前提・後続を表示します。</p><div class="status-summary"><span>☐ 未着手 ${counts.pending}</span><span>▶ 作業中 ${counts['in-progress']}</span><span>✅ 完了 ${counts.done}</span><span>⛔ ブロック中 ${counts.blocked}</span></div>${noteWarningMarkup}${dispatchSummary}${renderSeamProposalOverview(layout)}${renderPhaseProgress(readModel)}<h2>作業中</h2>${activeLinks}</section>`;
|
|
108
133
|
const details = sections.map((section) => {
|
|
109
134
|
const key = refKey(section.ref);
|
|
110
135
|
const node = nodeByKey.get(key);
|
|
@@ -126,7 +151,8 @@ export function renderRightPane(sections, layout, presentation, readModel) {
|
|
|
126
151
|
// Say it plainly when the reader will not find this ToDo on the diagram.
|
|
127
152
|
const foldedNote = !folds.has(key) ? ''
|
|
128
153
|
: '<p class="fold-note">完走済みのため図には描いていません。図に出すには <code>lattice todo gantt --scope all</code> を実行してください。</p>';
|
|
129
|
-
|
|
154
|
+
const workLog = notesEnabled ? renderNoteContext(section.noteContext) : '';
|
|
155
|
+
return `<article class="task-detail" data-detail-key="${escapeHtmlAttribute(key)}" hidden><header><span class="detail-status status-${escapeHtmlAttribute(section.state.status)}">${escapeHtmlText(status.mark)} ${escapeHtmlText(status.label)}</span><span class="detail-reference">${escapeHtmlText(taskReference(section, lookup))}</span></header><h1>${escapeHtmlText(section.task.title)}</h1><p class="detail-category"><strong>カテゴリ:</strong> ${escapeHtmlText(category)}</p>${categoryDescription}<p><strong>正規ID:</strong> <code>${escapeHtmlText(`${section.ref.plan_key}/${section.task.task_id}`)}</code></p>${blockedReason}${readiness}${independenceNote}${foldedNote}<section><h2>前提工程</h2>${renderRelationList(incoming.get(key), sectionByKey, lookup, '登録済みの前提工程はありません。', folds)}</section><section><h2>後続工程</h2>${renderRelationList(outgoing.get(key), sectionByKey, lookup, '登録済みの後続工程はありません。', folds)}</section>${workLog}<p class="anchor-status">${escapeHtmlText(anchorText)}</p><details class="task-diagnostics"><summary>開発者向け診断</summary><dl><dt>canonical ref</dt><dd><code>${escapeHtmlText(`${section.ref.project_id}/${section.ref.plan_key}/${section.task.task_id}`)}</code></dd><dt>anchor</dt><dd>${escapeHtmlText(section.anchorOutcome.anchored ? 'verified' : section.anchorOutcome.reason)}</dd></dl></details></article>`;
|
|
130
156
|
}).join('');
|
|
131
157
|
const taskIndex = renderTaskIndex(sections, lookup, folds, planActivity(readModel));
|
|
132
158
|
return `<div class="right-toolbar"><button type="button" data-show-overview>概要</button><button type="button" data-show-selected hidden>選択工程へ戻る</button><button type="button" data-show-task-index>全工程一覧</button></div><div class="right-content">${overview}<div data-right-panel="details" hidden>${details}</div><section class="task-index" data-right-panel="task-index" hidden><h1>全工程</h1><p>Latticeに登録された全工程を現在の状態とともに表示しています。planは動いているものを最終活動の新しい順で上に、完走したものを古い順で下にまとめ、plan内は登録順です。</p>${taskIndex}</section></div>`;
|
|
@@ -56,6 +56,12 @@ body{display:grid;grid-template-rows:minmax(0,1fr);height:100vh;margin:0;backgro
|
|
|
56
56
|
.active-list,.relation-list{margin:0;padding:0;list-style:none}.active-list li+li,.relation-list li+li{margin-top:8px}
|
|
57
57
|
.active-list button{width:100%}.anchor-status,.readiness-note,.category-description,.relation-empty{color:var(--text-secondary)}
|
|
58
58
|
.task-detail>header{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:8px}.detail-status,.detail-reference{font-size:12px;font-weight:600}.detail-reference{color:var(--text-secondary)}
|
|
59
|
+
.work-log{margin-top:24px;padding-top:1px;border-top:1px solid var(--border)}
|
|
60
|
+
.work-log-entry{margin-top:10px;padding:12px;border:1px solid var(--border);border-left:4px solid var(--accent);background:var(--surface-2)}
|
|
61
|
+
.work-log-entry>header{display:flex;flex-wrap:wrap;justify-content:space-between;gap:6px 12px;color:var(--text-secondary);font-size:12px;font-weight:600}
|
|
62
|
+
.work-log-body{overflow-wrap:anywhere}.work-log-body pre{max-width:100%;overflow:auto;padding:8px;background:var(--surface-1)}.work-log-body code{overflow-wrap:anywhere}
|
|
63
|
+
.work-log-origin,.work-log-head,.note-warning{color:var(--text-secondary);font-size:12px;overflow-wrap:anywhere}.note-warning{color:var(--critical)}
|
|
64
|
+
.gantt-warning{margin:16px 0;padding:12px;border:1px solid var(--critical);background:var(--surface-2)}.gantt-warning h2{margin:0 0 8px}.gantt-warning ul{margin:0;padding-left:20px}
|
|
59
65
|
.relation-list button{display:flex;width:100%;flex-direction:column}.relation-list button span{font-weight:400}.relation-kind{display:block;margin-top:4px;color:var(--text-secondary);font-size:12px}
|
|
60
66
|
.task-diagnostics{margin:16px 0;color:var(--text-secondary);font-size:12px}.task-diagnostics summary{cursor:pointer;color:var(--text-primary);font-weight:600}.task-diagnostics dl{display:grid;grid-template-columns:auto 1fr;gap:4px 12px}.task-diagnostics dd{margin:0;overflow-wrap:anywhere}
|
|
61
67
|
.task-index>h1{margin:0 0 8px;font-size:19px;font-weight:650;line-height:1.45}.task-index>p{margin:0 0 24px;color:var(--text-secondary)}
|