@quolu/lattice 0.38.1 → 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/src/cli-help.mjs +7 -7
- package/src/project-cli.mjs +32 -15
- 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-contracts.mjs
CHANGED
|
@@ -27,6 +27,8 @@ const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
|
27
27
|
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
28
28
|
const NOTE_FORBIDDEN_CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
|
|
29
29
|
|
|
30
|
+
export const TODO_DESIGN_MEMO_PROMPT = 'あなたがこのToDoに対して、何も考えていないならば、設計メモに `NO_PLAN` と書いてください';
|
|
31
|
+
|
|
30
32
|
export const isTodoDigest = (value) => typeof value === 'string' && DIGEST.test(value);
|
|
31
33
|
export const isTodoIdentifier = (value) => typeof value === 'string' && IDENTIFIER.test(value);
|
|
32
34
|
export const isNonNegativeSafeInteger = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
@@ -157,6 +159,9 @@ export function validateTodoNoteContext(value) {
|
|
|
157
159
|
|
|
158
160
|
const nullableDigest = (value) => value === null || isTodoDigest(value);
|
|
159
161
|
const nullableText = (value) => value === null || (typeof value === 'string' && value.length > 0 && Buffer.byteLength(value) <= 16_384);
|
|
162
|
+
export const isTodoDesignMemo = (value) => typeof value === 'string' && value.trim().length > 0
|
|
163
|
+
&& Buffer.byteLength(value, 'utf8') <= TODO_LIMITS.noteBodyBytes
|
|
164
|
+
&& !NOTE_FORBIDDEN_CONTROL.test(value);
|
|
160
165
|
const actor = (value) => exactRecord(value, ['host', 'session', 'agent'])
|
|
161
166
|
&& [value.host, value.session, value.agent].every(isTodoIdentifier);
|
|
162
167
|
const provenance = (value) => value === null || (exactRecord(value, ['source_commit', 'source_event_digest'])
|
|
@@ -246,6 +251,33 @@ function taskV4(value) {
|
|
|
246
251
|
&& isTodoIdentifier(value.phase_id);
|
|
247
252
|
}
|
|
248
253
|
|
|
254
|
+
function taskV5(value) {
|
|
255
|
+
return exactRecord(value, [
|
|
256
|
+
'task_id', 'title', 'lane', 'design_memo', 'narrative_ref', 'narrative_anchor',
|
|
257
|
+
'compile_binding', 'parent_task_id',
|
|
258
|
+
]) && isTodoIdentifier(value.task_id) && nullableText(value.title) && isTodoIdentifier(value.lane)
|
|
259
|
+
&& isTodoDesignMemo(value.design_memo)
|
|
260
|
+
&& (value.narrative_ref === null || isTodoRef(value.narrative_ref))
|
|
261
|
+
&& (value.narrative_anchor === null || (validateTodoNarrativeAnchor(value.narrative_anchor)
|
|
262
|
+
&& value.narrative_ref === value.narrative_anchor.origin_plan_ref))
|
|
263
|
+
&& compileBinding(value.compile_binding)
|
|
264
|
+
&& (value.parent_task_id === null || isTodoIdentifier(value.parent_task_id));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function taskV6(value) {
|
|
268
|
+
return exactRecord(value, [
|
|
269
|
+
'task_id', 'title', 'lane', 'design_memo', 'narrative_ref', 'narrative_anchor',
|
|
270
|
+
'compile_binding', 'parent_task_id', 'phase_id',
|
|
271
|
+
]) && isTodoIdentifier(value.task_id) && nullableText(value.title) && isTodoIdentifier(value.lane)
|
|
272
|
+
&& isTodoDesignMemo(value.design_memo)
|
|
273
|
+
&& (value.narrative_ref === null || isTodoRef(value.narrative_ref))
|
|
274
|
+
&& (value.narrative_anchor === null || (validateTodoNarrativeAnchor(value.narrative_anchor)
|
|
275
|
+
&& value.narrative_ref === value.narrative_anchor.origin_plan_ref))
|
|
276
|
+
&& compileBinding(value.compile_binding)
|
|
277
|
+
&& (value.parent_task_id === null || isTodoIdentifier(value.parent_task_id))
|
|
278
|
+
&& isTodoIdentifier(value.phase_id);
|
|
279
|
+
}
|
|
280
|
+
|
|
249
281
|
function phaseV1(value) {
|
|
250
282
|
return exactRecord(value, [
|
|
251
283
|
'phase_id', 'title', 'gate_policy', 'predecessor_phase_ids', 'required_evidence_slots',
|
|
@@ -300,13 +332,18 @@ export function validateTodoPlan(value) {
|
|
|
300
332
|
const taskValidator = value?.schema === 'lattice.todo_plan.v1' ? taskV1
|
|
301
333
|
: value?.schema === 'lattice.todo_plan.v2' ? taskV2
|
|
302
334
|
: value?.schema === 'lattice.todo_plan.v3' ? taskV3
|
|
303
|
-
: ['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(value?.schema) ? taskV4
|
|
335
|
+
: ['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(value?.schema) ? taskV4
|
|
336
|
+
: value?.schema === 'lattice.todo_plan.v6' ? taskV5
|
|
337
|
+
: value?.schema === 'lattice.todo_plan.v7' ? taskV6 : null;
|
|
304
338
|
const planKeys = [
|
|
305
339
|
'schema', 'project_id', 'plan_key', 'plan_version', 'predecessor_plan_digest',
|
|
306
340
|
'tasks', 'hard_dependencies', 'joins', 'topology_digest', 'plan_digest',
|
|
307
341
|
];
|
|
308
|
-
if (['lattice.todo_plan.v4', 'lattice.todo_plan.v5'
|
|
309
|
-
|
|
342
|
+
if (['lattice.todo_plan.v4', 'lattice.todo_plan.v5', 'lattice.todo_plan.v7']
|
|
343
|
+
.includes(value?.schema)) planKeys.push('phases');
|
|
344
|
+
if (['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(value?.schema)) {
|
|
345
|
+
planKeys.push('phase_accept_dependencies');
|
|
346
|
+
}
|
|
310
347
|
if (!exactRecord(value, planKeys) || taskValidator === null || !isTodoIdentifier(value.project_id)
|
|
311
348
|
|| !isTodoIdentifier(value.plan_key) || !isTodoIdentifier(value.plan_version)
|
|
312
349
|
|| !nullableDigest(value.predecessor_plan_digest) || !Array.isArray(value.tasks)
|
|
@@ -319,9 +356,10 @@ export function validateTodoPlan(value) {
|
|
|
319
356
|
&& Array.isArray(join.after) && join.after.length > 0 && join.after.length <= TODO_LIMITS.tasksPerPlan
|
|
320
357
|
&& join.after.every(nodeRef) && nodeRef(join.before))
|
|
321
358
|
|| !isTodoDigest(value.topology_digest) || !isTodoDigest(value.plan_digest)
|
|
322
|
-
|| (['lattice.todo_plan.v3', 'lattice.todo_plan.v4', 'lattice.todo_plan.v5'
|
|
359
|
+
|| (['lattice.todo_plan.v3', 'lattice.todo_plan.v4', 'lattice.todo_plan.v5',
|
|
360
|
+
'lattice.todo_plan.v6', 'lattice.todo_plan.v7'].includes(value.schema)
|
|
323
361
|
&& !validParentGraph(value.tasks))) return false;
|
|
324
|
-
if (['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(value.schema)
|
|
362
|
+
if (['lattice.todo_plan.v4', 'lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(value.schema)
|
|
325
363
|
&& (!Array.isArray(value.phases) || value.phases.length === 0
|
|
326
364
|
|| value.phases.length > TODO_LIMITS.tasksPerPlan || !value.phases.every(phaseV1)
|
|
327
365
|
|| value.phases.some((entry, index) => index > 0
|
|
@@ -329,7 +367,7 @@ export function validateTodoPlan(value) {
|
|
|
329
367
|
|| new Set(value.phases.map(({ phase_id }) => phase_id)).size !== value.phases.length
|
|
330
368
|
|| value.tasks.some(({ phase_id }) => !value.phases.some((phase) => phase.phase_id === phase_id))
|
|
331
369
|
|| !validPhaseGraph(value.phases))) return false;
|
|
332
|
-
if (
|
|
370
|
+
if (['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(value.schema)
|
|
333
371
|
&& (!Array.isArray(value.phase_accept_dependencies)
|
|
334
372
|
|| value.phase_accept_dependencies.length > TODO_LIMITS.edgesPerPlan
|
|
335
373
|
|| !value.phase_accept_dependencies.every((edge) => exactRecord(edge, ['from', 'to'])
|
|
@@ -347,9 +385,9 @@ export function validateTodoPlan(value) {
|
|
|
347
385
|
const topology = {
|
|
348
386
|
project_id: value.project_id, plan_key: value.plan_key, plan_version: value.plan_version,
|
|
349
387
|
tasks: value.tasks, hard_dependencies: value.hard_dependencies, joins: value.joins,
|
|
350
|
-
...(['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(value.schema)
|
|
388
|
+
...(['lattice.todo_plan.v4', 'lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(value.schema)
|
|
351
389
|
? { phases: value.phases } : {}),
|
|
352
|
-
...(
|
|
390
|
+
...(['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(value.schema)
|
|
353
391
|
? { phase_accept_dependencies: value.phase_accept_dependencies } : {}),
|
|
354
392
|
};
|
|
355
393
|
return value.topology_digest === digestTodoArtifact(topology)
|
|
@@ -151,11 +151,11 @@ export async function readVisibleTodoDashboardProjects({
|
|
|
151
151
|
|
|
152
152
|
export async function registerTodoDashboardActivity({
|
|
153
153
|
repoRoot, projectId, displayName = projectId, sessionId, env = process.env,
|
|
154
|
-
now = new Date(),
|
|
154
|
+
now = new Date(), adopt = false,
|
|
155
155
|
}) {
|
|
156
156
|
if (!path.isAbsolute(repoRoot) || !identifier(projectId) || !identifier(sessionId)
|
|
157
157
|
|| typeof displayName !== 'string' || displayName.length === 0 || displayName !== displayName.trim()
|
|
158
|
-
|| !(now instanceof Date) || !Number.isFinite(now.getTime())) {
|
|
158
|
+
|| !(now instanceof Date) || !Number.isFinite(now.getTime()) || typeof adopt !== 'boolean') {
|
|
159
159
|
throw new TypeError('dashboard activity is invalid');
|
|
160
160
|
}
|
|
161
161
|
const canonicalRoot = await realpath(repoRoot);
|
|
@@ -163,13 +163,27 @@ export async function registerTodoDashboardActivity({
|
|
|
163
163
|
await mkdir(refs.root, { recursive: true, mode: 0o700 });
|
|
164
164
|
await withLock(refs.lock, async () => {
|
|
165
165
|
const current = validateRegistry(await readJson(refs.registry, { schema: REGISTRY_SCHEMA, projects: [] }));
|
|
166
|
+
const existing = current.projects.find((entry) => entry.project_id === projectId);
|
|
167
|
+
if (existing !== undefined && existing.repo_root !== canonicalRoot && !adopt) {
|
|
168
|
+
const error = new Error('project id is already owned by another canonical root');
|
|
169
|
+
error.code = 'PROJECT_ROOT_CONFLICT';
|
|
170
|
+
// 公開・CLI errorへlocal absolute pathを運ばない。project_idだけで人と機械が
|
|
171
|
+
// 衝突対象を特定でき、registry bytesはこの分岐より後で一切変更しない。
|
|
172
|
+
error.detail = { project_id: projectId };
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
166
175
|
const projects = current.projects.filter((entry) => entry.project_id !== projectId);
|
|
167
176
|
projects.push({ project_id: projectId, display_name: displayName, repo_root: canonicalRoot,
|
|
168
177
|
session_id: sessionId, last_seen_at: now.toISOString() });
|
|
169
178
|
projects.sort((left, right) => left.project_id.localeCompare(right.project_id, 'en'));
|
|
170
179
|
await atomicJson(refs.registry, { schema: REGISTRY_SCHEMA, projects });
|
|
171
180
|
});
|
|
172
|
-
return { projectId, displayName, repoRoot: canonicalRoot, sessionId };
|
|
181
|
+
return { projectId, displayName, repoRoot: canonicalRoot, sessionId, adopted: adopt };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 配信元rootを移す唯一の明示入口。通常activity登録はこのflagを立てない。 */
|
|
185
|
+
export async function adoptTodoDashboardActivity(options) {
|
|
186
|
+
return registerTodoDashboardActivity({ ...options, adopt: true });
|
|
173
187
|
}
|
|
174
188
|
|
|
175
189
|
function validDaemonDescriptor(descriptor) {
|
|
@@ -21,6 +21,16 @@ function renderNoteContext(context) {
|
|
|
21
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
22
|
}
|
|
23
23
|
|
|
24
|
+
function renderDesignMemo(task) {
|
|
25
|
+
if (typeof task.design_memo !== 'string') {
|
|
26
|
+
return '<section class="design-memo"><h2>設計メモ</h2><p class="note-warning">未登録(legacy ToDo)</p></section>';
|
|
27
|
+
}
|
|
28
|
+
const rendered = renderTodoMarkdown(task.design_memo);
|
|
29
|
+
const filtering = rendered.discarded.length === 0 ? ''
|
|
30
|
+
: '<p class="note-warning">安全上表示できない要素を除外しました。</p>';
|
|
31
|
+
return `<section class="design-memo"><h2>設計メモ</h2><div class="design-memo-body">${rendered.html}</div>${filtering}</section>`;
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
/**
|
|
25
35
|
* 図の外が語るための独立性要約を、plan単位の投影から引ける形へ畳む(ADR 0129 Decision 3)。
|
|
26
36
|
*/
|
|
@@ -150,9 +160,10 @@ export function renderRightPane(
|
|
|
150
160
|
const independenceNote = renderIndependenceNote(section.ref, node, independenceSummary);
|
|
151
161
|
// Say it plainly when the reader will not find this ToDo on the diagram.
|
|
152
162
|
const foldedNote = !folds.has(key) ? ''
|
|
153
|
-
: '<p class="fold-note"
|
|
163
|
+
: '<p class="fold-note">完走済みのため図には描いていません。図に出すには動的dashboardを <code>lattice todo gantt serve --port <port> --scope all</code> で起動してください。</p>';
|
|
154
164
|
const workLog = notesEnabled ? renderNoteContext(section.noteContext) : '';
|
|
155
|
-
|
|
165
|
+
const designMemo = renderDesignMemo(section.task);
|
|
166
|
+
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}${designMemo}<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>`;
|
|
156
167
|
}).join('');
|
|
157
168
|
const taskIndex = renderTaskIndex(sections, lookup, folds, planActivity(readModel));
|
|
158
169
|
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>`;
|
|
@@ -172,7 +183,7 @@ export function renderDiagramLegend(presentation, layout = null, expandable = fa
|
|
|
172
183
|
const foldNote = foldedCount === 0 ? ''
|
|
173
184
|
: expandable
|
|
174
185
|
? '<p class="fold-note">後続に作業中・未着手が残っていない完了工程は図から外しています。生きた工程とその直接の前提工程は必ず描きます。上のバッジを押すと外した工程も含めて描きます。総数・進捗・最長依存鎖は外す前の全工程で数えています。</p>'
|
|
175
|
-
: '<p class="fold-note"
|
|
186
|
+
: '<p class="fold-note">後続に作業中・未着手が残っていない完了工程は図から外しています。生きた工程とその直接の前提工程は必ず描きます。外した工程は右の「全工程」から辿れ、図に出すには動的dashboardを <code>lattice todo gantt serve --port <port> --scope all</code> で起動してください。総数・進捗・最長依存鎖は外す前の全工程で数えています。</p>';
|
|
176
187
|
const independenceLegend = layout.independence === null ? ''
|
|
177
188
|
: '<span>∥ 独立検証済</span><span>⛓ 要直列</span><span>? 未検査</span>';
|
|
178
189
|
// 独立性の記録がある間は「全件同時dispatchが既定」と無条件に述べない(ADR 0129 Decision 3)。
|
|
@@ -116,7 +116,8 @@ export function renderPhaseProgress(readModel) {
|
|
|
116
116
|
const rows = [];
|
|
117
117
|
const settledRows = [];
|
|
118
118
|
for (const member of readModel.members) {
|
|
119
|
-
if (!['lattice.todo_plan.v4', 'lattice.todo_plan.v5'
|
|
119
|
+
if (!['lattice.todo_plan.v4', 'lattice.todo_plan.v5', 'lattice.todo_plan.v7']
|
|
120
|
+
.includes(member.plan.schema)) continue;
|
|
120
121
|
// snapshot artifactの形式には縛られない導出ビュー(member.phases)を読む(ADR 0147)。
|
|
121
122
|
const phases = new Map(member.phases.map((phase) => [phase.phase_id, phase]));
|
|
122
123
|
for (const phase of member.plan.phases) {
|
|
@@ -130,7 +131,9 @@ export function renderPhaseProgress(readModel) {
|
|
|
130
131
|
(SETTLED_PHASE_STATUS.includes(state.status) ? settledRows : rows).push(row);
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
|
-
const decoupled = readModel.members.some(({ plan }) =>
|
|
134
|
+
const decoupled = readModel.members.some(({ plan }) => (
|
|
135
|
+
['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(plan.schema)
|
|
136
|
+
));
|
|
134
137
|
const guidance = decoupled
|
|
135
138
|
? 'ToDo完了とPhase受理は別です。Phaseは重監査の順序を表し、通常ToDoの開始順はToDo依存だけで決まります。'
|
|
136
139
|
: 'ToDo完了とPhase受理は別です。<code>gate_ready</code>では後続Phaseはまだ解放されません。';
|
|
@@ -251,7 +251,7 @@ function readyTaskKeys(readModel, nodes, nodesByKey, incoming) {
|
|
|
251
251
|
member.plan.project_id, member.plan.plan_key, phase.phase_id,
|
|
252
252
|
]), phase.status);
|
|
253
253
|
}
|
|
254
|
-
if (
|
|
254
|
+
if (['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(member.plan.schema)) {
|
|
255
255
|
for (const edge of member.plan.phase_accept_dependencies ?? []) {
|
|
256
256
|
const target = refKey(refOf(edge.to, 'phase_accept_dependencies.to'));
|
|
257
257
|
if (!nodesByKey.has(target)) {
|
package/src/todo-gantt-scope.mjs
CHANGED
|
@@ -59,7 +59,7 @@ const AUDIT_PENDING_PHASE_STATUSES = new Set(['gate_ready', 'reviewing', 'reject
|
|
|
59
59
|
function auditPending(node) {
|
|
60
60
|
if (!AUDIT_PENDING_PHASE_STATUSES.has(node.phase_status ?? null)) return false;
|
|
61
61
|
const schema = node.plan_schema ?? null;
|
|
62
|
-
return
|
|
62
|
+
return !['lattice.todo_plan.v4', 'lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(schema);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
package/src/todo-migration.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
exactRecord,
|
|
4
4
|
isStrictTodoTimestamp,
|
|
5
5
|
isTodoDigest,
|
|
6
|
+
isTodoDesignMemo,
|
|
6
7
|
isTodoIdentifier,
|
|
7
8
|
isTodoRef,
|
|
8
9
|
todoSelfDigest,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
|
|
16
17
|
export const TODO_EXTRACTION_SCHEMA = 'lattice.todo_extraction.v1';
|
|
17
18
|
export const TODO_EXTRACTION_SCHEMA_V2 = 'lattice.todo_extraction.v2';
|
|
19
|
+
export const TODO_EXTRACTION_SCHEMA_V3 = 'lattice.todo_extraction.v3';
|
|
18
20
|
|
|
19
21
|
const V1_DISPOSITIONS = new Set([
|
|
20
22
|
'register_pending',
|
|
@@ -93,14 +95,17 @@ function historicalStart(value) {
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
function extractionTask(value, schema) {
|
|
96
|
-
const v2 = schema
|
|
98
|
+
const v2 = [TODO_EXTRACTION_SCHEMA_V2, TODO_EXTRACTION_SCHEMA_V3].includes(schema);
|
|
99
|
+
const v3 = schema === TODO_EXTRACTION_SCHEMA_V3;
|
|
97
100
|
const keys = [
|
|
98
101
|
'task_id', 'title', 'lane', 'narrative_ref', 'compile_binding', 'disposition',
|
|
99
102
|
'completion', 'source', 'migration_context',
|
|
100
103
|
];
|
|
101
104
|
if (v2) keys.push('start');
|
|
105
|
+
if (v3) keys.push('design_memo');
|
|
102
106
|
if (!exactRecord(value, keys) || !isTodoIdentifier(value.task_id) || !boundedText(value.title)
|
|
103
107
|
|| !isTodoIdentifier(value.lane) || (value.narrative_ref !== null && !isTodoRef(value.narrative_ref))
|
|
108
|
+
|| (v3 && !isTodoDesignMemo(value.design_memo))
|
|
104
109
|
|| value.compile_binding !== null || !(v2 ? V2_DISPOSITIONS : V1_DISPOSITIONS).has(value.disposition)
|
|
105
110
|
|| !sourceLocation(value.source) || !migrationContext(value.migration_context)) return false;
|
|
106
111
|
if (!v2) return value.disposition === 'register_done' ? completion(value.completion) : value.completion === null;
|
|
@@ -126,7 +131,7 @@ function validateJoins(value) {
|
|
|
126
131
|
&& sortedStrictly(value, (join) => join.id);
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
const reject = (reason, path = '') => ({ valid: false, reason, path });
|
|
134
|
+
const reject = (reason, path = '', detail = {}) => ({ valid: false, reason, path, ...detail });
|
|
130
135
|
|
|
131
136
|
/** value自体がexactRecordの対象になれる素朴なobjectかどうか(配列・nullを除く)。 */
|
|
132
137
|
function plainObject(value) {
|
|
@@ -175,10 +180,13 @@ export function explainTodoExtraction(value) {
|
|
|
175
180
|
try {
|
|
176
181
|
if (!plainObject(value)) return reject('not_an_object', '');
|
|
177
182
|
const schema = value.schema;
|
|
178
|
-
if (![TODO_EXTRACTION_SCHEMA, TODO_EXTRACTION_SCHEMA_V2].includes(schema)) {
|
|
179
|
-
return reject('schema_mismatch', '/schema'
|
|
183
|
+
if (![TODO_EXTRACTION_SCHEMA, TODO_EXTRACTION_SCHEMA_V2, TODO_EXTRACTION_SCHEMA_V3].includes(schema)) {
|
|
184
|
+
return reject('schema_mismatch', '/schema', {
|
|
185
|
+
expected: TODO_EXTRACTION_SCHEMA_V3, actual: typeof schema === 'string' ? schema : null,
|
|
186
|
+
});
|
|
180
187
|
}
|
|
181
|
-
const v2 = schema
|
|
188
|
+
const v2 = [TODO_EXTRACTION_SCHEMA_V2, TODO_EXTRACTION_SCHEMA_V3].includes(schema);
|
|
189
|
+
const v3 = schema === TODO_EXTRACTION_SCHEMA_V3;
|
|
182
190
|
const topKeys = [
|
|
183
191
|
'schema', 'project_id', 'plan_key', 'plan_version', 'actor', 'recorded_at',
|
|
184
192
|
'tasks', 'hard_dependencies', 'joins', 'extraction_digest',
|
|
@@ -199,11 +207,33 @@ export function explainTodoExtraction(value) {
|
|
|
199
207
|
'start', 'completion', 'source', 'migration_context']
|
|
200
208
|
: ['task_id', 'title', 'lane', 'narrative_ref', 'compile_binding', 'disposition',
|
|
201
209
|
'completion', 'source', 'migration_context'];
|
|
210
|
+
if (v3) taskKeys.push('design_memo');
|
|
202
211
|
for (const [index, task] of value.tasks.entries()) {
|
|
203
212
|
const taskKeyCheck = explainKeys(task, taskKeys, `/tasks/${index}`);
|
|
204
213
|
if (!taskKeyCheck.valid) return taskKeyCheck;
|
|
214
|
+
if (v3 && !isTodoDesignMemo(task.design_memo)) {
|
|
215
|
+
return reject('design_memo_required', `/tasks/${index}/design_memo`, {
|
|
216
|
+
task_id: isTodoIdentifier(task.task_id) ? task.task_id : null,
|
|
217
|
+
expected: 'non-empty Markdown or NO_PLAN',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (task.disposition === 'register_done'
|
|
221
|
+
&& task.completion?.done_mode !== 'historical_import') {
|
|
222
|
+
return reject('enum_mismatch', `/tasks/${index}/completion/done_mode`, {
|
|
223
|
+
task_id: isTodoIdentifier(task.task_id) ? task.task_id : null,
|
|
224
|
+
expected: 'historical_import', actual: task.completion?.done_mode ?? null,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (!Array.isArray(task.migration_context?.notes)) {
|
|
228
|
+
return reject('expected_array', `/tasks/${index}/migration_context/notes`, {
|
|
229
|
+
task_id: isTodoIdentifier(task.task_id) ? task.task_id : null,
|
|
230
|
+
expected: 'array', actual: typeof task.migration_context?.notes,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
205
233
|
if (!extractionTask(task, schema)) {
|
|
206
|
-
return reject('task_shape_invalid', `/tasks/${index}
|
|
234
|
+
return reject('task_shape_invalid', `/tasks/${index}`, {
|
|
235
|
+
task_id: isTodoIdentifier(task.task_id) ? task.task_id : null,
|
|
236
|
+
});
|
|
207
237
|
}
|
|
208
238
|
}
|
|
209
239
|
const sortCheck = explainSortedStrictly(value.tasks, (task) => task.task_id, '/tasks');
|
|
@@ -216,7 +246,9 @@ export function explainTodoExtraction(value) {
|
|
|
216
246
|
if (!isTodoDigest(value.extraction_digest)) return reject('invalid_digest', '/extraction_digest');
|
|
217
247
|
const expectedDigest = todoSelfDigest(value, 'extraction_digest');
|
|
218
248
|
if (value.extraction_digest !== expectedDigest) {
|
|
219
|
-
return reject('extraction_digest_mismatch', '/extraction_digest'
|
|
249
|
+
return reject('extraction_digest_mismatch', '/extraction_digest', {
|
|
250
|
+
expected: expectedDigest, actual: value.extraction_digest,
|
|
251
|
+
});
|
|
220
252
|
}
|
|
221
253
|
const taskIds = new Set(value.tasks.map(({ task_id: taskId }) => taskId));
|
|
222
254
|
const badParent = value.tasks.find((task) => task.source.parent_task_id === task.task_id
|
|
@@ -255,7 +287,7 @@ export function validateTodoExtraction(value) {
|
|
|
255
287
|
if (!exactRecord(value, [
|
|
256
288
|
'schema', 'project_id', 'plan_key', 'plan_version', 'actor', 'recorded_at',
|
|
257
289
|
'tasks', 'hard_dependencies', 'joins', 'extraction_digest',
|
|
258
|
-
]) || ![TODO_EXTRACTION_SCHEMA, TODO_EXTRACTION_SCHEMA_V2].includes(schema)
|
|
290
|
+
]) || ![TODO_EXTRACTION_SCHEMA, TODO_EXTRACTION_SCHEMA_V2, TODO_EXTRACTION_SCHEMA_V3].includes(schema)
|
|
259
291
|
|| !isTodoIdentifier(value.project_id)
|
|
260
292
|
|| !isTodoIdentifier(value.plan_key) || !isTodoIdentifier(value.plan_version)
|
|
261
293
|
|| !actor(value.actor) || !isStrictTodoTimestamp(value.recorded_at)
|
|
@@ -317,7 +349,8 @@ export function compileTodoExtraction(value, repoRoot) {
|
|
|
317
349
|
repositories: [{ repo_id: 'self', path: '.' }],
|
|
318
350
|
},
|
|
319
351
|
plan: {
|
|
320
|
-
schema:
|
|
352
|
+
schema: value.schema === TODO_EXTRACTION_SCHEMA_V3
|
|
353
|
+
? 'lattice.todo_plan.v6' : 'lattice.todo_plan.v2',
|
|
321
354
|
project_id: value.project_id,
|
|
322
355
|
plan_key: value.plan_key,
|
|
323
356
|
plan_version: value.plan_version,
|
|
@@ -326,9 +359,13 @@ export function compileTodoExtraction(value, repoRoot) {
|
|
|
326
359
|
task_id: task.task_id,
|
|
327
360
|
title: task.title,
|
|
328
361
|
lane: task.lane,
|
|
362
|
+
...(value.schema === TODO_EXTRACTION_SCHEMA_V3
|
|
363
|
+
? { design_memo: task.design_memo } : {}),
|
|
329
364
|
narrative_ref: task.narrative_ref,
|
|
330
365
|
narrative_anchor: null,
|
|
331
366
|
compile_binding: null,
|
|
367
|
+
...(value.schema === TODO_EXTRACTION_SCHEMA_V3
|
|
368
|
+
? { parent_task_id: task.source.parent_task_id } : {}),
|
|
332
369
|
})),
|
|
333
370
|
hard_dependencies: value.hard_dependencies,
|
|
334
371
|
joins: value.joins,
|
package/src/todo-revision.mjs
CHANGED
|
@@ -66,14 +66,14 @@ export function phaseTodoRevisionPlanVersion({
|
|
|
66
66
|
projectId, planKey, predecessor, desiredPlan, taskMigration, phaseMigration,
|
|
67
67
|
}) {
|
|
68
68
|
const versionDigest = digestTodoArtifact({
|
|
69
|
-
schema:
|
|
69
|
+
schema: ['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(desiredPlan.schema)
|
|
70
70
|
? 'lattice.phase_todo_revision_version.v2' : 'lattice.phase_todo_revision_version.v1', project_id: projectId,
|
|
71
71
|
plan_key: planKey, predecessor, desired_topology: {
|
|
72
72
|
schema: desiredPlan.schema, project_id: desiredPlan.project_id,
|
|
73
73
|
plan_key: desiredPlan.plan_key, predecessor_plan_digest: desiredPlan.predecessor_plan_digest,
|
|
74
74
|
tasks: desiredPlan.tasks, phases: desiredPlan.phases,
|
|
75
75
|
hard_dependencies: desiredPlan.hard_dependencies, joins: desiredPlan.joins,
|
|
76
|
-
...(
|
|
76
|
+
...(['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(desiredPlan.schema)
|
|
77
77
|
? { phase_accept_dependencies: desiredPlan.phase_accept_dependencies } : {}),
|
|
78
78
|
}, task_migration: taskMigration, phase_migration: phaseMigration,
|
|
79
79
|
});
|
|
@@ -295,8 +295,9 @@ export function validatePhaseTodoRevision(value) {
|
|
|
295
295
|
if (!exactRecord(value, keys) || (!revisionV1 && !revisionV2 && !revisionV3)) return false;
|
|
296
296
|
if (!isTodoIdentifier(value.project_id) || !isTodoIdentifier(value.plan_key)) return false;
|
|
297
297
|
if (!validPredecessor(value.predecessor) || !validateTodoPlan(value.desired_plan)
|
|
298
|
-
||
|
|
299
|
-
? 'lattice.todo_plan.v5'
|
|
298
|
+
|| !(revisionV3
|
|
299
|
+
? ['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(value.desired_plan.schema)
|
|
300
|
+
: value.desired_plan.schema === (revisionV2 ? 'lattice.todo_plan.v5' : 'lattice.todo_plan.v4'))
|
|
300
301
|
|| value.desired_plan.project_id !== value.project_id
|
|
301
302
|
|| value.desired_plan.plan_key !== value.plan_key
|
|
302
303
|
|| value.desired_plan.predecessor_plan_digest !== value.predecessor.plan_digest
|
|
@@ -370,7 +371,7 @@ export function validateTodoRevision(value) {
|
|
|
370
371
|
|| !exactRecord(value, revisionV1 ? REVISION_V1_KEYS : REVISION_V2_KEYS)
|
|
371
372
|
|| !isTodoIdentifier(value.project_id) || !isTodoIdentifier(value.plan_key)
|
|
372
373
|
|| !validPredecessor(value.predecessor) || !validateTodoPlan(value.desired_plan)
|
|
373
|
-
||
|
|
374
|
+
|| !['lattice.todo_plan.v3', 'lattice.todo_plan.v6'].includes(value.desired_plan.schema)
|
|
374
375
|
|| value.desired_plan.project_id !== value.project_id
|
|
375
376
|
|| value.desired_plan.plan_key !== value.plan_key
|
|
376
377
|
|| value.desired_plan.predecessor_plan_digest !== value.predecessor.plan_digest
|
|
@@ -729,7 +730,7 @@ export function explainTodoRevision(value) {
|
|
|
729
730
|
const predecessorCheck = explainPredecessor(value.predecessor);
|
|
730
731
|
if (!predecessorCheck.valid) return predecessorCheck;
|
|
731
732
|
if (!validateTodoPlan(value.desired_plan)) return reject('desired_plan_invalid', '/desired_plan');
|
|
732
|
-
if (
|
|
733
|
+
if (!['lattice.todo_plan.v3', 'lattice.todo_plan.v6'].includes(value.desired_plan.schema)) {
|
|
733
734
|
return reject('desired_plan_schema_mismatch', '/desired_plan/schema');
|
|
734
735
|
}
|
|
735
736
|
if (value.desired_plan.project_id !== value.project_id) {
|
|
@@ -817,8 +818,9 @@ export function explainPhaseTodoRevision(value) {
|
|
|
817
818
|
const predecessorCheck = explainPredecessor(value.predecessor);
|
|
818
819
|
if (!predecessorCheck.valid) return predecessorCheck;
|
|
819
820
|
if (!validateTodoPlan(value.desired_plan)) return reject('desired_plan_invalid', '/desired_plan');
|
|
820
|
-
const
|
|
821
|
-
|
|
821
|
+
const expectedPlanSchemas = revisionV3 ? ['lattice.todo_plan.v5', 'lattice.todo_plan.v7']
|
|
822
|
+
: [revisionV2 ? 'lattice.todo_plan.v5' : 'lattice.todo_plan.v4'];
|
|
823
|
+
if (!expectedPlanSchemas.includes(value.desired_plan.schema)) {
|
|
822
824
|
return reject('desired_plan_schema_mismatch', '/desired_plan/schema');
|
|
823
825
|
}
|
|
824
826
|
if (value.desired_plan.project_id !== value.project_id) {
|
package/src/todo-status.mjs
CHANGED
|
@@ -247,9 +247,9 @@ function buildTodoGraph(readModel) {
|
|
|
247
247
|
status: state.status,
|
|
248
248
|
blocked_reason: state.blocked_reason,
|
|
249
249
|
plan_schema: member.plan.schema,
|
|
250
|
-
phase_id: ['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(member.plan.schema)
|
|
250
|
+
phase_id: ['lattice.todo_plan.v4', 'lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(member.plan.schema)
|
|
251
251
|
? task.phase_id : null,
|
|
252
|
-
phase_status: ['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(member.plan.schema)
|
|
252
|
+
phase_status: ['lattice.todo_plan.v4', 'lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(member.plan.schema)
|
|
253
253
|
? phases.get(task.phase_id)?.status : null,
|
|
254
254
|
phase_ready: member.plan.schema !== 'lattice.todo_plan.v4'
|
|
255
255
|
|| phases.get(task.phase_id)?.status === 'active',
|
|
@@ -272,7 +272,7 @@ function buildTodoGraph(readModel) {
|
|
|
272
272
|
for (const join of member.plan.joins) {
|
|
273
273
|
for (const after of join.after) addPredecessor(after, join.before);
|
|
274
274
|
}
|
|
275
|
-
if (
|
|
275
|
+
if (['lattice.todo_plan.v5', 'lattice.todo_plan.v7'].includes(member.plan.schema)) {
|
|
276
276
|
for (const edge of member.plan.phase_accept_dependencies) {
|
|
277
277
|
const target = refKey(edge.to);
|
|
278
278
|
if (!nodes.has(target)) fail('TODO_STATUS_INVALID_INPUT', 'todo_status_dependency_dangling');
|