@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.
- package/README.ja.md +32 -0
- package/README.md +33 -0
- package/docs/schemas/lattice.todo_structure_binding.v1.schema.json +47 -0
- package/docs/schemas/lattice.todo_structure_realization.v1.schema.json +55 -0
- package/docs/schemas/lattice.todo_structure_set.v1.schema.json +264 -0
- package/package.json +4 -1
- package/sensor/dist/bin/exact-traversal.d.ts +20 -0
- package/sensor/dist/bin/exact-traversal.d.ts.map +1 -0
- package/sensor/dist/bin/exact-traversal.js +17 -0
- package/sensor/dist/bin/exact-traversal.js.map +1 -0
- package/sensor/dist/bin/lattice-sensor.js +27 -8
- package/sensor/package.json +1 -1
- package/src/cli-help.mjs +16 -3
- package/src/dag-chain.mjs +37 -0
- package/src/project-cli.mjs +9 -3
- package/src/runtime-pull-intake.mjs +8 -4
- package/src/sensor-adapter.mjs +8 -2
- package/src/todo-chain.mjs +19 -5
- package/src/todo-cli.mjs +612 -7
- package/src/todo-gantt-html-independence.mjs +9 -1
- package/src/todo-gantt-html-style.mjs +8 -0
- package/src/todo-gantt-html.mjs +14 -4
- package/src/todo-gantt-structure.mjs +134 -0
- package/src/todo-status.mjs +32 -6
- package/src/todo-store.mjs +645 -27
- package/src/todo-structure-contracts.mjs +706 -0
- package/src/todo-structure-git-adapter.mjs +452 -0
- package/src/todo-structure-overlay.mjs +599 -0
- package/src/todo-structure-presentation.mjs +163 -0
- package/src/todo-structure-source-adapter.mjs +417 -0
- package/src/todo-structure-store.mjs +475 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalizeTodoArtifact,
|
|
3
|
+
exactRecord,
|
|
4
|
+
isStrictTodoTimestamp,
|
|
5
|
+
isTodoDigest,
|
|
6
|
+
isTodoIdentifier,
|
|
7
|
+
isTodoRef,
|
|
8
|
+
todoSelfDigest,
|
|
9
|
+
} from './todo-contracts.mjs';
|
|
10
|
+
|
|
11
|
+
export const TODO_STRUCTURE_SET_SCHEMA = 'lattice.todo_structure_set.v1';
|
|
12
|
+
export const TODO_STRUCTURE_REALIZATION_SCHEMA = 'lattice.todo_structure_realization.v1';
|
|
13
|
+
export const TODO_STRUCTURE_BINDING_SCHEMA = 'lattice.todo_structure_binding.v1';
|
|
14
|
+
export const TODO_STRUCTURE_COMPILE_ARTIFACT_SCHEMA = 'lattice.todo_structure_compile_artifact.v1';
|
|
15
|
+
export const TODO_STRUCTURE_PROFILE = 'code-dataflow';
|
|
16
|
+
export const TODO_STRUCTURE_CONTRACT_ERROR = 'TODO_STRUCTURE_CONTRACT_INVALID';
|
|
17
|
+
|
|
18
|
+
export const TODO_STRUCTURE_LIMITS = Object.freeze({
|
|
19
|
+
tasks: 512,
|
|
20
|
+
externalContracts: 256,
|
|
21
|
+
portsPerTask: 256,
|
|
22
|
+
operationsPerTask: 256,
|
|
23
|
+
anchorsPerTask: 256,
|
|
24
|
+
sinksPerOutput: 256,
|
|
25
|
+
identifiersPerList: 256,
|
|
26
|
+
textList: 128,
|
|
27
|
+
textBytes: 4_096,
|
|
28
|
+
constantBytes: 16_384,
|
|
29
|
+
commitsPerRealization: 256,
|
|
30
|
+
totalAnchors: 512,
|
|
31
|
+
totalPorts: 4_096,
|
|
32
|
+
totalOperations: 2_048,
|
|
33
|
+
totalSinks: 4_096,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const GIT_SHA = /^[0-9a-f]{40}$/u;
|
|
37
|
+
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
38
|
+
const JSON_POINTER = /^(?:\/(?:[^~/]|~[01])*)*$/u;
|
|
39
|
+
|
|
40
|
+
const isGitSha = (value) => typeof value === 'string' && GIT_SHA.test(value);
|
|
41
|
+
const isPlain = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
42
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
43
|
+
const boundedText = (value, maximumBytes = TODO_STRUCTURE_LIMITS.textBytes) => typeof value === 'string'
|
|
44
|
+
&& value.length > 0 && Buffer.byteLength(value, 'utf8') <= maximumBytes && !CONTROL.test(value);
|
|
45
|
+
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
46
|
+
const strictlySorted = (values, key = (value) => value) => values.every((value, index) => index === 0
|
|
47
|
+
|| compareText(key(values[index - 1]), key(value)) < 0);
|
|
48
|
+
const boundedList = (value, limit, predicate) => Array.isArray(value) && value.length <= limit
|
|
49
|
+
&& value.every(predicate);
|
|
50
|
+
const pointerPart = (value) => String(value).replaceAll('~', '~0').replaceAll('/', '~1');
|
|
51
|
+
|
|
52
|
+
function rejection(reason, path = '', detail = undefined) {
|
|
53
|
+
return {
|
|
54
|
+
valid: false,
|
|
55
|
+
code: TODO_STRUCTURE_CONTRACT_ERROR,
|
|
56
|
+
reason,
|
|
57
|
+
path,
|
|
58
|
+
...(detail === undefined ? {} : { detail }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sortedIdentifiers(value, { minimum = 0, maximum = TODO_STRUCTURE_LIMITS.identifiersPerList } = {}) {
|
|
63
|
+
return Array.isArray(value) && value.length >= minimum && value.length <= maximum
|
|
64
|
+
&& value.every(isTodoIdentifier) && strictlySorted(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sortedText(value) {
|
|
68
|
+
return boundedList(value, TODO_STRUCTURE_LIMITS.textList, (entry) => boundedText(entry))
|
|
69
|
+
&& strictlySorted(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function dataContract(value, at) {
|
|
73
|
+
if (!exactRecord(value, [
|
|
74
|
+
'shape_id', 'schema_ref', 'identity_fields', 'lifecycle', 'cardinality',
|
|
75
|
+
'compatible_shape_ids',
|
|
76
|
+
])) return rejection('unexpected_or_missing_keys', at);
|
|
77
|
+
if (!isTodoIdentifier(value.shape_id)) return rejection('invalid_identifier', `${at}/shape_id`);
|
|
78
|
+
if (!['snapshot', 'event', 'stream', 'mutable_state', 'immutable_artifact'].includes(value.lifecycle)) {
|
|
79
|
+
return rejection('invalid_enum', `${at}/lifecycle`);
|
|
80
|
+
}
|
|
81
|
+
if (!['one', 'optional', 'many'].includes(value.cardinality)) {
|
|
82
|
+
return rejection('invalid_enum', `${at}/cardinality`);
|
|
83
|
+
}
|
|
84
|
+
if (!sortedIdentifiers(value.identity_fields)) {
|
|
85
|
+
return rejection('unsorted_or_duplicate_collection', `${at}/identity_fields`);
|
|
86
|
+
}
|
|
87
|
+
if (!sortedIdentifiers(value.compatible_shape_ids)) {
|
|
88
|
+
return rejection('unsorted_or_duplicate_collection', `${at}/compatible_shape_ids`);
|
|
89
|
+
}
|
|
90
|
+
if (value.compatible_shape_ids.includes(value.shape_id)) {
|
|
91
|
+
return rejection('self_compatibility_redundant', `${at}/compatible_shape_ids`);
|
|
92
|
+
}
|
|
93
|
+
if (value.schema_ref !== null) {
|
|
94
|
+
if (!exactRecord(value.schema_ref, ['path', 'symbol', 'json_pointer'])) {
|
|
95
|
+
return rejection('unexpected_or_missing_keys', `${at}/schema_ref`);
|
|
96
|
+
}
|
|
97
|
+
if (!isTodoRef(value.schema_ref.path)) return rejection('invalid_repo_relative_path', `${at}/schema_ref/path`);
|
|
98
|
+
if (!(value.schema_ref.symbol === null || boundedText(value.schema_ref.symbol, 1_024))) {
|
|
99
|
+
return rejection('invalid_symbol', `${at}/schema_ref/symbol`);
|
|
100
|
+
}
|
|
101
|
+
if (!(value.schema_ref.json_pointer === null
|
|
102
|
+
|| (typeof value.schema_ref.json_pointer === 'string' && JSON_POINTER.test(value.schema_ref.json_pointer)))) {
|
|
103
|
+
return rejection('invalid_json_pointer', `${at}/schema_ref/json_pointer`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { valid: true };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function codeAnchor(value, at) {
|
|
110
|
+
if (!exactRecord(value, ['anchor_id', 'effect', 'path', 'symbol', 'expected_at'])) {
|
|
111
|
+
return rejection('unexpected_or_missing_keys', at);
|
|
112
|
+
}
|
|
113
|
+
if (!isTodoIdentifier(value.anchor_id)) return rejection('invalid_identifier', `${at}/anchor_id`);
|
|
114
|
+
if (!['read', 'modify', 'create', 'delete'].includes(value.effect)) {
|
|
115
|
+
return rejection('invalid_enum', `${at}/effect`);
|
|
116
|
+
}
|
|
117
|
+
if (!isTodoRef(value.path)) return rejection('invalid_repo_relative_path', `${at}/path`);
|
|
118
|
+
if (!(value.symbol === null || boundedText(value.symbol, 1_024))) {
|
|
119
|
+
return rejection('invalid_symbol', `${at}/symbol`);
|
|
120
|
+
}
|
|
121
|
+
if (!['baseline', 'current', 'after_task'].includes(value.expected_at)) {
|
|
122
|
+
return rejection('invalid_enum', `${at}/expected_at`);
|
|
123
|
+
}
|
|
124
|
+
return { valid: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sourceRef(value, at) {
|
|
128
|
+
if (!isPlain(value) || typeof value.kind !== 'string') return rejection('invalid_source_ref', at);
|
|
129
|
+
if (value.kind === 'code') {
|
|
130
|
+
if (!exactRecord(value, ['kind', 'anchor_id']) || !isTodoIdentifier(value.anchor_id)) {
|
|
131
|
+
return rejection('invalid_source_ref', at);
|
|
132
|
+
}
|
|
133
|
+
} else if (value.kind === 'task_output') {
|
|
134
|
+
if (!exactRecord(value, ['kind', 'task_id', 'port_id'])
|
|
135
|
+
|| !isTodoIdentifier(value.task_id) || !isTodoIdentifier(value.port_id)) {
|
|
136
|
+
return rejection('invalid_source_ref', at);
|
|
137
|
+
}
|
|
138
|
+
} else if (value.kind === 'external') {
|
|
139
|
+
if (!exactRecord(value, ['kind', 'contract_id']) || !isTodoIdentifier(value.contract_id)) {
|
|
140
|
+
return rejection('invalid_source_ref', at);
|
|
141
|
+
}
|
|
142
|
+
} else if (value.kind === 'constant') {
|
|
143
|
+
if (!exactRecord(value, ['kind', 'constant_id', 'value']) || !isTodoIdentifier(value.constant_id)) {
|
|
144
|
+
return rejection('invalid_source_ref', at);
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
if (Buffer.byteLength(canonicalizeTodoArtifact(value.value), 'utf8')
|
|
148
|
+
> TODO_STRUCTURE_LIMITS.constantBytes) return rejection('constant_too_large', `${at}/value`);
|
|
149
|
+
} catch {
|
|
150
|
+
return rejection('invalid_json_tree', `${at}/value`);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
return rejection('invalid_enum', `${at}/kind`);
|
|
154
|
+
}
|
|
155
|
+
return { valid: true };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function inputPort(value, at) {
|
|
159
|
+
if (!exactRecord(value, ['port_id', 'source', 'access', 'contract'])) {
|
|
160
|
+
return rejection('unexpected_or_missing_keys', at);
|
|
161
|
+
}
|
|
162
|
+
if (!isTodoIdentifier(value.port_id)) return rejection('invalid_identifier', `${at}/port_id`);
|
|
163
|
+
if (!['read', 'consume', 'observe'].includes(value.access)) return rejection('invalid_enum', `${at}/access`);
|
|
164
|
+
const source = sourceRef(value.source, `${at}/source`);
|
|
165
|
+
if (!source.valid) return source;
|
|
166
|
+
return dataContract(value.contract, `${at}/contract`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function outputSink(value, at) {
|
|
170
|
+
if (!isPlain(value) || typeof value.kind !== 'string') return rejection('invalid_output_sink', at);
|
|
171
|
+
if (value.kind === 'task') {
|
|
172
|
+
if (!exactRecord(value, ['kind', 'task_id', 'port_id'])
|
|
173
|
+
|| !isTodoIdentifier(value.task_id) || !isTodoIdentifier(value.port_id)) {
|
|
174
|
+
return rejection('invalid_output_sink', at);
|
|
175
|
+
}
|
|
176
|
+
} else if (value.kind === 'code') {
|
|
177
|
+
if (!exactRecord(value, ['kind', 'anchor_id']) || !isTodoIdentifier(value.anchor_id)) {
|
|
178
|
+
return rejection('invalid_output_sink', at);
|
|
179
|
+
}
|
|
180
|
+
} else if (value.kind === 'external') {
|
|
181
|
+
if (!exactRecord(value, ['kind', 'contract_id']) || !isTodoIdentifier(value.contract_id)) {
|
|
182
|
+
return rejection('invalid_output_sink', at);
|
|
183
|
+
}
|
|
184
|
+
} else if (value.kind === 'final_product') {
|
|
185
|
+
if (!exactRecord(value, ['kind', 'product_id']) || !isTodoIdentifier(value.product_id)) {
|
|
186
|
+
return rejection('invalid_output_sink', at);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
return rejection('invalid_enum', `${at}/kind`);
|
|
190
|
+
}
|
|
191
|
+
return { valid: true };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function sinkKey(value) {
|
|
195
|
+
if (value.kind === 'task') return `task\0${value.task_id}\0${value.port_id}`;
|
|
196
|
+
if (value.kind === 'code') return `code\0${value.anchor_id}`;
|
|
197
|
+
if (value.kind === 'external') return `external\0${value.contract_id}`;
|
|
198
|
+
return `final_product\0${value.product_id}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function outputPort(value, at) {
|
|
202
|
+
if (!exactRecord(value, ['port_id', 'data_id', 'contract', 'sinks'])) {
|
|
203
|
+
return rejection('unexpected_or_missing_keys', at);
|
|
204
|
+
}
|
|
205
|
+
if (!isTodoIdentifier(value.port_id)) return rejection('invalid_identifier', `${at}/port_id`);
|
|
206
|
+
if (!isTodoIdentifier(value.data_id)) return rejection('invalid_identifier', `${at}/data_id`);
|
|
207
|
+
const contract = dataContract(value.contract, `${at}/contract`);
|
|
208
|
+
if (!contract.valid) return contract;
|
|
209
|
+
if (!boundedList(value.sinks, TODO_STRUCTURE_LIMITS.sinksPerOutput, () => true)) {
|
|
210
|
+
return rejection('bounded_collection_violation', `${at}/sinks`);
|
|
211
|
+
}
|
|
212
|
+
for (const [index, sink] of value.sinks.entries()) {
|
|
213
|
+
const result = outputSink(sink, `${at}/sinks/${index}`);
|
|
214
|
+
if (!result.valid) return result;
|
|
215
|
+
}
|
|
216
|
+
if (!strictlySorted(value.sinks, sinkKey)) {
|
|
217
|
+
return rejection('unsorted_or_duplicate_collection', `${at}/sinks`);
|
|
218
|
+
}
|
|
219
|
+
return { valid: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function operation(value, at) {
|
|
223
|
+
if (!exactRecord(value, ['operation_id', 'input_port_ids', 'output_port_ids', 'summary'])) {
|
|
224
|
+
return rejection('unexpected_or_missing_keys', at);
|
|
225
|
+
}
|
|
226
|
+
if (!isTodoIdentifier(value.operation_id)) return rejection('invalid_identifier', `${at}/operation_id`);
|
|
227
|
+
if (!sortedIdentifiers(value.input_port_ids)) {
|
|
228
|
+
return rejection('unsorted_or_duplicate_collection', `${at}/input_port_ids`);
|
|
229
|
+
}
|
|
230
|
+
if (!sortedIdentifiers(value.output_port_ids, { minimum: 1 })) {
|
|
231
|
+
return rejection('unsorted_or_duplicate_collection', `${at}/output_port_ids`);
|
|
232
|
+
}
|
|
233
|
+
if (!boundedText(value.summary)) return rejection('invalid_text', `${at}/summary`);
|
|
234
|
+
return { valid: true };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function transform(value, at) {
|
|
238
|
+
if (!exactRecord(value, [
|
|
239
|
+
'outcome', 'inputs', 'operations', 'outputs', 'code_anchors', 'failures',
|
|
240
|
+
'first_live_e2e', 'non_goals',
|
|
241
|
+
])) return rejection('unexpected_or_missing_keys', at);
|
|
242
|
+
if (!boundedText(value.outcome)) return rejection('invalid_text', `${at}/outcome`);
|
|
243
|
+
if (!boundedText(value.first_live_e2e)) return rejection('invalid_text', `${at}/first_live_e2e`);
|
|
244
|
+
if (!boundedList(value.inputs, TODO_STRUCTURE_LIMITS.portsPerTask, () => true)) {
|
|
245
|
+
return rejection('bounded_collection_violation', `${at}/inputs`);
|
|
246
|
+
}
|
|
247
|
+
for (const [index, entry] of value.inputs.entries()) {
|
|
248
|
+
const result = inputPort(entry, `${at}/inputs/${index}`);
|
|
249
|
+
if (!result.valid) return result;
|
|
250
|
+
}
|
|
251
|
+
if (!strictlySorted(value.inputs, (entry) => entry.port_id)) {
|
|
252
|
+
return rejection('unsorted_or_duplicate_collection', `${at}/inputs`);
|
|
253
|
+
}
|
|
254
|
+
if (!boundedList(value.operations, TODO_STRUCTURE_LIMITS.operationsPerTask, () => true)) {
|
|
255
|
+
return rejection('bounded_collection_violation', `${at}/operations`);
|
|
256
|
+
}
|
|
257
|
+
for (const [index, entry] of value.operations.entries()) {
|
|
258
|
+
const result = operation(entry, `${at}/operations/${index}`);
|
|
259
|
+
if (!result.valid) return result;
|
|
260
|
+
}
|
|
261
|
+
if (!strictlySorted(value.operations, (entry) => entry.operation_id)) {
|
|
262
|
+
return rejection('unsorted_or_duplicate_collection', `${at}/operations`);
|
|
263
|
+
}
|
|
264
|
+
if (!boundedList(value.outputs, TODO_STRUCTURE_LIMITS.portsPerTask, () => true)) {
|
|
265
|
+
return rejection('bounded_collection_violation', `${at}/outputs`);
|
|
266
|
+
}
|
|
267
|
+
for (const [index, entry] of value.outputs.entries()) {
|
|
268
|
+
const result = outputPort(entry, `${at}/outputs/${index}`);
|
|
269
|
+
if (!result.valid) return result;
|
|
270
|
+
}
|
|
271
|
+
if (!strictlySorted(value.outputs, (entry) => entry.port_id)) {
|
|
272
|
+
return rejection('unsorted_or_duplicate_collection', `${at}/outputs`);
|
|
273
|
+
}
|
|
274
|
+
if (!boundedList(value.code_anchors, TODO_STRUCTURE_LIMITS.anchorsPerTask, () => true)) {
|
|
275
|
+
return rejection('bounded_collection_violation', `${at}/code_anchors`);
|
|
276
|
+
}
|
|
277
|
+
for (const [index, entry] of value.code_anchors.entries()) {
|
|
278
|
+
const result = codeAnchor(entry, `${at}/code_anchors/${index}`);
|
|
279
|
+
if (!result.valid) return result;
|
|
280
|
+
}
|
|
281
|
+
if (!strictlySorted(value.code_anchors, (entry) => entry.anchor_id)) {
|
|
282
|
+
return rejection('unsorted_or_duplicate_collection', `${at}/code_anchors`);
|
|
283
|
+
}
|
|
284
|
+
if (!sortedText(value.failures)) return rejection('unsorted_or_duplicate_collection', `${at}/failures`);
|
|
285
|
+
if (!sortedText(value.non_goals)) return rejection('unsorted_or_duplicate_collection', `${at}/non_goals`);
|
|
286
|
+
|
|
287
|
+
const inputIds = new Set(value.inputs.map(({ port_id: id }) => id));
|
|
288
|
+
const outputIds = new Set(value.outputs.map(({ port_id: id }) => id));
|
|
289
|
+
for (const outputId of outputIds) {
|
|
290
|
+
if (inputIds.has(outputId)) return rejection('duplicate_port_id', `${at}/outputs`);
|
|
291
|
+
}
|
|
292
|
+
for (const [index, entry] of value.operations.entries()) {
|
|
293
|
+
for (const inputId of entry.input_port_ids) {
|
|
294
|
+
if (!inputIds.has(inputId)) return rejection('input_port_reference_missing', `${at}/operations/${index}/input_port_ids`);
|
|
295
|
+
}
|
|
296
|
+
for (const outputId of entry.output_port_ids) {
|
|
297
|
+
if (!outputIds.has(outputId)) return rejection('output_port_reference_missing', `${at}/operations/${index}/output_port_ids`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return { valid: true };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function externalContract(value, at) {
|
|
304
|
+
if (!exactRecord(value, ['contract_id', 'description', 'contract'])) {
|
|
305
|
+
return rejection('unexpected_or_missing_keys', at);
|
|
306
|
+
}
|
|
307
|
+
if (!isTodoIdentifier(value.contract_id)) return rejection('invalid_identifier', `${at}/contract_id`);
|
|
308
|
+
if (!boundedText(value.description)) return rejection('invalid_text', `${at}/description`);
|
|
309
|
+
return dataContract(value.contract, `${at}/contract`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function taskEntry(value, at) {
|
|
313
|
+
if (!isPlain(value) || !isTodoIdentifier(value.task_id)) {
|
|
314
|
+
return rejection('invalid_task_entry', at);
|
|
315
|
+
}
|
|
316
|
+
if (value.applicability === 'graph') {
|
|
317
|
+
if (!exactRecord(value, ['task_id', 'applicability', 'planned'])) {
|
|
318
|
+
return rejection('unexpected_or_missing_keys', at);
|
|
319
|
+
}
|
|
320
|
+
return transform(value.planned, `${at}/planned`);
|
|
321
|
+
}
|
|
322
|
+
if (value.applicability === 'excluded') {
|
|
323
|
+
if (!exactRecord(value, ['task_id', 'applicability', 'excluded_reason'])) {
|
|
324
|
+
return rejection('unexpected_or_missing_keys', at);
|
|
325
|
+
}
|
|
326
|
+
if (!boundedText(value.excluded_reason)) return rejection('invalid_text', `${at}/excluded_reason`);
|
|
327
|
+
return { valid: true };
|
|
328
|
+
}
|
|
329
|
+
return rejection('invalid_enum', `${at}/applicability`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function validateStructureReferences(value) {
|
|
333
|
+
const tasks = new Map(value.tasks.map((entry) => [entry.task_id, entry]));
|
|
334
|
+
const externals = new Set(value.external_contracts.map(({ contract_id: id }) => id));
|
|
335
|
+
const dataIds = new Set();
|
|
336
|
+
for (const [taskIndex, task] of value.tasks.entries()) {
|
|
337
|
+
if (task.applicability !== 'graph') continue;
|
|
338
|
+
const at = `/tasks/${taskIndex}/planned`;
|
|
339
|
+
const anchors = new Set(task.planned.code_anchors.map(({ anchor_id: id }) => id));
|
|
340
|
+
for (const [inputIndex, input] of task.planned.inputs.entries()) {
|
|
341
|
+
const sourceAt = `${at}/inputs/${inputIndex}/source`;
|
|
342
|
+
if (input.source.kind === 'code' && !anchors.has(input.source.anchor_id)) {
|
|
343
|
+
return rejection('code_anchor_reference_missing', sourceAt);
|
|
344
|
+
}
|
|
345
|
+
if (input.source.kind === 'external' && !externals.has(input.source.contract_id)) {
|
|
346
|
+
return rejection('external_contract_reference_missing', sourceAt);
|
|
347
|
+
}
|
|
348
|
+
if (input.source.kind === 'task_output') {
|
|
349
|
+
const producer = tasks.get(input.source.task_id);
|
|
350
|
+
if (producer?.applicability !== 'graph') return rejection('task_output_reference_missing', sourceAt);
|
|
351
|
+
if (!producer.planned.outputs.some(({ port_id: id }) => id === input.source.port_id)) {
|
|
352
|
+
return rejection('task_output_reference_missing', sourceAt);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (const [outputIndex, output] of task.planned.outputs.entries()) {
|
|
357
|
+
if (dataIds.has(output.data_id)) return rejection('duplicate_data_id', `${at}/outputs/${outputIndex}/data_id`);
|
|
358
|
+
dataIds.add(output.data_id);
|
|
359
|
+
for (const [sinkIndex, sink] of output.sinks.entries()) {
|
|
360
|
+
const sinkAt = `${at}/outputs/${outputIndex}/sinks/${sinkIndex}`;
|
|
361
|
+
if (sink.kind === 'code' && !anchors.has(sink.anchor_id)) {
|
|
362
|
+
return rejection('code_anchor_reference_missing', sinkAt);
|
|
363
|
+
}
|
|
364
|
+
if (sink.kind === 'external' && !externals.has(sink.contract_id)) {
|
|
365
|
+
return rejection('external_contract_reference_missing', sinkAt);
|
|
366
|
+
}
|
|
367
|
+
if (sink.kind === 'task') {
|
|
368
|
+
const consumer = tasks.get(sink.task_id);
|
|
369
|
+
if (consumer?.applicability !== 'graph'
|
|
370
|
+
|| !consumer.planned.inputs.some(({ port_id: id }) => id === sink.port_id)) {
|
|
371
|
+
return rejection('task_input_reference_missing', sinkAt);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return { valid: true };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* AI-authored structure setを検証する。expectedTaskIdsを渡した時だけactive planとのcoverageを
|
|
382
|
+
* 照合する。storeを読まない純粋contractなので、sg04のdry-runとCLI schema取得の双方で使える。
|
|
383
|
+
*/
|
|
384
|
+
export function explainTodoStructureSet(value, { expectedTaskIds = null } = {}) {
|
|
385
|
+
try {
|
|
386
|
+
if (!exactRecord(value, [
|
|
387
|
+
'schema', 'project_id', 'plan_key', 'plan_version', 'topology_digest', 'profile',
|
|
388
|
+
'baseline_sha', 'external_contracts', 'tasks', 'structure_set_digest',
|
|
389
|
+
])) return rejection('unexpected_or_missing_keys');
|
|
390
|
+
if (value.schema !== TODO_STRUCTURE_SET_SCHEMA) return rejection('unsupported_schema', '/schema');
|
|
391
|
+
if (!isTodoIdentifier(value.project_id)) return rejection('invalid_identifier', '/project_id');
|
|
392
|
+
if (!isTodoIdentifier(value.plan_key)) return rejection('invalid_identifier', '/plan_key');
|
|
393
|
+
if (!isTodoIdentifier(value.plan_version)) return rejection('invalid_identifier', '/plan_version');
|
|
394
|
+
if (!isTodoDigest(value.topology_digest)) return rejection('invalid_digest', '/topology_digest');
|
|
395
|
+
if (value.profile !== TODO_STRUCTURE_PROFILE) return rejection('invalid_enum', '/profile');
|
|
396
|
+
if (!isGitSha(value.baseline_sha)) return rejection('invalid_git_sha', '/baseline_sha');
|
|
397
|
+
if (!boundedList(value.external_contracts, TODO_STRUCTURE_LIMITS.externalContracts, () => true)) {
|
|
398
|
+
return rejection('bounded_collection_violation', '/external_contracts');
|
|
399
|
+
}
|
|
400
|
+
for (const [index, entry] of value.external_contracts.entries()) {
|
|
401
|
+
const result = externalContract(entry, `/external_contracts/${index}`);
|
|
402
|
+
if (!result.valid) return result;
|
|
403
|
+
}
|
|
404
|
+
if (!strictlySorted(value.external_contracts, (entry) => entry.contract_id)) {
|
|
405
|
+
return rejection('unsorted_or_duplicate_collection', '/external_contracts');
|
|
406
|
+
}
|
|
407
|
+
if (!Array.isArray(value.tasks) || value.tasks.length < 1
|
|
408
|
+
|| value.tasks.length > TODO_STRUCTURE_LIMITS.tasks) {
|
|
409
|
+
return rejection('bounded_collection_violation', '/tasks');
|
|
410
|
+
}
|
|
411
|
+
for (const [index, entry] of value.tasks.entries()) {
|
|
412
|
+
const result = taskEntry(entry, `/tasks/${index}`);
|
|
413
|
+
if (!result.valid) return result;
|
|
414
|
+
}
|
|
415
|
+
if (!strictlySorted(value.tasks, (entry) => entry.task_id)) {
|
|
416
|
+
return rejection('unsorted_or_duplicate_collection', '/tasks');
|
|
417
|
+
}
|
|
418
|
+
const graphTasks = value.tasks.filter(({ applicability }) => applicability === 'graph');
|
|
419
|
+
const totals = {
|
|
420
|
+
anchors: graphTasks.reduce((sum, task) => sum + task.planned.code_anchors.length, 0),
|
|
421
|
+
ports: graphTasks.reduce((sum, task) => sum
|
|
422
|
+
+ task.planned.inputs.length + task.planned.outputs.length, 0),
|
|
423
|
+
operations: graphTasks.reduce((sum, task) => sum + task.planned.operations.length, 0),
|
|
424
|
+
sinks: graphTasks.reduce((sum, task) => sum
|
|
425
|
+
+ task.planned.outputs.reduce((count, output) => count + output.sinks.length, 0), 0),
|
|
426
|
+
};
|
|
427
|
+
for (const [kind, limit] of [
|
|
428
|
+
['anchors', TODO_STRUCTURE_LIMITS.totalAnchors],
|
|
429
|
+
['ports', TODO_STRUCTURE_LIMITS.totalPorts],
|
|
430
|
+
['operations', TODO_STRUCTURE_LIMITS.totalOperations],
|
|
431
|
+
['sinks', TODO_STRUCTURE_LIMITS.totalSinks],
|
|
432
|
+
]) {
|
|
433
|
+
if (totals[kind] > limit) {
|
|
434
|
+
return rejection('bounded_total_collection_violation', '/tasks', {
|
|
435
|
+
collection: kind, limit, actual: totals[kind],
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const references = validateStructureReferences(value);
|
|
440
|
+
if (!references.valid) return references;
|
|
441
|
+
if (expectedTaskIds !== null) {
|
|
442
|
+
if (!Array.isArray(expectedTaskIds) || !expectedTaskIds.every(isTodoIdentifier)) {
|
|
443
|
+
throw new TypeError('expectedTaskIds must be an identifier array');
|
|
444
|
+
}
|
|
445
|
+
const expected = [...new Set(expectedTaskIds)].sort(compareText);
|
|
446
|
+
if (expected.length !== expectedTaskIds.length) throw new TypeError('expectedTaskIds must be unique');
|
|
447
|
+
const actual = value.tasks.map(({ task_id: id }) => id);
|
|
448
|
+
const actualSet = new Set(actual); const expectedSet = new Set(expected);
|
|
449
|
+
const missing = expected.filter((id) => !actualSet.has(id));
|
|
450
|
+
const extra = actual.filter((id) => !expectedSet.has(id));
|
|
451
|
+
if (missing.length > 0) return rejection('coverage_missing', '/tasks', { task_ids: missing });
|
|
452
|
+
if (extra.length > 0) return rejection('coverage_extra', '/tasks', { task_ids: extra });
|
|
453
|
+
}
|
|
454
|
+
if (!isTodoDigest(value.structure_set_digest)) return rejection('invalid_digest', '/structure_set_digest');
|
|
455
|
+
if (value.structure_set_digest !== todoSelfDigest(value, 'structure_set_digest')) {
|
|
456
|
+
return rejection('structure_set_digest_mismatch', '/structure_set_digest');
|
|
457
|
+
}
|
|
458
|
+
return { valid: true };
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (error instanceof TypeError && String(error.message).startsWith('expectedTaskIds')) throw error;
|
|
461
|
+
return rejection('invalid_json_tree');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export const validateTodoStructureSet = (value, options) => explainTodoStructureSet(value, options).valid;
|
|
466
|
+
export const digestTodoStructureTransform = (value) => todoSelfDigest(
|
|
467
|
+
{ schema: 'lattice.todo_structure_transform.v1', transform: value, digest: '' }, 'digest',
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
export function explainTodoStructureRealization(value, { structureSet = null, previous = null,
|
|
471
|
+
priorDigests = null } = {}) {
|
|
472
|
+
try {
|
|
473
|
+
if (!exactRecord(value, [
|
|
474
|
+
'schema', 'project_id', 'plan_key', 'plan_version', 'task_id', 'sequence',
|
|
475
|
+
'previous_digest', 'structure_set_digest', 'planned_digest', 'head_sha', 'commit_oids',
|
|
476
|
+
'realized', 'supersedes', 'actor', 'recorded_at', 'realization_digest',
|
|
477
|
+
])) return rejection('unexpected_or_missing_keys');
|
|
478
|
+
if (value.schema !== TODO_STRUCTURE_REALIZATION_SCHEMA) return rejection('unsupported_schema', '/schema');
|
|
479
|
+
for (const field of ['project_id', 'plan_key', 'plan_version', 'task_id']) {
|
|
480
|
+
if (!isTodoIdentifier(value[field])) return rejection('invalid_identifier', `/${field}`);
|
|
481
|
+
}
|
|
482
|
+
if (!Number.isSafeInteger(value.sequence) || value.sequence < 1) return rejection('invalid_sequence', '/sequence');
|
|
483
|
+
if (value.sequence === 1 ? value.previous_digest !== null : !isTodoDigest(value.previous_digest)) {
|
|
484
|
+
return rejection('invalid_previous_digest', '/previous_digest');
|
|
485
|
+
}
|
|
486
|
+
if (!isTodoDigest(value.structure_set_digest)) return rejection('invalid_digest', '/structure_set_digest');
|
|
487
|
+
if (!isTodoDigest(value.planned_digest)) return rejection('invalid_digest', '/planned_digest');
|
|
488
|
+
if (!isGitSha(value.head_sha)) return rejection('invalid_git_sha', '/head_sha');
|
|
489
|
+
if (!Array.isArray(value.commit_oids) || value.commit_oids.length < 1
|
|
490
|
+
|| value.commit_oids.length > TODO_STRUCTURE_LIMITS.commitsPerRealization
|
|
491
|
+
|| !value.commit_oids.every(isGitSha)) return rejection('bounded_collection_violation', '/commit_oids');
|
|
492
|
+
if (!strictlySorted(value.commit_oids)) return rejection('unsorted_or_duplicate_collection', '/commit_oids');
|
|
493
|
+
const realized = transform(value.realized, '/realized');
|
|
494
|
+
if (!realized.valid) return realized;
|
|
495
|
+
if (!(value.supersedes === null || isTodoDigest(value.supersedes))) {
|
|
496
|
+
return rejection('invalid_digest', '/supersedes');
|
|
497
|
+
}
|
|
498
|
+
if (value.supersedes === value.realization_digest) return rejection('self_supersedes', '/supersedes');
|
|
499
|
+
if (!exactRecord(value.actor, ['host', 'session', 'agent'])
|
|
500
|
+
|| ![value.actor.host, value.actor.session, value.actor.agent].every(isTodoIdentifier)) {
|
|
501
|
+
return rejection('invalid_actor', '/actor');
|
|
502
|
+
}
|
|
503
|
+
if (!isStrictTodoTimestamp(value.recorded_at)) return rejection('invalid_timestamp', '/recorded_at');
|
|
504
|
+
if (previous !== null) {
|
|
505
|
+
if (!validateTodoStructureRealization(previous)) throw new TypeError('previous must be a valid realization');
|
|
506
|
+
if (value.sequence !== previous.sequence + 1) return rejection('chain_sequence_mismatch', '/sequence');
|
|
507
|
+
if (value.previous_digest !== previous.realization_digest) {
|
|
508
|
+
return rejection('chain_previous_digest_mismatch', '/previous_digest');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (priorDigests !== null) {
|
|
512
|
+
if (!(priorDigests instanceof Set) || ![...priorDigests].every(isTodoDigest)) {
|
|
513
|
+
throw new TypeError('priorDigests must be a Set of digests');
|
|
514
|
+
}
|
|
515
|
+
if (value.supersedes !== null && !priorDigests.has(value.supersedes)) {
|
|
516
|
+
return rejection('supersedes_target_missing', '/supersedes');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (structureSet !== null) {
|
|
520
|
+
const setResult = explainTodoStructureSet(structureSet);
|
|
521
|
+
if (!setResult.valid) throw new TypeError('structureSet must be valid');
|
|
522
|
+
for (const field of ['project_id', 'plan_key', 'plan_version']) {
|
|
523
|
+
if (value[field] !== structureSet[field]) return rejection('structure_identity_mismatch', `/${field}`);
|
|
524
|
+
}
|
|
525
|
+
if (value.structure_set_digest !== structureSet.structure_set_digest) {
|
|
526
|
+
return rejection('structure_identity_mismatch', '/structure_set_digest');
|
|
527
|
+
}
|
|
528
|
+
const task = structureSet.tasks.find(({ task_id: id }) => id === value.task_id);
|
|
529
|
+
if (task?.applicability !== 'graph') return rejection('realization_task_not_applicable', '/task_id');
|
|
530
|
+
if (value.planned_digest !== digestTodoStructureTransform(task.planned)) {
|
|
531
|
+
return rejection('planned_digest_mismatch', '/planned_digest');
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (!isTodoDigest(value.realization_digest)) return rejection('invalid_digest', '/realization_digest');
|
|
535
|
+
if (value.realization_digest !== todoSelfDigest(value, 'realization_digest')) {
|
|
536
|
+
return rejection('realization_digest_mismatch', '/realization_digest');
|
|
537
|
+
}
|
|
538
|
+
return { valid: true };
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (error instanceof TypeError && ['previous must', 'priorDigests must', 'structureSet must']
|
|
541
|
+
.some((prefix) => String(error.message).startsWith(prefix))) throw error;
|
|
542
|
+
return rejection('invalid_json_tree');
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export const validateTodoStructureRealization = (value, options) => explainTodoStructureRealization(value, options).valid;
|
|
547
|
+
|
|
548
|
+
export function explainTodoStructureBinding(value) {
|
|
549
|
+
try {
|
|
550
|
+
if (!exactRecord(value, [
|
|
551
|
+
'schema', 'project_id', 'plan_key', 'plan_version', 'topology_digest', 'profile',
|
|
552
|
+
'baseline_sha', 'structure_set_digest', 'compiled_head_sha', 'compile_artifact_digest',
|
|
553
|
+
'activated_at', 'actor', 'binding_digest',
|
|
554
|
+
])) return rejection('unexpected_or_missing_keys');
|
|
555
|
+
if (value.schema !== TODO_STRUCTURE_BINDING_SCHEMA) return rejection('unsupported_schema', '/schema');
|
|
556
|
+
for (const field of ['project_id', 'plan_key', 'plan_version']) {
|
|
557
|
+
if (!isTodoIdentifier(value[field])) return rejection('invalid_identifier', `/${field}`);
|
|
558
|
+
}
|
|
559
|
+
if (!isTodoDigest(value.topology_digest)) return rejection('invalid_digest', '/topology_digest');
|
|
560
|
+
if (value.profile !== TODO_STRUCTURE_PROFILE) return rejection('invalid_enum', '/profile');
|
|
561
|
+
if (!isGitSha(value.baseline_sha)) return rejection('invalid_git_sha', '/baseline_sha');
|
|
562
|
+
if (!isTodoDigest(value.structure_set_digest)) return rejection('invalid_digest', '/structure_set_digest');
|
|
563
|
+
if (!isGitSha(value.compiled_head_sha)) return rejection('invalid_git_sha', '/compiled_head_sha');
|
|
564
|
+
if (!isTodoDigest(value.compile_artifact_digest)) return rejection('invalid_digest', '/compile_artifact_digest');
|
|
565
|
+
if (!isStrictTodoTimestamp(value.activated_at)) return rejection('invalid_timestamp', '/activated_at');
|
|
566
|
+
if (!exactRecord(value.actor, ['host', 'session', 'agent'])
|
|
567
|
+
|| ![value.actor.host, value.actor.session, value.actor.agent].every(isTodoIdentifier)) {
|
|
568
|
+
return rejection('invalid_actor', '/actor');
|
|
569
|
+
}
|
|
570
|
+
if (!isTodoDigest(value.binding_digest)) return rejection('invalid_digest', '/binding_digest');
|
|
571
|
+
if (value.binding_digest !== todoSelfDigest(value, 'binding_digest')) {
|
|
572
|
+
return rejection('binding_digest_mismatch', '/binding_digest');
|
|
573
|
+
}
|
|
574
|
+
return { valid: true };
|
|
575
|
+
} catch {
|
|
576
|
+
return rejection('invalid_json_tree');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export const validateTodoStructureBinding = (value) => explainTodoStructureBinding(value).valid;
|
|
581
|
+
|
|
582
|
+
function realizationHead(value, at) {
|
|
583
|
+
if (!exactRecord(value, ['task_id', 'sequence', 'realization_digest'])) {
|
|
584
|
+
return rejection('unexpected_or_missing_keys', at);
|
|
585
|
+
}
|
|
586
|
+
if (!isTodoIdentifier(value.task_id)) return rejection('invalid_identifier', `${at}/task_id`);
|
|
587
|
+
if (!Number.isSafeInteger(value.sequence) || value.sequence < 1) {
|
|
588
|
+
return rejection('invalid_sequence', `${at}/sequence`);
|
|
589
|
+
}
|
|
590
|
+
if (!isTodoDigest(value.realization_digest)) {
|
|
591
|
+
return rejection('invalid_digest', `${at}/realization_digest`);
|
|
592
|
+
}
|
|
593
|
+
return { valid: true };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** realization chainの現行headだけをportableな鮮度identityへ畳む。 */
|
|
597
|
+
export function digestTodoStructureRealizationHeads(heads) {
|
|
598
|
+
if (!Array.isArray(heads) || heads.length > TODO_STRUCTURE_LIMITS.tasks) {
|
|
599
|
+
throw new TypeError('realization heads must be a bounded array');
|
|
600
|
+
}
|
|
601
|
+
for (const [index, head] of heads.entries()) {
|
|
602
|
+
const result = realizationHead(head, `/realization_heads/${index}`);
|
|
603
|
+
if (!result.valid) throw new TypeError(`realization head invalid: ${result.reason}`);
|
|
604
|
+
}
|
|
605
|
+
if (!strictlySorted(heads, (entry) => entry.task_id)) {
|
|
606
|
+
throw new TypeError('realization heads must be sorted and unique');
|
|
607
|
+
}
|
|
608
|
+
return todoSelfDigest({
|
|
609
|
+
schema: 'lattice.todo_structure_realization_heads.v1',
|
|
610
|
+
heads,
|
|
611
|
+
realization_head_digest: '',
|
|
612
|
+
}, 'realization_head_digest');
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* compile時に保存するbounded derived artifactのexact contract。
|
|
617
|
+
* nested artifactはそれぞれのself digestと相互bindingまで検証し、sensorを再実行しない。
|
|
618
|
+
*/
|
|
619
|
+
export function explainTodoStructureCompileArtifact(value) {
|
|
620
|
+
try {
|
|
621
|
+
if (!exactRecord(value, [
|
|
622
|
+
'schema', 'project_id', 'plan_key', 'plan_version', 'topology_digest', 'profile',
|
|
623
|
+
'baseline_sha', 'current_head_sha', 'structure_set_digest', 'source_projection',
|
|
624
|
+
'git_provenance', 'realization_heads', 'realization_head_digest', 'overlay',
|
|
625
|
+
'compiled_at', 'actor', 'artifact_digest',
|
|
626
|
+
])) return rejection('unexpected_or_missing_keys');
|
|
627
|
+
if (value.schema !== TODO_STRUCTURE_COMPILE_ARTIFACT_SCHEMA) {
|
|
628
|
+
return rejection('unsupported_schema', '/schema');
|
|
629
|
+
}
|
|
630
|
+
for (const field of ['project_id', 'plan_key', 'plan_version']) {
|
|
631
|
+
if (!isTodoIdentifier(value[field])) return rejection('invalid_identifier', `/${field}`);
|
|
632
|
+
}
|
|
633
|
+
if (!isTodoDigest(value.topology_digest)) return rejection('invalid_digest', '/topology_digest');
|
|
634
|
+
if (value.profile !== TODO_STRUCTURE_PROFILE) return rejection('invalid_enum', '/profile');
|
|
635
|
+
if (!isGitSha(value.baseline_sha)) return rejection('invalid_git_sha', '/baseline_sha');
|
|
636
|
+
if (!isGitSha(value.current_head_sha)) return rejection('invalid_git_sha', '/current_head_sha');
|
|
637
|
+
if (!isTodoDigest(value.structure_set_digest)) {
|
|
638
|
+
return rejection('invalid_digest', '/structure_set_digest');
|
|
639
|
+
}
|
|
640
|
+
const source = value.source_projection;
|
|
641
|
+
if (!isPlain(source)
|
|
642
|
+
|| source.schema !== 'lattice.todo_structure_source_projection.v1'
|
|
643
|
+
|| source.structure_set_digest !== value.structure_set_digest
|
|
644
|
+
|| !isTodoDigest(source.projection_digest)
|
|
645
|
+
|| source.projection_digest !== todoSelfDigest(source, 'projection_digest')) {
|
|
646
|
+
return rejection('source_projection_invalid', '/source_projection');
|
|
647
|
+
}
|
|
648
|
+
const provenance = value.git_provenance;
|
|
649
|
+
if (!isPlain(provenance)
|
|
650
|
+
|| provenance.schema !== 'lattice.todo_structure_git_provenance.v1'
|
|
651
|
+
|| provenance.structure_set_digest !== value.structure_set_digest
|
|
652
|
+
|| provenance.baseline_sha !== value.baseline_sha
|
|
653
|
+
|| provenance.head_sha !== value.current_head_sha
|
|
654
|
+
|| !isTodoDigest(provenance.provenance_digest)
|
|
655
|
+
|| provenance.provenance_digest !== todoSelfDigest(provenance, 'provenance_digest')) {
|
|
656
|
+
return rejection('git_provenance_invalid', '/git_provenance');
|
|
657
|
+
}
|
|
658
|
+
if (!Array.isArray(value.realization_heads)
|
|
659
|
+
|| value.realization_heads.length > TODO_STRUCTURE_LIMITS.tasks) {
|
|
660
|
+
return rejection('bounded_collection_violation', '/realization_heads');
|
|
661
|
+
}
|
|
662
|
+
for (const [index, head] of value.realization_heads.entries()) {
|
|
663
|
+
const result = realizationHead(head, `/realization_heads/${index}`);
|
|
664
|
+
if (!result.valid) return result;
|
|
665
|
+
}
|
|
666
|
+
if (!strictlySorted(value.realization_heads, (entry) => entry.task_id)) {
|
|
667
|
+
return rejection('unsorted_or_duplicate_collection', '/realization_heads');
|
|
668
|
+
}
|
|
669
|
+
if (!isTodoDigest(value.realization_head_digest)
|
|
670
|
+
|| value.realization_head_digest
|
|
671
|
+
!== digestTodoStructureRealizationHeads(value.realization_heads)) {
|
|
672
|
+
return rejection('realization_head_digest_mismatch', '/realization_head_digest');
|
|
673
|
+
}
|
|
674
|
+
const overlay = value.overlay;
|
|
675
|
+
if (!isPlain(overlay) || overlay.schema !== 'lattice.todo_structure_overlay.v1'
|
|
676
|
+
|| overlay.structure_set_digest !== value.structure_set_digest
|
|
677
|
+
|| overlay.source_projection_digest !== source.projection_digest
|
|
678
|
+
|| overlay.git_provenance_digest !== provenance.provenance_digest
|
|
679
|
+
|| !isTodoDigest(overlay.overlay_digest)
|
|
680
|
+
|| overlay.overlay_digest !== todoSelfDigest(overlay, 'overlay_digest')) {
|
|
681
|
+
return rejection('overlay_invalid', '/overlay');
|
|
682
|
+
}
|
|
683
|
+
if (!isStrictTodoTimestamp(value.compiled_at)) {
|
|
684
|
+
return rejection('invalid_timestamp', '/compiled_at');
|
|
685
|
+
}
|
|
686
|
+
if (!exactRecord(value.actor, ['host', 'session', 'agent'])
|
|
687
|
+
|| ![value.actor.host, value.actor.session, value.actor.agent].every(isTodoIdentifier)) {
|
|
688
|
+
return rejection('invalid_actor', '/actor');
|
|
689
|
+
}
|
|
690
|
+
if (!isTodoDigest(value.artifact_digest)) return rejection('invalid_digest', '/artifact_digest');
|
|
691
|
+
if (value.artifact_digest !== todoSelfDigest(value, 'artifact_digest')) {
|
|
692
|
+
return rejection('artifact_digest_mismatch', '/artifact_digest');
|
|
693
|
+
}
|
|
694
|
+
return { valid: true };
|
|
695
|
+
} catch {
|
|
696
|
+
return rejection('invalid_json_tree');
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export const validateTodoStructureCompileArtifact = (value) => (
|
|
701
|
+
explainTodoStructureCompileArtifact(value).valid
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
/** fixture/writerがcontractと同じcanonical orderを作るための公開比較key。 */
|
|
705
|
+
export const todoStructureSinkKey = sinkKey;
|
|
706
|
+
export const todoStructurePointerPart = pointerPart;
|