@quolu/lattice 0.57.2 → 0.58.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.
@@ -0,0 +1,599 @@
1
+ import { DagCycleError, analyzeDagChains, analyzeDagReachability } from './dag-chain.mjs';
2
+ import { digestTodoArtifact, todoSelfDigest } from './todo-contracts.mjs';
3
+ import {
4
+ TODO_STRUCTURE_GIT_PROVENANCE_SCHEMA,
5
+ TodoStructureGitError,
6
+ bindTodoStructureRealizationCommits,
7
+ } from './todo-structure-git-adapter.mjs';
8
+ import {
9
+ TODO_STRUCTURE_SOURCE_PROJECTION_SCHEMA,
10
+ } from './todo-structure-source-adapter.mjs';
11
+ import {
12
+ digestTodoStructureTransform,
13
+ explainTodoStructureRealization,
14
+ explainTodoStructureSet,
15
+ } from './todo-structure-contracts.mjs';
16
+ import { projectTodoChainV1, projectTodoTopologyDagV1 } from './todo-chain.mjs';
17
+
18
+ export const TODO_STRUCTURE_OVERLAY_SCHEMA = 'lattice.todo_structure_overlay.v1';
19
+ export const TODO_STRUCTURE_FINDING_LIMIT = 1_024;
20
+
21
+ const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
22
+ const isPlain = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
23
+ const canonicalCompare = (left, right) => compareText(
24
+ JSON.stringify(left), JSON.stringify(right),
25
+ );
26
+
27
+ export class TodoStructureOverlayError extends Error {
28
+ constructor(code, reason, detail = {}) {
29
+ super(reason);
30
+ this.name = 'TodoStructureOverlayError';
31
+ this.code = code;
32
+ this.detail = { reason, ...detail };
33
+ }
34
+ }
35
+
36
+ function fail(code, reason, detail = {}) {
37
+ throw new TodoStructureOverlayError(code, reason, detail);
38
+ }
39
+
40
+ function assertInputs(structureSet, sourceProjection, gitProvenance) {
41
+ const structure = explainTodoStructureSet(structureSet);
42
+ if (!structure.valid) fail('STRUCTURE_OVERLAY_INPUT_INVALID', structure.reason, { path: structure.path });
43
+ if (!isPlain(sourceProjection)
44
+ || sourceProjection.schema !== TODO_STRUCTURE_SOURCE_PROJECTION_SCHEMA
45
+ || sourceProjection.structure_set_digest !== structureSet.structure_set_digest
46
+ || sourceProjection.projection_digest !== todoSelfDigest(sourceProjection, 'projection_digest')) {
47
+ fail('STRUCTURE_OVERLAY_SOURCE_INVALID', 'source_projection_invalid');
48
+ }
49
+ if (!isPlain(gitProvenance)
50
+ || gitProvenance.schema !== TODO_STRUCTURE_GIT_PROVENANCE_SCHEMA
51
+ || gitProvenance.structure_set_digest !== structureSet.structure_set_digest
52
+ || gitProvenance.provenance_digest !== todoSelfDigest(gitProvenance, 'provenance_digest')) {
53
+ fail('STRUCTURE_OVERLAY_PROVENANCE_INVALID', 'git_provenance_invalid');
54
+ }
55
+ }
56
+
57
+ function normalizeTaskStates(structureSet, taskStates) {
58
+ if (!Array.isArray(taskStates)) fail('STRUCTURE_OVERLAY_INPUT_INVALID', 'task_states_invalid');
59
+ const expected = new Set(structureSet.tasks.map(({ task_id: id }) => id));
60
+ const states = new Map();
61
+ for (const entry of taskStates) {
62
+ if (!isPlain(entry) || Object.keys(entry).length !== 2
63
+ || typeof entry.task_id !== 'string'
64
+ || !['pending', 'in-progress', 'blocked', 'done'].includes(entry.status)
65
+ || !expected.has(entry.task_id) || states.has(entry.task_id)) {
66
+ fail('STRUCTURE_OVERLAY_INPUT_INVALID', 'task_state_entry_invalid');
67
+ }
68
+ states.set(entry.task_id, entry.status);
69
+ }
70
+ if (states.size !== expected.size) fail('STRUCTURE_OVERLAY_INPUT_INVALID', 'task_state_coverage_invalid');
71
+ return states;
72
+ }
73
+
74
+ function normalizeRealizations(structureSet, realizations) {
75
+ if (!Array.isArray(realizations)) fail('STRUCTURE_OVERLAY_INPUT_INVALID', 'realizations_invalid');
76
+ const grouped = new Map();
77
+ for (const realization of realizations) {
78
+ const taskEntries = grouped.get(realization?.task_id) ?? [];
79
+ taskEntries.push(realization);
80
+ grouped.set(realization?.task_id, taskEntries);
81
+ }
82
+ const latest = new Map();
83
+ for (const [taskId, entries] of grouped) {
84
+ entries.sort((left, right) => left.sequence - right.sequence);
85
+ let previous = null;
86
+ const priorDigests = new Set();
87
+ for (const realization of entries) {
88
+ const explained = explainTodoStructureRealization(realization, {
89
+ structureSet, previous, priorDigests,
90
+ });
91
+ if (!explained.valid) {
92
+ fail('STRUCTURE_OVERLAY_REALIZATION_INVALID', explained.reason, {
93
+ task_id: taskId ?? null, path: explained.path,
94
+ });
95
+ }
96
+ priorDigests.add(realization.realization_digest);
97
+ previous = realization;
98
+ }
99
+ latest.set(taskId, previous);
100
+ }
101
+ return { grouped, latest };
102
+ }
103
+
104
+ function finding({ code, severity, taskIds = [], dataRefs = [], codeRefs = [], commitOids = [],
105
+ observed = null, expected = null, nextAction }) {
106
+ return {
107
+ code,
108
+ severity,
109
+ task_ids: [...new Set(taskIds)].sort(compareText),
110
+ data_refs: [...new Set(dataRefs)].sort(compareText),
111
+ code_refs: [...new Set(codeRefs)].sort(compareText),
112
+ commit_oids: [...new Set(commitOids)].sort(compareText),
113
+ evidence: { observed, expected },
114
+ next_action: nextAction,
115
+ };
116
+ }
117
+
118
+ function mergeFindings(raw) {
119
+ const grouped = new Map();
120
+ for (const entry of raw) {
121
+ const key = digestTodoArtifact({
122
+ code: entry.code, severity: entry.severity, evidence: entry.evidence,
123
+ next_action: entry.next_action,
124
+ });
125
+ const current = grouped.get(key);
126
+ if (current === undefined) {
127
+ grouped.set(key, structuredClone(entry));
128
+ continue;
129
+ }
130
+ for (const [field, values] of [
131
+ ['task_ids', entry.task_ids], ['data_refs', entry.data_refs],
132
+ ['code_refs', entry.code_refs], ['commit_oids', entry.commit_oids],
133
+ ]) current[field] = [...new Set([...current[field], ...values])].sort(compareText);
134
+ }
135
+ const merged = [...grouped.values()].sort((left, right) => compareText(left.code, right.code)
136
+ || canonicalCompare(left.evidence, right.evidence));
137
+ return merged.map((entry) => {
138
+ const value = { ...entry, finding_digest: '' };
139
+ value.finding_digest = todoSelfDigest(value, 'finding_digest');
140
+ return value;
141
+ });
142
+ }
143
+
144
+ function contractsDiffer(producer, consumer) {
145
+ const fields = [];
146
+ const shapeCompatible = producer.shape_id === consumer.shape_id
147
+ || consumer.compatible_shape_ids.includes(producer.shape_id);
148
+ if (!shapeCompatible) fields.push('shape_id');
149
+ if (JSON.stringify(producer.identity_fields) !== JSON.stringify(consumer.identity_fields)) {
150
+ fields.push('identity_fields');
151
+ }
152
+ if (producer.lifecycle !== consumer.lifecycle) fields.push('lifecycle');
153
+ if (producer.cardinality !== consumer.cardinality) fields.push('cardinality');
154
+ return fields;
155
+ }
156
+
157
+ function nextActionForAnchor(code) {
158
+ if (code === 'STRUCTURE_SENSOR_UNREADY') return 'refresh_sensor_index_then_recompile_structure';
159
+ if (code === 'STRUCTURE_CODE_ANCHOR_AMBIGUOUS') return 'replace_symbol_with_an_exact_qualified_anchor';
160
+ if (code === 'STRUCTURE_CODE_ANCHOR_TIME_DEFERRED') return 'compile_the_required_baseline_or_task_overlay';
161
+ return 'correct_the_code_anchor_or_source_then_recompile_structure';
162
+ }
163
+
164
+ function anchorFindings(structureSet, sourceProjection) {
165
+ const findings = [];
166
+ const graphTaskIds = structureSet.tasks
167
+ .filter(({ applicability }) => applicability === 'graph').map(({ task_id: id }) => id);
168
+ if (sourceProjection.sensor_status.outcome !== 'ready') {
169
+ findings.push(finding({
170
+ code: 'STRUCTURE_SENSOR_UNREADY', severity: 'unknown', taskIds: graphTaskIds,
171
+ observed: sourceProjection.sensor_status.outcome, expected: 'ready',
172
+ nextAction: nextActionForAnchor('STRUCTURE_SENSOR_UNREADY'),
173
+ }));
174
+ return findings;
175
+ }
176
+ for (const anchor of sourceProjection.anchors) {
177
+ if (anchor.verdict === 'consistent') continue;
178
+ findings.push(finding({
179
+ code: anchor.reason ?? 'STRUCTURE_INPUT_UNRESOLVED',
180
+ severity: anchor.verdict === 'inconsistent' ? 'error' : 'unknown',
181
+ taskIds: [anchor.task_id], codeRefs: [`${anchor.task_id}/${anchor.anchor_id}`],
182
+ observed: {
183
+ existence: anchor.existence, coverage: anchor.coverage,
184
+ candidates: anchor.candidates ?? [], edge_state: anchor.edges.state,
185
+ },
186
+ expected: { effect: anchor.effect, expected_at: anchor.expected_at },
187
+ nextAction: nextActionForAnchor(anchor.reason),
188
+ }));
189
+ }
190
+ return findings;
191
+ }
192
+
193
+ function sourceRefKey(taskId, source) {
194
+ if (source.kind === 'task_output') return `data:${source.task_id}/${source.port_id}`;
195
+ if (source.kind === 'code') return `code:${taskId}/${source.anchor_id}`;
196
+ if (source.kind === 'external') return `external:${source.contract_id}`;
197
+ return `constant:${taskId}/${source.constant_id}`;
198
+ }
199
+
200
+ function sinkRefKey(taskId, sink) {
201
+ if (sink.kind === 'task') return `task:${sink.task_id}`;
202
+ if (sink.kind === 'code') return `code:${taskId}/${sink.anchor_id}`;
203
+ if (sink.kind === 'external') return `external:${sink.contract_id}`;
204
+ return `final:${sink.product_id}`;
205
+ }
206
+
207
+ function sourceNeighborRef(edge) {
208
+ return `source:${digestTodoArtifact(edge).slice(0, 32)}`;
209
+ }
210
+
211
+ function buildOverlayGraph(structureSet, states, latestRealizations, sourceProjection, gitProvenance) {
212
+ const nodes = [];
213
+ const edges = [];
214
+ const effective = new Map();
215
+ for (const task of structureSet.tasks) {
216
+ if (task.applicability !== 'graph') continue;
217
+ const realization = latestRealizations.get(task.task_id) ?? null;
218
+ const transform = realization?.realized ?? task.planned;
219
+ effective.set(task.task_id, transform);
220
+ nodes.push({
221
+ kind: 'task_transform', ref: `task:${task.task_id}`, task_id: task.task_id,
222
+ state: states.get(task.task_id), form: realization === null ? 'planned' : 'realized',
223
+ transform_digest: digestTodoStructureTransform(transform),
224
+ });
225
+ for (const anchor of transform.code_anchors) {
226
+ const observed = sourceProjection.anchors.find((entry) => entry.task_id === task.task_id
227
+ && entry.anchor_id === anchor.anchor_id) ?? null;
228
+ nodes.push({
229
+ kind: 'code', ref: `code:${task.task_id}/${anchor.anchor_id}`,
230
+ task_id: task.task_id, anchor_id: anchor.anchor_id,
231
+ natural_ref: observed?.node ?? null, observation: observed?.verdict ?? 'unknown',
232
+ });
233
+ if (observed !== null) {
234
+ for (const incoming of observed.edges.incoming) {
235
+ const neighbor = sourceNeighborRef(incoming);
236
+ nodes.push({ kind: 'source_symbol', ref: neighbor, natural_ref: structuredClone(incoming) });
237
+ edges.push({
238
+ kind: 'source_edge', from: neighbor, to: `code:${task.task_id}/${anchor.anchor_id}`,
239
+ edge_kind: incoming.edge_kind,
240
+ });
241
+ }
242
+ for (const outgoing of observed.edges.outgoing) {
243
+ const neighbor = sourceNeighborRef(outgoing);
244
+ nodes.push({ kind: 'source_symbol', ref: neighbor, natural_ref: structuredClone(outgoing) });
245
+ edges.push({
246
+ kind: 'source_edge', from: `code:${task.task_id}/${anchor.anchor_id}`, to: neighbor,
247
+ edge_kind: outgoing.edge_kind,
248
+ });
249
+ }
250
+ }
251
+ }
252
+ for (const input of transform.inputs) {
253
+ edges.push({
254
+ kind: 'input', from: sourceRefKey(task.task_id, input.source),
255
+ to: `task:${task.task_id}`, port_id: input.port_id,
256
+ });
257
+ if (input.source.kind === 'constant') {
258
+ nodes.push({ kind: 'constant', ref: sourceRefKey(task.task_id, input.source) });
259
+ }
260
+ }
261
+ for (const output of transform.outputs) {
262
+ const dataRef = `data:${task.task_id}/${output.port_id}`;
263
+ nodes.push({
264
+ kind: 'data', ref: dataRef, task_id: task.task_id, port_id: output.port_id,
265
+ data_id: output.data_id, contract: structuredClone(output.contract),
266
+ });
267
+ edges.push({ kind: 'output', from: `task:${task.task_id}`, to: dataRef, port_id: output.port_id });
268
+ for (const sink of output.sinks) {
269
+ const sinkRef = sinkRefKey(task.task_id, sink);
270
+ edges.push({
271
+ kind: 'sink', from: dataRef, to: sinkRef, port_id: output.port_id,
272
+ target_port_id: sink.kind === 'task' ? sink.port_id : null,
273
+ });
274
+ if (sink.kind === 'final_product') nodes.push({ kind: 'final_product', ref: sinkRef });
275
+ }
276
+ }
277
+ }
278
+ for (const external of structureSet.external_contracts) {
279
+ nodes.push({
280
+ kind: 'external', ref: `external:${external.contract_id}`,
281
+ contract_id: external.contract_id, contract: structuredClone(external.contract),
282
+ });
283
+ }
284
+ for (const changeset of gitProvenance.changesets) {
285
+ nodes.push({
286
+ kind: 'changeset', ref: `commit:${changeset.commit_oid}`,
287
+ commit_oid: changeset.commit_oid, changeset_digest: changeset.changeset_digest,
288
+ });
289
+ }
290
+ for (const [taskId, realization] of latestRealizations) {
291
+ for (const commitOid of realization.commit_oids) {
292
+ edges.push({
293
+ kind: 'realization', from: `commit:${commitOid}`, to: `task:${taskId}`,
294
+ realization_digest: realization.realization_digest,
295
+ });
296
+ }
297
+ }
298
+ const uniqueNodes = [...new Map(nodes.map((node) => [`${node.kind}\0${node.ref}`, node])).values()];
299
+ uniqueNodes.sort((left, right) => compareText(left.ref, right.ref) || compareText(left.kind, right.kind));
300
+ edges.sort((left, right) => compareText(left.from, right.from)
301
+ || compareText(left.to, right.to) || compareText(left.kind, right.kind));
302
+ return { nodes: uniqueNodes, edges, effective };
303
+ }
304
+
305
+ function connectionFindings(structureSet, effective, topologyDag) {
306
+ const findings = [];
307
+ const externalById = new Map(structureSet.external_contracts
308
+ .map((external) => [external.contract_id, external]));
309
+ const topologyByTask = new Map(topologyDag.nodes
310
+ .filter(({ ref }) => ref.project_id === structureSet.project_id
311
+ && ref.plan_key === structureSet.plan_key)
312
+ .map(({ key, ref }) => [ref.task_id, key]));
313
+ for (const task of structureSet.tasks) {
314
+ if (!topologyByTask.has(task.task_id)) {
315
+ findings.push(finding({
316
+ code: 'STRUCTURE_COVERAGE_MISSING', severity: 'error', taskIds: [task.task_id],
317
+ observed: null, expected: { topology_task: task.task_id },
318
+ nextAction: 'repair_the_registered_todo_topology',
319
+ }));
320
+ }
321
+ }
322
+ const structureTaskIds = new Set(structureSet.tasks.map(({ task_id: id }) => id));
323
+ const extraTopologyTasks = topologyDag.nodes
324
+ .filter(({ ref }) => ref.project_id === structureSet.project_id
325
+ && ref.plan_key === structureSet.plan_key && !structureTaskIds.has(ref.task_id))
326
+ .map(({ ref }) => ref.task_id).sort(compareText);
327
+ if (extraTopologyTasks.length > 0) findings.push(finding({
328
+ code: 'STRUCTURE_COVERAGE_MISSING', severity: 'error', taskIds: extraTopologyTasks,
329
+ observed: { topology_only_task_ids: extraTopologyTasks }, expected: 'graph or excluded structure entry',
330
+ nextAction: 'add_structure_entries_for_every_registered_todo',
331
+ }));
332
+
333
+ const relations = [];
334
+ const dataEdges = [];
335
+ for (const [consumerId, transform] of effective) {
336
+ for (const input of transform.inputs) {
337
+ if (input.source.kind === 'external') {
338
+ const external = externalById.get(input.source.contract_id);
339
+ const fields = contractsDiffer(external.contract, input.contract);
340
+ if (fields.length > 0) findings.push(finding({
341
+ code: 'STRUCTURE_CONTRACT_MISMATCH', severity: 'error', taskIds: [consumerId],
342
+ dataRefs: [`external/${external.contract_id}`, `${consumerId}/${input.port_id}`],
343
+ observed: { producer: external.contract, mismatched_fields: fields },
344
+ expected: { consumer: input.contract },
345
+ nextAction: 'align_the_external_and_consumer_data_contracts',
346
+ }));
347
+ }
348
+ if (input.source.kind !== 'task_output') continue;
349
+ const producerId = input.source.task_id;
350
+ const producer = effective.get(producerId);
351
+ const output = producer?.outputs.find(({ port_id: id }) => id === input.source.port_id) ?? null;
352
+ if (output === null) {
353
+ findings.push(finding({
354
+ code: 'STRUCTURE_INPUT_UNRESOLVED', severity: 'error',
355
+ taskIds: [producerId, consumerId],
356
+ dataRefs: [`${producerId}/${input.source.port_id}`, `${consumerId}/${input.port_id}`],
357
+ observed: null, expected: input.source,
358
+ nextAction: 'correct_the_task_output_reference',
359
+ }));
360
+ continue;
361
+ }
362
+ dataEdges.push([producerId, consumerId]);
363
+ const fields = contractsDiffer(output.contract, input.contract);
364
+ if (fields.length > 0) {
365
+ findings.push(finding({
366
+ code: 'STRUCTURE_CONTRACT_MISMATCH', severity: 'error',
367
+ taskIds: [producerId, consumerId],
368
+ dataRefs: [`${producerId}/${output.port_id}`, `${consumerId}/${input.port_id}`],
369
+ observed: { producer: output.contract, mismatched_fields: fields },
370
+ expected: { consumer: input.contract },
371
+ nextAction: 'align_the_producer_and_consumer_data_contracts',
372
+ }));
373
+ }
374
+ const from = topologyByTask.get(producerId);
375
+ const to = topologyByTask.get(consumerId);
376
+ if (from !== undefined && to !== undefined) relations.push({ from, to, producerId, consumerId });
377
+ }
378
+ }
379
+ if (relations.length > 0) {
380
+ const reachable = analyzeDagReachability(
381
+ topologyDag.nodes.map(({ key }) => key),
382
+ topologyDag.edges.map(({ from, to }) => [from, to]),
383
+ relations.map(({ from, to }) => [from, to]),
384
+ );
385
+ relations.forEach((relation, index) => {
386
+ if (reachable[index]) return;
387
+ findings.push(finding({
388
+ code: 'STRUCTURE_DEPENDENCY_MISSING', severity: 'error',
389
+ taskIds: [relation.producerId, relation.consumerId],
390
+ observed: { reachable: false },
391
+ expected: { source_task_id: relation.producerId, target_task_id: relation.consumerId },
392
+ nextAction: 'add_a_todo_dependency_from_source_task_to_target_task',
393
+ }));
394
+ });
395
+ }
396
+ try {
397
+ analyzeDagChains([...effective.keys()], dataEdges, { representativeLimit: 0 });
398
+ } catch (error) {
399
+ if (!(error instanceof DagCycleError)) throw error;
400
+ findings.push(finding({
401
+ code: 'STRUCTURE_GRAPH_CYCLE', severity: 'error', taskIds: [...effective.keys()],
402
+ observed: { data_edges: dataEdges }, expected: 'acyclic task dataflow',
403
+ nextAction: 'remove_the_cyclic_task_output_connection',
404
+ }));
405
+ }
406
+
407
+ const consumed = new Set();
408
+ for (const transform of effective.values()) {
409
+ for (const input of transform.inputs) {
410
+ if (input.source.kind === 'task_output') {
411
+ consumed.add(`${input.source.task_id}/${input.source.port_id}`);
412
+ }
413
+ }
414
+ }
415
+ for (const [taskId, transform] of effective) {
416
+ for (const output of transform.outputs) {
417
+ const dataRef = `${taskId}/${output.port_id}`;
418
+ if (output.sinks.length === 0 && !consumed.has(dataRef)) {
419
+ findings.push(finding({
420
+ code: 'STRUCTURE_OUTPUT_ORPHANED', severity: 'error', taskIds: [taskId],
421
+ dataRefs: [dataRef], observed: { sinks: [], consumers: [] },
422
+ expected: 'at least one consumer or explicit sink',
423
+ nextAction: 'connect_the_output_or_declare_its_explicit_sink',
424
+ }));
425
+ }
426
+ for (const sink of output.sinks.filter(({ kind }) => kind === 'task')) {
427
+ const consumer = effective.get(sink.task_id);
428
+ const input = consumer?.inputs.find(({ port_id: id }) => id === sink.port_id) ?? null;
429
+ if (input?.source.kind === 'task_output'
430
+ && input.source.task_id === taskId && input.source.port_id === output.port_id) continue;
431
+ findings.push(finding({
432
+ code: 'STRUCTURE_INPUT_UNRESOLVED', severity: 'error',
433
+ taskIds: [taskId, sink.task_id], dataRefs: [dataRef, `${sink.task_id}/${sink.port_id}`],
434
+ observed: input?.source ?? null,
435
+ expected: { kind: 'task_output', task_id: taskId, port_id: output.port_id },
436
+ nextAction: 'make_the_output_sink_and_consumer_source_reciprocal',
437
+ }));
438
+ }
439
+ for (const sink of output.sinks.filter(({ kind }) => kind === 'external')) {
440
+ const external = externalById.get(sink.contract_id);
441
+ const fields = contractsDiffer(output.contract, external.contract);
442
+ if (fields.length > 0) findings.push(finding({
443
+ code: 'STRUCTURE_CONTRACT_MISMATCH', severity: 'error', taskIds: [taskId],
444
+ dataRefs: [dataRef, `external/${external.contract_id}`],
445
+ observed: { producer: output.contract, mismatched_fields: fields },
446
+ expected: { consumer: external.contract },
447
+ nextAction: 'align_the_output_and_external_data_contracts',
448
+ }));
449
+ }
450
+ }
451
+ }
452
+ return findings;
453
+ }
454
+
455
+ function realizationFindings(structureSet, states, realizations, gitProvenance, effective) {
456
+ const findings = [];
457
+ const changesets = new Map(gitProvenance.changesets.map((entry) => [entry.commit_oid, entry]));
458
+ for (const task of structureSet.tasks) {
459
+ if (task.applicability !== 'graph') continue;
460
+ const state = states.get(task.task_id);
461
+ const realization = realizations.latest.get(task.task_id) ?? null;
462
+ if (realization === null) {
463
+ if (state === 'done') {
464
+ findings.push(finding({
465
+ code: 'STRUCTURE_REALIZATION_MISSING', severity: 'error', taskIds: [task.task_id],
466
+ observed: null, expected: 'fresh task realization',
467
+ nextAction: 'record_the_task_realization_before_done',
468
+ }));
469
+ } else if (state === 'in-progress') {
470
+ findings.push(finding({
471
+ code: 'STRUCTURE_COMMIT_UNBOUND', severity: 'unknown', taskIds: [task.task_id],
472
+ observed: { commits: [] }, expected: 'explicit realization commit OIDs when implementation exists',
473
+ nextAction: 'record_a_realization_when_the_task_has_committed_implementation',
474
+ }));
475
+ }
476
+ continue;
477
+ }
478
+ let bound;
479
+ try {
480
+ bound = bindTodoStructureRealizationCommits({
481
+ provenance: gitProvenance, realizations: [realization],
482
+ })[0];
483
+ } catch (error) {
484
+ if (!(error instanceof TodoStructureGitError)
485
+ || error.code !== 'STRUCTURE_REALIZATION_COMMIT_UNREACHABLE') throw error;
486
+ findings.push(finding({
487
+ code: 'STRUCTURE_COMMIT_UNBOUND', severity: 'error', taskIds: [task.task_id],
488
+ commitOids: realization.commit_oids,
489
+ observed: { baseline_range: gitProvenance.commit_order },
490
+ expected: { commit_oids: realization.commit_oids },
491
+ nextAction: 'replace_or_fetch_the_realization_commit_then_recompile',
492
+ }));
493
+ continue;
494
+ }
495
+ const changedPaths = new Set(bound.commits.flatMap(({ commit_oid: oid }) => (
496
+ changesets.get(oid)?.changes.map(({ path }) => path) ?? []
497
+ )));
498
+ const unboundAnchors = effective.get(task.task_id).code_anchors
499
+ .filter(({ effect }) => effect !== 'read')
500
+ .filter(({ path }) => !changedPaths.has(path));
501
+ if (unboundAnchors.length > 0) {
502
+ findings.push(finding({
503
+ code: 'STRUCTURE_COMMIT_UNBOUND', severity: 'unknown', taskIds: [task.task_id],
504
+ codeRefs: unboundAnchors.map(({ anchor_id: id }) => `${task.task_id}/${id}`),
505
+ commitOids: realization.commit_oids,
506
+ observed: { changed_paths: [...changedPaths].sort(compareText) },
507
+ expected: { anchor_paths: unboundAnchors.map(({ path }) => path).sort(compareText) },
508
+ nextAction: 'correct_the_realization_commits_or_realized_code_anchors',
509
+ }));
510
+ }
511
+ }
512
+ return findings;
513
+ }
514
+
515
+ /** 四層の既存artifactを結合し、structure固有findingだけを導出するpure compiler。 */
516
+ export function compileTodoStructureOverlay({
517
+ structureSet, topology, taskStates, sourceProjection, gitProvenance, realizations = [],
518
+ } = {}) {
519
+ assertInputs(structureSet, sourceProjection, gitProvenance);
520
+ const states = normalizeTaskStates(structureSet, taskStates);
521
+ const normalizedRealizations = normalizeRealizations(structureSet, realizations);
522
+ let todoChain;
523
+ let topologyDag;
524
+ try {
525
+ todoChain = projectTodoChainV1(topology);
526
+ topologyDag = projectTodoTopologyDagV1(topology);
527
+ } catch (error) {
528
+ if (error?.code !== 'TODO_CHAIN_CYCLE') throw error;
529
+ const cycleFinding = mergeFindings([finding({
530
+ code: 'STRUCTURE_GRAPH_CYCLE', severity: 'error',
531
+ taskIds: structureSet.tasks.map(({ task_id: id }) => id),
532
+ observed: 'cyclic registered todo topology', expected: 'acyclic registered todo topology',
533
+ nextAction: 'repair_the_registered_todo_topology_cycle',
534
+ })]);
535
+ const result = {
536
+ schema: TODO_STRUCTURE_OVERLAY_SCHEMA,
537
+ structure_set_digest: structureSet.structure_set_digest,
538
+ source_projection_digest: sourceProjection.projection_digest,
539
+ git_provenance_digest: gitProvenance.provenance_digest,
540
+ todo_chain: null, graph: { nodes: [], edges: [] },
541
+ verdict: 'inconsistent', findings: cycleFinding,
542
+ finding_summary: { total: 1, returned: 1, omitted: 0, errors: 1, unknowns: 0, notices: 0 },
543
+ overlay_digest: '',
544
+ };
545
+ result.overlay_digest = todoSelfDigest(result, 'overlay_digest');
546
+ return result;
547
+ }
548
+ const graph = buildOverlayGraph(
549
+ structureSet, states, normalizedRealizations.latest, sourceProjection, gitProvenance,
550
+ );
551
+ const sourceAnchorKeys = new Set(sourceProjection.anchors
552
+ .map(({ task_id: taskId, anchor_id: anchorId }) => `${taskId}\0${anchorId}`));
553
+ const missingEffectiveAnchors = [...graph.effective].flatMap(([taskId, transform]) => (
554
+ transform.code_anchors
555
+ .filter(({ anchor_id: anchorId }) => !sourceAnchorKeys.has(`${taskId}\0${anchorId}`))
556
+ .map(({ anchor_id: anchorId }) => finding({
557
+ code: 'STRUCTURE_INPUT_UNRESOLVED', severity: 'unknown', taskIds: [taskId],
558
+ codeRefs: [`${taskId}/${anchorId}`], observed: null,
559
+ expected: 'source observation for the effective code anchor',
560
+ nextAction: 'recompile_source_evidence_for_the_effective_realization',
561
+ }))
562
+ ));
563
+ const rawFindings = [
564
+ ...anchorFindings(structureSet, sourceProjection),
565
+ ...missingEffectiveAnchors,
566
+ ...connectionFindings(structureSet, graph.effective, topologyDag),
567
+ ...realizationFindings(
568
+ structureSet, states, normalizedRealizations, gitProvenance, graph.effective,
569
+ ),
570
+ ];
571
+ const allFindings = mergeFindings(rawFindings);
572
+ const errors = allFindings.filter(({ severity }) => severity === 'error').length;
573
+ const unknowns = allFindings.filter(({ severity }) => severity === 'unknown').length;
574
+ const notices = allFindings.filter(({ severity }) => severity === 'notice').length;
575
+ const overlay = {
576
+ schema: TODO_STRUCTURE_OVERLAY_SCHEMA,
577
+ structure_set_digest: structureSet.structure_set_digest,
578
+ source_projection_digest: sourceProjection.projection_digest,
579
+ git_provenance_digest: gitProvenance.provenance_digest,
580
+ todo_chain: {
581
+ schema: todoChain.schema,
582
+ maximum_dependency_depth: todoChain.maximum_dependency_depth,
583
+ longest_chain_count: todoChain.longest_chain_count,
584
+ chain_digest: digestTodoArtifact(todoChain),
585
+ },
586
+ graph: { nodes: graph.nodes, edges: graph.edges },
587
+ verdict: errors > 0 ? 'inconsistent' : unknowns > 0 ? 'unknown' : 'consistent',
588
+ findings: allFindings.slice(0, TODO_STRUCTURE_FINDING_LIMIT),
589
+ finding_summary: {
590
+ total: allFindings.length,
591
+ returned: Math.min(allFindings.length, TODO_STRUCTURE_FINDING_LIMIT),
592
+ omitted: Math.max(0, allFindings.length - TODO_STRUCTURE_FINDING_LIMIT),
593
+ errors, unknowns, notices,
594
+ },
595
+ overlay_digest: '',
596
+ };
597
+ overlay.overlay_digest = todoSelfDigest(overlay, 'overlay_digest');
598
+ return overlay;
599
+ }