@quolu/lattice 0.34.2 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +54 -4
- package/README.md +8 -0
- package/bin/lattice.mjs +15 -3
- package/docs/bridge-setup.md +13 -0
- package/package.json +1 -1
- package/src/cli-help.mjs +20 -9
- package/src/project-cli.mjs +126 -3
- package/src/todo-cli.mjs +224 -22
- package/src/todo-contracts.mjs +2 -2
- package/src/todo-dispatch-shape.mjs +190 -0
- package/src/todo-gantt-html-shared.mjs +2 -1
- package/src/todo-gantt-layout.mjs +13 -3
- package/src/todo-gantt-live.mjs +21 -2
- package/src/todo-gantt-scope.mjs +27 -2
- package/src/todo-migration.mjs +108 -0
- package/src/todo-revision.mjs +568 -2
- package/src/todo-status.mjs +5 -2
- package/src/todo-store.mjs +155 -33
|
@@ -153,7 +153,9 @@ function normalizeInput(readModel, chainProjection) {
|
|
|
153
153
|
}
|
|
154
154
|
const { plan } = member;
|
|
155
155
|
const statusByTask = new Map(member.tasks.map((task) => [task.task_id, task]));
|
|
156
|
-
|
|
156
|
+
// snapshot artifactの形式(v1にはphasesキーが無い)には縛られない導出ビューを読む
|
|
157
|
+
// (readTodoStoreが常にmember.phasesとして埋める。ADR 0147)。
|
|
158
|
+
const statusByPhase = new Map((member.phases ?? [])
|
|
157
159
|
.map((phase) => [phase.phase_id, phase.status]));
|
|
158
160
|
for (const task of plan.tasks) {
|
|
159
161
|
if (!plain(task) || typeof task.task_id !== 'string' || typeof task.lane !== 'string') {
|
|
@@ -174,7 +176,14 @@ function normalizeInput(readModel, chainProjection) {
|
|
|
174
176
|
status: state.status,
|
|
175
177
|
plan_schema: plan.schema ?? null,
|
|
176
178
|
phase_id: task.phase_id ?? null,
|
|
177
|
-
|
|
179
|
+
// ADR 0147: phase無しplan(task.phase_idが無い世代)も、終端に暗黙のterminal-audit
|
|
180
|
+
// Phaseを1つ持つ(todo-store.mjsのphasesOf/TERMINAL_AUDIT_PHASE_ID)。ここで素通しせず
|
|
181
|
+
// nullのままにすると、gantt scope側(todo-gantt-scope.mjs)が「監査未了のplanを畳まない」
|
|
182
|
+
// 判定に使える材料を一切受け取れない。'terminal-audit'はTERMINAL_AUDIT_PHASE_IDと同じ
|
|
183
|
+
// 予約IDで、phase_readyの判定(v4だけを見る既存分岐)には影響しない。
|
|
184
|
+
phase_status: task.phase_id === undefined
|
|
185
|
+
? statusByPhase.get('terminal-audit') ?? null
|
|
186
|
+
: statusByPhase.get(task.phase_id) ?? null,
|
|
178
187
|
phase_ready: plan.schema !== 'lattice.todo_plan.v4'
|
|
179
188
|
|| statusByPhase.get(task.phase_id) === 'active',
|
|
180
189
|
});
|
|
@@ -236,7 +245,8 @@ function readyTaskKeys(readModel, nodes, nodesByKey, incoming) {
|
|
|
236
245
|
const phaseStatuses = new Map();
|
|
237
246
|
const phaseAcceptIncoming = new Map(nodes.map(({ key }) => [key, new Set()]));
|
|
238
247
|
for (const member of readModel.members) {
|
|
239
|
-
|
|
248
|
+
// snapshot artifactの形式には縛られない導出ビュー(member.phases)を読む(ADR 0147)。
|
|
249
|
+
for (const phase of member.phases ?? []) {
|
|
240
250
|
phaseStatuses.set(JSON.stringify([
|
|
241
251
|
member.plan.project_id, member.plan.plan_key, phase.phase_id,
|
|
242
252
|
]), phase.status);
|
package/src/todo-gantt-live.mjs
CHANGED
|
@@ -40,6 +40,13 @@ function dashboardHtml(projects) {
|
|
|
40
40
|
return `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Latticeが管理している公開中の工程と現在地を確認できます。"><meta name="robots" content="noindex, nofollow"><meta property="og:title" content="公開中の工程表 — Lattice"><meta property="og:description" content="Latticeが管理している公開中の工程と現在地を確認できます。"><meta name="theme-color" content="#f7f3ea"><title>公開中の工程表 — Lattice</title><style>:root{color-scheme:light;--paper:#f7f3ea;--panel:#fffdf8;--ink:#201d19;--soft:#6c655d;--line:#d8d0c5;--cobalt:#315cbe;--orange:#e85f2a}*{box-sizing:border-box}body{min-height:100vh;margin:0;color:var(--ink);background:var(--paper);font:16px/1.7 system-ui,-apple-system,sans-serif}.shell{max-width:880px;margin:0 auto;padding:28px 22px 40px}.brand{display:flex;align-items:center;gap:9px;padding-bottom:24px;border-bottom:1px solid var(--line);font-size:.88rem}.brand a,.footer a{color:var(--ink);font-weight:800;text-decoration:none}.brand a:hover,.footer a:hover{color:var(--cobalt)}.brand span{color:var(--soft)}main{padding:64px 0 72px}.eyebrow{margin:0 0 8px;color:var(--orange);font-size:.76rem;font-weight:800;letter-spacing:.14em}.lead{max-width:620px;margin:0 0 34px;color:var(--soft)}h1{margin:0 0 14px;font-size:clamp(2rem,6vw,3.4rem);line-height:1.12;letter-spacing:-.04em}ul{display:grid;gap:12px;margin:0;padding:0;list-style:none}li a{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:20px;padding:18px 20px;border:1px solid var(--line);border-radius:12px;color:inherit;background:var(--panel);text-decoration:none;box-shadow:0 8px 28px rgba(48,39,27,.04)}li a:hover{border-color:var(--cobalt);transform:translateY(-1px)}li strong{font-size:1.04rem}li code{color:var(--soft);font-size:.78rem}li span{color:var(--cobalt);font-weight:800}.note{margin:28px 0 0;padding:16px 18px;border-left:3px solid var(--orange);color:var(--soft);background:rgba(255,253,248,.72);font-size:.88rem}.footer{display:flex;flex-wrap:wrap;justify-content:space-between;gap:16px;padding-top:20px;border-top:1px solid var(--line);color:var(--soft);font-size:.82rem}.footer nav{display:flex;gap:18px}@media(max-width:560px){.shell{padding:20px 16px 32px}main{padding:44px 0 56px}li a{grid-template-columns:minmax(0,1fr) auto;padding:16px}li code{grid-column:1/-1;grid-row:2}.footer{display:block}.footer nav{margin-top:10px}}</style></head><body><div class="shell"><header class="brand"><a href="https://kitepon.dev/">kitepon.dev</a><span aria-hidden="true">/</span><strong>Lattice</strong></header><main><p class="eyebrow">LIVE DEVELOPMENT</p><h1>公開中の工程表</h1><p class="lead">Latticeが管理しているプロジェクトの工程と、いまどこまで進んでいるかを公開データから確認できます。</p>${content}<p class="note">表示内容はLatticeの記録から自動生成されます。製品の紹介や使い方はGitHubをご覧ください。</p></main><footer class="footer"><span>kitepon.dev の開発工程を、Latticeで可視化しています。</span><nav aria-label="関連リンク"><a href="https://kitepon.dev/">kitepon.dev</a><a href="https://github.com/kitepon-rgb/Lattice">GitHub</a></nav></footer></div></body></html>`;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function notFoundHtml(code, path) {
|
|
44
|
+
const reason = code === 'PROJECT_NOT_FOUND'
|
|
45
|
+
? '指定された工程表は、公開を終了したかURLが変わった可能性があります。'
|
|
46
|
+
: '指定されたページは、この公開工程表にはありません。';
|
|
47
|
+
return `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex, nofollow"><meta name="theme-color" content="#f7f3ea"><title>ページが見つかりません — Lattice</title><style>:root{color-scheme:light;--paper:#f7f3ea;--panel:#fffdf8;--ink:#201d19;--soft:#6c655d;--line:#d8d0c5;--cobalt:#315cbe;--orange:#e85f2a}*{box-sizing:border-box}body{min-height:100vh;margin:0;color:var(--ink);background:var(--paper);font:16px/1.7 system-ui,-apple-system,"Hiragino Sans","Yu Gothic UI",sans-serif}.shell{width:min(720px,calc(100% - 32px));margin:0 auto;padding:28px 0 40px}.brand{display:flex;align-items:center;gap:9px;padding-bottom:24px;border-bottom:1px solid var(--line);font-size:.88rem}.brand a{color:var(--ink);font-weight:800;text-decoration:none}.brand a:hover{color:var(--cobalt)}.brand span{color:var(--soft)}main{padding:clamp(64px,12vw,112px) 0}.eyebrow{margin:0 0 10px;color:var(--orange);font-size:.76rem;font-weight:800;letter-spacing:.14em}h1{margin:0 0 16px;font-size:clamp(2.1rem,7vw,4rem);line-height:1.1;letter-spacing:-.045em}p{max-width:620px;margin:0;color:var(--soft)}code{display:block;margin-top:22px;padding:12px 14px;overflow-wrap:anywhere;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--soft);font-size:.78rem}.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:28px}.actions a{display:inline-flex;align-items:center;min-height:44px;padding:0 16px;border:1px solid var(--line);border-radius:8px;color:var(--ink);background:var(--panel);font-weight:750;text-decoration:none}.actions a:first-child{border-color:var(--cobalt);color:#fff;background:var(--cobalt)}.actions a:hover{transform:translateY(-1px)}</style></head><body><div class="shell"><header class="brand"><a href="https://kitepon.dev/">kitepon.dev</a><span aria-hidden="true">/</span><strong>Lattice</strong></header><main><p class="eyebrow">404 · ${escapeHtml(code)}</p><h1>ページが見つかりません</h1><p>${reason}</p><code>${escapeHtml(path)}</code><nav class="actions" aria-label="戻り先"><a href="/projects/">公開工程表の一覧へ</a><a href="https://kitepon.dev/">kitepon.devへ</a></nav></main></div></body></html>`;
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
function validateProject(project) {
|
|
44
51
|
if (project === null || typeof project !== 'object' || Array.isArray(project)
|
|
45
52
|
|| typeof project.projectId !== 'string' || !PROJECT_ID.test(project.projectId)
|
|
@@ -105,6 +112,18 @@ function sendHttpError(response, status, code, path) {
|
|
|
105
112
|
response.end(`${JSON.stringify({ schema: HTTP_ERROR_SCHEMA, code, path })}\n`);
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
function sendNotFound(request, response, code, path) {
|
|
116
|
+
if (!String(request.headers.accept ?? '').toLowerCase().includes('text/html')) {
|
|
117
|
+
sendHttpError(response, 404, code, path);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const html = notFoundHtml(code, path);
|
|
121
|
+
response.writeHead(404, { 'content-type': 'text/html; charset=utf-8',
|
|
122
|
+
'content-length': Buffer.byteLength(html), 'cache-control': 'no-store',
|
|
123
|
+
'x-content-type-options': 'nosniff' });
|
|
124
|
+
response.end(html);
|
|
125
|
+
}
|
|
126
|
+
|
|
108
127
|
function finishRequestFailure(response, code, path) {
|
|
109
128
|
try {
|
|
110
129
|
if (!response.headersSent) sendHttpError(response, 500, code, path);
|
|
@@ -167,7 +186,7 @@ export async function startTodoGanttDashboardServer({ registry, port = 0, redire
|
|
|
167
186
|
}
|
|
168
187
|
const match = /^\/projects\/([^/]+)\/(events)?$/u.exec(url.pathname);
|
|
169
188
|
if (match === null) {
|
|
170
|
-
|
|
189
|
+
sendNotFound(request, response, 'ROUTE_NOT_FOUND', url.pathname);
|
|
171
190
|
return;
|
|
172
191
|
}
|
|
173
192
|
let requestedId;
|
|
@@ -177,7 +196,7 @@ export async function startTodoGanttDashboardServer({ registry, port = 0, redire
|
|
|
177
196
|
}
|
|
178
197
|
const project = registry.get(requestedId);
|
|
179
198
|
if (project === null) {
|
|
180
|
-
|
|
199
|
+
sendNotFound(request, response, 'PROJECT_NOT_FOUND', url.pathname);
|
|
181
200
|
return;
|
|
182
201
|
}
|
|
183
202
|
if (match[2] === 'events') {
|
package/src/todo-gantt-scope.mjs
CHANGED
|
@@ -38,10 +38,35 @@ function compareText(left, right) {
|
|
|
38
38
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* ADR 0147: phase無しplan(v1/v2/v3)は終端の暗黙Phase(terminal-audit)がacceptedになるまで
|
|
43
|
+
* 「閉じた」ことにならない。全taskがdoneでも監査未了なら、そのplanのToDoは生きた作業と同じ
|
|
44
|
+
* 扱い(distance 0)にして畳ませない——畳んでしまうと監査待ちであることが図から消え、
|
|
45
|
+
* ADR 0147が塞ごうとした「一度も重監査を通らず完走した」事故と外形が同じになる。
|
|
46
|
+
*
|
|
47
|
+
* v4/v5(phaseを宣言したplan)はこの判定の対象外にする——既存のPhase gateが重監査を担って
|
|
48
|
+
* おり、ここで同じ規律を足すとPhase単位の既存fold挙動を変えてしまう(非目標)。
|
|
49
|
+
* `phase_status`はレイアウト層(todo-gantt-layout.mjs)がplanの世代を問わず埋める。
|
|
50
|
+
* phase無しplanのtaskにはphase_idフィールド自体が無いため、そこでは暗黙Phaseの状態を
|
|
51
|
+
* 埋める。フィールドが無い/nullの入力(既存test・素のnode)は従来どおり対象外(false)になる。
|
|
52
|
+
*
|
|
53
|
+
* 対象は`gate_ready`(全task doneで監査待ち)・`reviewing`(監査中)・`rejected`
|
|
54
|
+
* (監査が通らず要フォロー)の3状態だけに絞る。`active`(一部taskがまだpending)は、
|
|
55
|
+
* 他のtaskが図に残っている限りplanが未完了だと分かるので対象にしない——ここまで
|
|
56
|
+
* 広げると、完走していない枝の通常foldまで止めてしまい既存挙動を変える。
|
|
57
|
+
*/
|
|
58
|
+
const AUDIT_PENDING_PHASE_STATUSES = new Set(['gate_ready', 'reviewing', 'rejected']);
|
|
59
|
+
function auditPending(node) {
|
|
60
|
+
if (!AUDIT_PENDING_PHASE_STATUSES.has(node.phase_status ?? null)) return false;
|
|
61
|
+
const schema = node.plan_schema ?? null;
|
|
62
|
+
return schema !== 'lattice.todo_plan.v4' && schema !== 'lattice.todo_plan.v5';
|
|
63
|
+
}
|
|
64
|
+
|
|
41
65
|
/**
|
|
42
66
|
* Forward distance from each node to the nearest live (non-done) node, over the
|
|
43
67
|
* dependency DAG. A live node is at distance 0; a node with no live descendant
|
|
44
|
-
* is at Infinity.
|
|
68
|
+
* is at Infinity. A done node whose plan's terminal audit (ADR 0147) has not
|
|
69
|
+
* been accepted is also pinned at distance 0 — it must not fold away silently.
|
|
45
70
|
*
|
|
46
71
|
* Edges always increase the wave (`assignWaves` is a longest-path layering), so
|
|
47
72
|
* visiting nodes in descending wave order guarantees every successor is settled
|
|
@@ -54,7 +79,7 @@ function distanceToLive(nodes, edges, wave) {
|
|
|
54
79
|
const ordered = [...nodes].sort((left, right) => wave.get(right.key) - wave.get(left.key)
|
|
55
80
|
|| compareText(right.key, left.key));
|
|
56
81
|
for (const node of ordered) {
|
|
57
|
-
if (node.status !== 'done') {
|
|
82
|
+
if (node.status !== 'done' || auditPending(node)) {
|
|
58
83
|
distance.set(node.key, 0);
|
|
59
84
|
continue;
|
|
60
85
|
}
|
package/src/todo-migration.mjs
CHANGED
|
@@ -126,6 +126,114 @@ function validateJoins(value) {
|
|
|
126
126
|
&& sortedStrictly(value, (join) => join.id);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
const reject = (reason, path = '') => ({ valid: false, reason, path });
|
|
130
|
+
|
|
131
|
+
/** value自体がexactRecordの対象になれる素朴なobjectかどうか(配列・nullを除く)。 */
|
|
132
|
+
function plainObject(value) {
|
|
133
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 期待keyとの過不足を1件ずつ言い当てる。missing/unexpectedのどちらが先に見つかっても
|
|
138
|
+
* そこで止め、複数の欠落を一度に説明しない——`exactRecord`のbooleanと違い、
|
|
139
|
+
* 最初に見つかった違反fieldをpathへ刻む。
|
|
140
|
+
*/
|
|
141
|
+
function explainKeys(value, requiredKeys, at) {
|
|
142
|
+
if (!plainObject(value) || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
143
|
+
return reject('not_an_object', at);
|
|
144
|
+
}
|
|
145
|
+
const actualKeys = new Set(Object.keys(value));
|
|
146
|
+
for (const key of requiredKeys) {
|
|
147
|
+
if (!actualKeys.has(key)) return reject('missing_required_key', `${at}/${key}`);
|
|
148
|
+
}
|
|
149
|
+
const requiredSet = new Set(requiredKeys);
|
|
150
|
+
for (const key of actualKeys) {
|
|
151
|
+
if (!requiredSet.has(key)) return reject('unexpected_key', `${at}/${key}`);
|
|
152
|
+
}
|
|
153
|
+
return { valid: true };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function explainSortedStrictly(values, key, at) {
|
|
157
|
+
for (let index = 1; index < values.length; index += 1) {
|
|
158
|
+
if (!(key(values[index - 1]) < key(values[index]))) {
|
|
159
|
+
return reject('unsorted_or_duplicate_collection', `${at}/${index}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { valid: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* `lattice.todo_extraction.v1/v2`を、既存の`validateTodoExtraction`の可否は変えずに
|
|
167
|
+
* 診断する(ADR 0130の案内規律をmigration入口へ拡張)。
|
|
168
|
+
*
|
|
169
|
+
* 深いgraph整合(親task解決・edge/join local参照解決)はここでは個別に言い当てず、
|
|
170
|
+
* 単一のreasonへ丸める——既知の実運用の詰まりどころ(必須key欠落・task/edge/joinの
|
|
171
|
+
* ソート違反・digest不一致)を優先して解消する。それでも掴めない違反は
|
|
172
|
+
* `diagnosis_incomplete`として正直に返す。
|
|
173
|
+
*/
|
|
174
|
+
export function explainTodoExtraction(value) {
|
|
175
|
+
try {
|
|
176
|
+
if (!plainObject(value)) return reject('not_an_object', '');
|
|
177
|
+
const schema = value.schema;
|
|
178
|
+
if (![TODO_EXTRACTION_SCHEMA, TODO_EXTRACTION_SCHEMA_V2].includes(schema)) {
|
|
179
|
+
return reject('schema_mismatch', '/schema');
|
|
180
|
+
}
|
|
181
|
+
const v2 = schema === TODO_EXTRACTION_SCHEMA_V2;
|
|
182
|
+
const topKeys = [
|
|
183
|
+
'schema', 'project_id', 'plan_key', 'plan_version', 'actor', 'recorded_at',
|
|
184
|
+
'tasks', 'hard_dependencies', 'joins', 'extraction_digest',
|
|
185
|
+
];
|
|
186
|
+
const topKeyCheck = explainKeys(value, topKeys, '');
|
|
187
|
+
if (!topKeyCheck.valid) return topKeyCheck;
|
|
188
|
+
if (!isTodoIdentifier(value.project_id)) return reject('invalid_identifier', '/project_id');
|
|
189
|
+
if (!isTodoIdentifier(value.plan_key)) return reject('invalid_identifier', '/plan_key');
|
|
190
|
+
if (!isTodoIdentifier(value.plan_version)) return reject('invalid_identifier', '/plan_version');
|
|
191
|
+
if (!actor(value.actor)) return reject('invalid_actor', '/actor');
|
|
192
|
+
if (!isStrictTodoTimestamp(value.recorded_at)) return reject('invalid_timestamp', '/recorded_at');
|
|
193
|
+
if (!Array.isArray(value.tasks) || value.tasks.length === 0
|
|
194
|
+
|| value.tasks.length > TODO_LIMITS.tasksPerPlan) {
|
|
195
|
+
return reject('bounded_collection_violation', '/tasks');
|
|
196
|
+
}
|
|
197
|
+
const taskKeys = v2
|
|
198
|
+
? ['task_id', 'title', 'lane', 'narrative_ref', 'compile_binding', 'disposition',
|
|
199
|
+
'start', 'completion', 'source', 'migration_context']
|
|
200
|
+
: ['task_id', 'title', 'lane', 'narrative_ref', 'compile_binding', 'disposition',
|
|
201
|
+
'completion', 'source', 'migration_context'];
|
|
202
|
+
for (const [index, task] of value.tasks.entries()) {
|
|
203
|
+
const taskKeyCheck = explainKeys(task, taskKeys, `/tasks/${index}`);
|
|
204
|
+
if (!taskKeyCheck.valid) return taskKeyCheck;
|
|
205
|
+
if (!extractionTask(task, schema)) {
|
|
206
|
+
return reject('task_shape_invalid', `/tasks/${index}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const sortCheck = explainSortedStrictly(value.tasks, (task) => task.task_id, '/tasks');
|
|
210
|
+
if (!sortCheck.valid) return sortCheck;
|
|
211
|
+
if (new Set(value.tasks.map(({ task_id: taskId }) => taskId)).size !== value.tasks.length) {
|
|
212
|
+
return reject('duplicate_task_id', '/tasks');
|
|
213
|
+
}
|
|
214
|
+
if (!validateEdges(value.hard_dependencies)) return reject('hard_dependencies_invalid', '/hard_dependencies');
|
|
215
|
+
if (!validateJoins(value.joins)) return reject('joins_invalid', '/joins');
|
|
216
|
+
if (!isTodoDigest(value.extraction_digest)) return reject('invalid_digest', '/extraction_digest');
|
|
217
|
+
const expectedDigest = todoSelfDigest(value, 'extraction_digest');
|
|
218
|
+
if (value.extraction_digest !== expectedDigest) {
|
|
219
|
+
return reject('extraction_digest_mismatch', '/extraction_digest');
|
|
220
|
+
}
|
|
221
|
+
const taskIds = new Set(value.tasks.map(({ task_id: taskId }) => taskId));
|
|
222
|
+
const badParent = value.tasks.find((task) => task.source.parent_task_id === task.task_id
|
|
223
|
+
|| (task.source.parent_task_id !== null && !taskIds.has(task.source.parent_task_id)));
|
|
224
|
+
if (badParent !== undefined) {
|
|
225
|
+
return reject('parent_task_id_unresolved',
|
|
226
|
+
`/tasks/${value.tasks.indexOf(badParent)}/source/parent_task_id`);
|
|
227
|
+
}
|
|
228
|
+
if (!localRefsResolve(value)) return reject('local_ref_unresolved', '');
|
|
229
|
+
// ここまでの個別検査を全て通過したのに`validateTodoExtraction`がfalseを返す状況は、
|
|
230
|
+
// このexplainがまだ言い当てられない違反があるということ。捏造せず未特定と申告する。
|
|
231
|
+
return { valid: true };
|
|
232
|
+
} catch {
|
|
233
|
+
return reject('diagnosis_failed', '');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
129
237
|
function registeredTaskIds(value) {
|
|
130
238
|
return new Set(value.tasks
|
|
131
239
|
.filter(({ disposition }) => disposition.startsWith('register_'))
|