@quolu/lattice 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +28 -1
- package/README.md +8 -0
- package/bin/lattice.mjs +9 -1
- package/docs/bridge-setup.md +13 -0
- package/package.json +1 -1
- package/src/cli-help.mjs +12 -7
- package/src/project-cli.mjs +103 -1
- package/src/todo-cli.mjs +103 -10
- package/src/todo-contracts.mjs +2 -2
- package/src/todo-gantt-html-shared.mjs +2 -1
- package/src/todo-gantt-layout.mjs +13 -3
- package/src/todo-gantt-scope.mjs +27 -2
- package/src/todo-migration.mjs +108 -0
- package/src/todo-revision.mjs +568 -2
- package/src/todo-status.mjs +5 -2
- package/src/todo-store.mjs +155 -33
package/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_'))
|